Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91f664c4d5 | ||
|
|
ce55aeeadc | ||
|
|
8f66961c7b | ||
|
|
121558b1fd | ||
|
|
df60709e6d | ||
|
|
d208ba02df | ||
|
|
5db3bb0bd0 | ||
|
|
9ad21eb5bb | ||
|
|
4752eb8ad7 | ||
|
|
887b8f3aa3 | ||
|
|
2f3643f09c | ||
|
|
cc529afeb2 | ||
|
|
15a7c4e18a | ||
|
|
7d725f99f4 | ||
|
|
e9ef6565a7 | ||
|
|
47bb92144e | ||
|
|
d3698a1504 | ||
|
|
42062dca55 | ||
|
|
5502191725 | ||
|
|
8e2865efbd | ||
|
|
99eb42b319 | ||
|
|
8243e95ef1 | ||
|
|
0752df0f62 | ||
|
|
98303f7ce8 | ||
|
|
7982df922b | ||
|
|
8da18a6a86 | ||
|
|
3b05704e97 | ||
|
|
00594d9eb2 | ||
|
|
af25a31639 | ||
|
|
f52e7f4682 | ||
|
|
e2f6d326aa | ||
|
|
b18bd4d021 | ||
|
|
738cde12bb | ||
|
|
b147590a39 | ||
|
|
d52cc1c509 | ||
|
|
9d4f92c98b | ||
|
|
f51fe3d820 | ||
|
|
fe17c35c67 | ||
|
|
8fd06887d1 | ||
|
|
580c13e854 | ||
|
|
645db217fa | ||
|
|
cd961cf718 | ||
|
|
c0214b233b | ||
|
|
d054d1b246 | ||
|
|
cd5bb94223 | ||
|
|
178ce486c2 | ||
|
|
811c6c482d | ||
|
|
59cd9b524c | ||
|
|
28e55a0615 | ||
|
|
5c5cd1d5da | ||
|
|
377e5d0b97 | ||
|
|
648a5932f8 | ||
|
|
b54f3fc622 | ||
|
|
63bb41425a | ||
|
|
9a2c677153 | ||
|
|
4a9a037cd8 | ||
|
|
67b06c0a10 | ||
|
|
368f9d2d6a | ||
|
|
651db50a93 |
No files matched your search
@@ -33,7 +33,10 @@ configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/versi
|
||||
# Sources comuns del sistema d'entitats: s'afegeixen tant a l'executable
|
||||
# principal com al smoke test. Mantenir-los en variable separada evita
|
||||
# duplicació entre els dos targets.
|
||||
set(ENTITY_SOURCES
|
||||
# Entitats sense dependència directa amb Player.cpp: les enllacen tant
|
||||
# l'executable principal com el test_entities_smoke (que evita arrossegar
|
||||
# l'stack del jugador i les seues dependències pesades).
|
||||
set(ENTITY_SOURCES_BASE
|
||||
source/game/entities/entity.cpp
|
||||
source/game/entities/animation_component.cpp
|
||||
source/game/entities/event_bus.cpp
|
||||
@@ -50,12 +53,21 @@ set(ENTITY_SOURCES
|
||||
source/game/entities/concrete/dust.cpp
|
||||
)
|
||||
|
||||
# Entitats que criden directament símbols de Player (com WhipAttack via
|
||||
# `enterHurt`/`getBaseBox`): només van a l'executable principal.
|
||||
set(ENTITY_SOURCES
|
||||
${ENTITY_SOURCES_BASE}
|
||||
source/game/entities/concrete/whip_attack.cpp
|
||||
)
|
||||
|
||||
set(APP_SOURCES
|
||||
source/main.cpp
|
||||
source/core/system/director.cpp
|
||||
source/core/audio/audio.cpp
|
||||
source/core/audio/audio_adapter.cpp
|
||||
source/core/audio/audio_effects.cpp
|
||||
source/core/audio/jail_audio.cpp
|
||||
source/core/audio/sound_effects_config.cpp
|
||||
source/core/locale/locale.cpp
|
||||
source/core/ui/ui_config.cpp
|
||||
source/core/input/key_config.cpp
|
||||
@@ -95,11 +107,13 @@ set(APP_SOURCES
|
||||
source/game/scenes/title_scene.cpp
|
||||
source/game/scenes/intro_scene.cpp
|
||||
source/game/scenes/game_scene.cpp
|
||||
source/game/scenes/room_fsm.cpp
|
||||
source/game/entities/player.cpp
|
||||
source/game/map/map.cpp
|
||||
source/game/map/tilemap.cpp
|
||||
source/game/map/tilemap_writer.cpp
|
||||
source/game/map/paint_layer.cpp
|
||||
source/game/levels/level_manager.cpp
|
||||
# Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`).
|
||||
# En Release el fitxer compila a un object buit i l'enllaçador no
|
||||
# arrossega cap símbol del joc final.
|
||||
@@ -162,6 +176,15 @@ endif()
|
||||
|
||||
add_executable(${PROJECT_NAME} ${APP_SOURCES})
|
||||
|
||||
# Silencia warnings de codi de tercers (mateixa filosofia que el
|
||||
# .clang-tidy de source/external/). Només afecta aquests TUs concrets;
|
||||
# la resta del codi continua compilant amb -Wall -Wextra -Wpedantic.
|
||||
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"
|
||||
)
|
||||
|
||||
if(NOT APPLE AND GLSLC_EXE)
|
||||
add_dependencies(${PROJECT_NAME} shaders)
|
||||
endif()
|
||||
@@ -325,7 +348,7 @@ if(BUILD_TESTS)
|
||||
source/core/resources/resource_helper.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
source/game/map/map.cpp
|
||||
${ENTITY_SOURCES}
|
||||
${ENTITY_SOURCES_BASE}
|
||||
)
|
||||
target_include_directories(test_entities_smoke PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/source"
|
||||
@@ -338,6 +361,29 @@ if(BUILD_TESTS)
|
||||
COMMAND test_entities_smoke
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
|
||||
# Smoke del WhipAttack. Inclou tot l'stack d'entitats + whip_attack.cpp.
|
||||
# `whip_attack_player_stubs.cpp` proveeix símbols mínims de Player per
|
||||
# satisfer el linker sense arrossegar audio/gamepad/screen — els tests
|
||||
# del whip s'enfoquen a hitbox + Mummy, no a friendly fire.
|
||||
add_executable(test_whip_attack
|
||||
tests/test_whip_attack.cpp
|
||||
tests/whip_attack_player_stubs.cpp
|
||||
source/core/resources/resource_helper.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
source/game/map/map.cpp
|
||||
${ENTITY_SOURCES_BASE}
|
||||
source/game/entities/concrete/whip_attack.cpp
|
||||
)
|
||||
target_include_directories(test_whip_attack PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/source"
|
||||
"${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
target_link_libraries(test_whip_attack PRIVATE SDL3::SDL3)
|
||||
target_compile_options(test_whip_attack PRIVATE -Wall -Wextra -Wpedantic)
|
||||
add_test(NAME whip_attack
|
||||
COMMAND test_whip_attack
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
|
||||
# Smoke del flux d'escenes i del parser de debug.yaml. Stand-alone:
|
||||
# només enllaça els helpers purs (debug_config, placeholder_input,
|
||||
# key_config) perquè la integració completa dels placeholders arrossegaria
|
||||
@@ -374,4 +420,28 @@ if(BUILD_TESTS)
|
||||
add_test(NAME tilemap_writer
|
||||
COMMAND test_tilemap_writer
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
|
||||
# Smoke de la FSM de sala (RoomFsm). Cobreix loadConfig (timings i
|
||||
# corbes des de YAML), advance (transicions entre estats), i
|
||||
# formatRoomTitle (locale + substitució `{n}`). Stand-alone: no
|
||||
# arrossega Audio/Screen/Player/EntityManager — només els helpers
|
||||
# purs de la FSM, Locale i UiConfig.
|
||||
add_executable(test_game_fsm
|
||||
tests/test_game_fsm.cpp
|
||||
source/game/scenes/room_fsm.cpp
|
||||
source/core/locale/locale.cpp
|
||||
source/core/ui/ui_config.cpp
|
||||
source/core/resources/resource_helper.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
source/utils/easing.cpp
|
||||
)
|
||||
target_include_directories(test_game_fsm PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/source"
|
||||
"${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
target_link_libraries(test_game_fsm PRIVATE SDL3::SDL3)
|
||||
target_compile_options(test_game_fsm PRIVATE -Wall -Wextra -Wpedantic)
|
||||
add_test(NAME game_fsm
|
||||
COMMAND test_game_fsm
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
endif()
|
||||
@@ -1,13 +1,43 @@
|
||||
PROJECT := aee_2026
|
||||
BUILDDIR := build
|
||||
JOBS ?= $(shell nproc 2>/dev/null || echo 4)
|
||||
|
||||
# VERSION (única font de veritat: source/utils/defines.hpp)
|
||||
VERSION := v$(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
|
||||
# Detecció de plataforma. En Windows CMake defaulteja a "NMake Makefiles"
|
||||
# (que requereix Visual Studio); forcem MinGW Makefiles per usar el g++ de
|
||||
# MinGW. nproc tampoc existeix en cmd.exe → fem servir NUMBER_OF_PROCESSORS.
|
||||
ifeq ($(OS),Windows_NT)
|
||||
# Si Make corre dins MSYS2/Git Bash/MinGW, $(shell ...) usa sh.exe i
|
||||
# "NUL" NO és dispositiu — un redirect "2>NUL" crearia un fitxer literal
|
||||
# anomenat NUL al cwd (i en Windows es complica esborrar-lo). Detectem
|
||||
# MSYSTEM per usar /dev/null en aquests entorns; només cmd.exe pur necessita NUL.
|
||||
ifneq ($(MSYSTEM),)
|
||||
NULDEV := /dev/null
|
||||
else
|
||||
NULDEV := NUL
|
||||
endif
|
||||
JOBS ?= $(NUMBER_OF_PROCESSORS)
|
||||
# Si Ninja està al PATH, l'usem (més ràpid i paral·lelitza millor).
|
||||
# Si no, caiem a MinGW Makefiles. Sense aquesta selecció CMake
|
||||
# defaultejaria a "NMake Makefiles", que requereix Visual Studio.
|
||||
HAS_NINJA := $(shell ninja --version 2>$(NULDEV))
|
||||
ifneq ($(HAS_NINJA),)
|
||||
CMAKE_GEN := -G "Ninja"
|
||||
else
|
||||
CMAKE_GEN := -G "MinGW Makefiles"
|
||||
endif
|
||||
else
|
||||
NULDEV := /dev/null
|
||||
JOBS ?= $(shell nproc 2>/dev/null || echo 4)
|
||||
CMAKE_GEN :=
|
||||
endif
|
||||
|
||||
# VERSION (única font de veritat: source/utils/defines.hpp).
|
||||
# Lazy (=): només es calcula quan s'invoca un target que la usa
|
||||
# (show-version/help). Evita executar grep/sed des de cmd.exe en `make debug`.
|
||||
VERSION = v$(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
|
||||
|
||||
# GIT_HASH calculat al host i passat a CMake (evita problemes si CMake corre en
|
||||
# entorns sense accés a git, p.ex. Docker/emscripten).
|
||||
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null)
|
||||
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>$(NULDEV))
|
||||
ifeq ($(GIT_HASH),)
|
||||
GIT_HASH := unknown
|
||||
endif
|
||||
@@ -26,11 +56,11 @@ CMAKE_DEFS := -DGIT_HASH=$(GIT_HASH)
|
||||
# ==============================================================================
|
||||
|
||||
all:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||
|
||||
debug:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||
|
||||
run: all
|
||||
@@ -57,7 +87,7 @@ rebuild: clean all
|
||||
# del pack encara que res no hagi canviat dins data/. L'empaquetat també es fa
|
||||
# automàticament a cada `make`/`make debug` via el target CMake `resource_pack`.
|
||||
pack:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target pack_resources
|
||||
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
||||
|
||||
@@ -74,23 +104,23 @@ controllerdb:
|
||||
# CODE QUALITY (delegados a cmake)
|
||||
# ==============================================================================
|
||||
format:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target format
|
||||
|
||||
format-check:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target format-check
|
||||
|
||||
tidy:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target tidy
|
||||
|
||||
tidy-fix:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target tidy-fix
|
||||
|
||||
cppcheck:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target cppcheck
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
@@ -5,6 +5,14 @@ sprite:
|
||||
size: [48, 32]
|
||||
tile_size: [3, 2]
|
||||
collision:
|
||||
# Caixa de col·lisió: 16×24 centrada al sprite 48×32. Coincideix amb el
|
||||
# tile central transitable (els 16 px del passadís de sortida) i es
|
||||
# restringeix a la franja del mur trasser (24 px d'alçada). Així
|
||||
# `checkPlayerCollisions` només dispara `onPlayerContact` quan el
|
||||
# jugador entra al forat real, no als marcs flanquejants ni als 8 px
|
||||
# superiors que queden tapats pel HUD.
|
||||
size: [16, 24]
|
||||
offset: [16, 8]
|
||||
# Porta = mur infranquejable. Igual que el sarc, alçada efectiva
|
||||
# infinita per impedir que un jugador la salte per damunt.
|
||||
height: 999
|
||||
|
||||
@@ -38,3 +38,13 @@ ai:
|
||||
stats:
|
||||
speed: 30
|
||||
damage: 1
|
||||
|
||||
# Estat HURT: post-fuetada, fugint en línia recta. Si la direcció
|
||||
# preferida (la del whip) està bloquejada, prova perpendiculars; si totes
|
||||
# ho estan, queda quieta tot el `duration_ms` i després torna a ROAMING.
|
||||
# `speed` opcional: si no apareix, s'usa `stats.speed`. El `whippable`
|
||||
# habilita la reacció al látigo (false ⇒ el whip la ignora).
|
||||
hurt:
|
||||
duration_ms: 600
|
||||
speed: 60
|
||||
whippable: true
|
||||
@@ -0,0 +1,38 @@
|
||||
name: whip_attack
|
||||
|
||||
# Quatre sprites distints, un per cardinal. El sistema d'animació actual
|
||||
# només permet UNA mida de frame per surface, i els sprites del látigo
|
||||
# tenen aspect ratios distints segons direcció. Per això són 4 fitxers
|
||||
# en lloc d'una sola sheet compartida amb flips.
|
||||
#
|
||||
# Mides (= `draw_rect` del WhipAttack, que NO es retalla per
|
||||
# bloquejadors):
|
||||
# E/W → 80×32 px per frame: 16 px de wind-up darrere del jugador
|
||||
# + 16 px de solapament + 48 px d'atac
|
||||
# frontal. Cobreix tota l'altura del jugador.
|
||||
# N/S → 16×96 px per frame: 48 px d'atac + 32 px solapant amb el jugador
|
||||
# + 16 px de wind-up "darrere".
|
||||
#
|
||||
# La caja de DANY és independent i més petita (48×16 / 16×48), pegada
|
||||
# al borde del jugador i retallada per murs/sarcs. La calcula
|
||||
# `WhipAttack::configure` a partir del `owner_box` del payload.
|
||||
sprites:
|
||||
east: { path: whip_east.gif, size: [80, 32] }
|
||||
west: { path: whip_west.gif, size: [80, 32] }
|
||||
north: { path: whip_north.gif, size: [16, 96] }
|
||||
south: { path: whip_south.gif, size: [16, 96] }
|
||||
|
||||
# Animació compartida entre les 4 direccions: la seqüència de frames i les
|
||||
# durades són idèntiques; només canvia quina surface/mida es clipa.
|
||||
# - `frames` : índex de tile dins l'spritesheet (graella deduïda).
|
||||
# - `frame_ms` : durada uniforme per frame; 50 ms × 4 ≈ 200 ms swing.
|
||||
# - `loop: false` : el látigo viu una animació i mor (`markDead`).
|
||||
# - `active_frames` : valors dins de `frames` durant els quals la
|
||||
# hitbox aplica dany. Wind-up (0) i recovery (3) no
|
||||
# danyen — només els frames "actius" 1 i 2.
|
||||
animations:
|
||||
swing:
|
||||
frames: [0, 1, 2, 3]
|
||||
frame_ms: 50
|
||||
loop: false
|
||||
active_frames: [1, 2]
|
||||
|
After Width: | Height: | Size: 313 B |
|
After Width: | Height: | Size: 378 B |
|
After Width: | Height: | Size: 369 B |
|
After Width: | Height: | Size: 313 B |
@@ -0,0 +1,132 @@
|
||||
# Font: CaesarDressing-Regular — generado desde CaesarDressing-Regular.ttf size 24
|
||||
# Generado con font_gen.py
|
||||
|
||||
box_width 18
|
||||
box_height 28
|
||||
columns 15
|
||||
|
||||
# codepoint_decimal ancho_visual
|
||||
32 8 # U+0020
|
||||
33 5 # !
|
||||
34 6 # "
|
||||
35 14 # #
|
||||
36 7 # $
|
||||
37 17 # %
|
||||
38 9 # &
|
||||
39 3 # '
|
||||
40 10 # (
|
||||
41 8 # )
|
||||
42 10 # *
|
||||
43 11 # +
|
||||
44 5 # ,
|
||||
45 10 # -
|
||||
46 3 # .
|
||||
47 9 # /
|
||||
48 12 # 0
|
||||
49 6 # 1
|
||||
50 10 # 2
|
||||
51 9 # 3
|
||||
52 12 # 4
|
||||
53 11 # 5
|
||||
54 13 # 6
|
||||
55 11 # 7
|
||||
56 13 # 8
|
||||
57 13 # 9
|
||||
58 5 # :
|
||||
59 4 # ;
|
||||
60 11 # <
|
||||
61 11 # =
|
||||
62 11 # >
|
||||
63 11 # ?
|
||||
64 14 # @
|
||||
65 14 # A
|
||||
66 12 # B
|
||||
67 11 # C
|
||||
68 12 # D
|
||||
69 10 # E
|
||||
70 10 # F
|
||||
71 15 # G
|
||||
72 12 # H
|
||||
73 9 # I
|
||||
74 9 # J
|
||||
75 13 # K
|
||||
76 10 # L
|
||||
77 18 # M
|
||||
78 12 # N
|
||||
79 13 # O
|
||||
80 12 # P
|
||||
81 12 # Q
|
||||
82 13 # R
|
||||
83 9 # S
|
||||
84 12 # T
|
||||
85 11 # U
|
||||
86 12 # V
|
||||
87 17 # W
|
||||
88 11 # X
|
||||
89 12 # Y
|
||||
90 10 # Z
|
||||
91 8 # [
|
||||
92 8 # \
|
||||
93 7 # ]
|
||||
94 11 # ^
|
||||
95 13 # _
|
||||
96 7 # `
|
||||
97 13 # a
|
||||
98 12 # b
|
||||
99 11 # c
|
||||
100 12 # d
|
||||
101 10 # e
|
||||
102 10 # f
|
||||
103 15 # g
|
||||
104 12 # h
|
||||
105 5 # i
|
||||
106 9 # j
|
||||
107 13 # k
|
||||
108 10 # l
|
||||
109 18 # m
|
||||
110 12 # n
|
||||
111 13 # o
|
||||
112 12 # p
|
||||
113 12 # q
|
||||
114 12 # r
|
||||
115 10 # s
|
||||
116 12 # t
|
||||
117 11 # u
|
||||
118 12 # v
|
||||
119 17 # w
|
||||
120 11 # x
|
||||
121 12 # y
|
||||
122 10 # z
|
||||
123 9 # {
|
||||
124 5 # |
|
||||
125 9 # }
|
||||
126 14 # ~
|
||||
192 14 # À
|
||||
193 14 # Á
|
||||
200 10 # È
|
||||
201 10 # É
|
||||
204 9 # Ì
|
||||
205 9 # Í
|
||||
210 13 # Ò
|
||||
211 13 # Ó
|
||||
217 11 # Ù
|
||||
218 11 # Ú
|
||||
209 12 # Ñ
|
||||
199 11 # Ç
|
||||
224 13 # à
|
||||
225 13 # á
|
||||
232 10 # è
|
||||
233 11 # é
|
||||
236 7 # ì
|
||||
237 8 # í
|
||||
242 13 # ò
|
||||
243 13 # ó
|
||||
249 11 # ù
|
||||
250 11 # ú
|
||||
241 12 # ñ
|
||||
231 11 # ç
|
||||
161 5 # ¡
|
||||
191 10 # ¿
|
||||
171 11 # «
|
||||
187 11 # »
|
||||
183 3 # ·
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -47,6 +47,15 @@ keys:
|
||||
- id: editor_picker
|
||||
code: "T"
|
||||
desc: "Obri/tanca el tilepicker dins de l'editor"
|
||||
- id: prev_room
|
||||
code: "["
|
||||
desc: "Salta a la sala anterior del levels.yaml (només Debug)"
|
||||
- id: next_room
|
||||
code: "]"
|
||||
desc: "Salta a la sala següent del levels.yaml (només Debug)"
|
||||
- id: open_door
|
||||
code: "Keypad 1"
|
||||
desc: "Força l'obertura de la porta de la sala (només Debug)"
|
||||
- id: exit
|
||||
code: "Escape"
|
||||
desc: "Eixir (confirmar amb segona pulsació)"
|
||||
@@ -36,6 +36,17 @@ hud:
|
||||
pyramid: "Piramide"
|
||||
room: "Sala"
|
||||
|
||||
# Textos de la transició d'entrada/sortida de sala (FSM de GameScene).
|
||||
# Tant l'INTRO com l'OUTRO es renderitzen com a dues línies separades
|
||||
# (cadascuna amb la seua animació). L'INTRO accepta els placeholders
|
||||
# `{p}` (piràmide) i `{r}` (sala); l'OUTRO no en té cap — són les
|
||||
# dues paraules del rètol "fase completada".
|
||||
game:
|
||||
room_intro_pyramid: "Piramide {p}"
|
||||
room_intro_room: "Sala {r}"
|
||||
room_complete_line1: "Fase"
|
||||
room_complete_line2: "Completada"
|
||||
|
||||
menu:
|
||||
titles:
|
||||
root: "Menú de servei"
|
||||
|
||||
@@ -36,6 +36,16 @@ hud:
|
||||
pyramid: "Pyramid"
|
||||
room: "Room"
|
||||
|
||||
# Room transition texts (GameScene FSM intro/outro). Both INTRO and
|
||||
# OUTRO render as two independent lines with their own animation.
|
||||
# INTRO uses `{p}` (pyramid) and `{r}` (room) placeholders; OUTRO has
|
||||
# no placeholders — it's the two words of the "stage complete" label.
|
||||
game:
|
||||
room_intro_pyramid: "Pyramid {p}"
|
||||
room_intro_room: "Room {r}"
|
||||
room_complete_line1: "Stage"
|
||||
room_complete_line2: "Complete"
|
||||
|
||||
menu:
|
||||
titles:
|
||||
root: "Service menu"
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Índex ordenat de les 30 sales del joc (5 piràmides × 6 sales). El
|
||||
# LevelManager carrega aquesta llista a l'arrencada i la fa servir com a
|
||||
# font de veritat per a `currentPath()`, `advance()`, `setIndex()` i la
|
||||
# resta de consultes que li facin la GameScene, l'Editor i els salts de
|
||||
# debug. Per reordenar la progressió, canvia l'ordre d'aquesta seqüència
|
||||
# (no cal renombrar fitxers). Per afegir una sala, crea el YAML i posa
|
||||
# una entrada nova; els camps `pyramid` i `room` són purament informatius
|
||||
# (apareixen als logs i al text d'intro de sala) — l'únic camp que
|
||||
# realment carrega l'engine és `file`.
|
||||
#
|
||||
# Convenció de nomenclatura del fitxer: `data/maps/p<N>/r<M>.yaml` per
|
||||
# dins la piràmide N, sala M. La carpeta `p<N>/` agruparà més endavant
|
||||
# les metadades comunes de la piràmide (tileset, paleta, música).
|
||||
|
||||
sequence:
|
||||
- { pyramid: 1, room: 1, file: data/maps/p1/r1.yaml }
|
||||
- { pyramid: 1, room: 2, file: data/maps/p1/r2.yaml }
|
||||
- { pyramid: 1, room: 3, file: data/maps/p1/r3.yaml }
|
||||
- { pyramid: 1, room: 4, file: data/maps/p1/r4.yaml }
|
||||
- { pyramid: 1, room: 5, file: data/maps/p1/r5.yaml }
|
||||
- { pyramid: 1, room: 6, file: data/maps/p1/r6.yaml }
|
||||
- { pyramid: 2, room: 1, file: data/maps/p2/r1.yaml }
|
||||
- { pyramid: 2, room: 2, file: data/maps/p2/r2.yaml }
|
||||
- { pyramid: 2, room: 3, file: data/maps/p2/r3.yaml }
|
||||
- { pyramid: 2, room: 4, file: data/maps/p2/r4.yaml }
|
||||
- { pyramid: 2, room: 5, file: data/maps/p2/r5.yaml }
|
||||
- { pyramid: 2, room: 6, file: data/maps/p2/r6.yaml }
|
||||
- { pyramid: 3, room: 1, file: data/maps/p3/r1.yaml }
|
||||
- { pyramid: 3, room: 2, file: data/maps/p3/r2.yaml }
|
||||
- { pyramid: 3, room: 3, file: data/maps/p3/r3.yaml }
|
||||
- { pyramid: 3, room: 4, file: data/maps/p3/r4.yaml }
|
||||
- { pyramid: 3, room: 5, file: data/maps/p3/r5.yaml }
|
||||
- { pyramid: 3, room: 6, file: data/maps/p3/r6.yaml }
|
||||
- { pyramid: 4, room: 1, file: data/maps/p4/r1.yaml }
|
||||
- { pyramid: 4, room: 2, file: data/maps/p4/r2.yaml }
|
||||
- { pyramid: 4, room: 3, file: data/maps/p4/r3.yaml }
|
||||
- { pyramid: 4, room: 4, file: data/maps/p4/r4.yaml }
|
||||
- { pyramid: 4, room: 5, file: data/maps/p4/r5.yaml }
|
||||
- { pyramid: 4, room: 6, file: data/maps/p4/r6.yaml }
|
||||
- { pyramid: 5, room: 1, file: data/maps/p5/r1.yaml }
|
||||
- { pyramid: 5, room: 2, file: data/maps/p5/r2.yaml }
|
||||
- { pyramid: 5, room: 3, file: data/maps/p5/r3.yaml }
|
||||
- { pyramid: 5, room: 4, file: data/maps/p5/r4.yaml }
|
||||
- { pyramid: 5, room: 5, file: data/maps/p5/r5.yaml }
|
||||
- { pyramid: 5, room: 6, file: data/maps/p5/r6.yaml }
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 1
|
||||
room: 1
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 1
|
||||
room: 2
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 1
|
||||
room: 3
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 1
|
||||
room: 4
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 1
|
||||
room: 5
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 1
|
||||
room: 6
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 2
|
||||
room: 1
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 2
|
||||
room: 2
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 2
|
||||
room: 3
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 2
|
||||
room: 4
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 2
|
||||
room: 5
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 2
|
||||
room: 6
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 3
|
||||
room: 1
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 3
|
||||
room: 2
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 3
|
||||
room: 3
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 3
|
||||
room: 4
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 3
|
||||
room: 5
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 3
|
||||
room: 6
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 4
|
||||
room: 1
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 4
|
||||
room: 2
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 4
|
||||
room: 3
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 4
|
||||
room: 4
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 4
|
||||
room: 5
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 4
|
||||
room: 6
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 5
|
||||
room: 1
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 5
|
||||
room: 2
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 5
|
||||
room: 3
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 5
|
||||
room: 4
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 5
|
||||
room: 5
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
pyramid: 5
|
||||
room: 6
|
||||
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: 4
|
||||
|
||||
# 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]
|
||||
@@ -42,6 +42,12 @@ geometry:
|
||||
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]
|
||||
@@ -123,7 +129,9 @@ entities:
|
||||
- type: sarcophagus
|
||||
sarc_position: [3, 4]
|
||||
- type: door
|
||||
position: [9, 1]
|
||||
# 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
|
||||
|
||||
@@ -68,6 +68,36 @@ animations:
|
||||
frames: [8, 9, 10, 11]
|
||||
frame_ms: 100
|
||||
|
||||
# Placeholder mentre no arribe l'art real (el GIF de morcus encara no té
|
||||
# frames d'atac específics — reaprofitem `walk_*`). El bloc està declarat
|
||||
# perquè `Player::animationName` el busque sense fallback i la mecànica
|
||||
# de l'atac estiga lligada amb morcus tan bon punt s'afegisquen els
|
||||
# frames; ara mateix visualment el sprite caminarà mentre llança el
|
||||
# látigo (acceptat per al placeholder).
|
||||
attack_down:
|
||||
frames: [0, 1, 2, 3]
|
||||
frame_ms: 50
|
||||
|
||||
attack_up:
|
||||
frames: [4, 5, 6, 7]
|
||||
frame_ms: 50
|
||||
|
||||
attack:
|
||||
frames: [8, 9, 10, 11]
|
||||
frame_ms: 50
|
||||
|
||||
# Cooldown post-swing (recovery): igual que a sam.yaml, placeholder als
|
||||
# frames de `stand_*`. Quan arriben els frames reals només cal canviar
|
||||
# els índexs ací.
|
||||
recover_down:
|
||||
frames: [0]
|
||||
|
||||
recover_up:
|
||||
frames: [4]
|
||||
|
||||
recover:
|
||||
frames: [8]
|
||||
|
||||
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
||||
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
||||
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||
@@ -127,3 +157,11 @@ jump:
|
||||
hurt:
|
||||
speed: 120
|
||||
brake_time_ms: 500
|
||||
|
||||
# Configuració del látigo. Mateixos valors que Sam de moment;
|
||||
# diferenciar quan equilibrem cada personatge.
|
||||
whip:
|
||||
range_tiles: 3
|
||||
swing_ms: 200
|
||||
cooldown_ms: 200
|
||||
damage: 1
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -68,6 +68,36 @@ animations:
|
||||
frames: [8, 9, 10, 11]
|
||||
frame_ms: 100
|
||||
|
||||
# Animacions del braç llançant el látigo. Placeholder: reaprofiten els
|
||||
# frames de `walk_*` perquè els sprites reals encara no existeixen
|
||||
# (TODO: dibuixar-ne d'específics quan arribe l'art). El sprite del
|
||||
# látigo viu al `WhipAttack` i es renderitza superposat per z-order.
|
||||
attack_down:
|
||||
frames: [28, 29, 30, 31]
|
||||
frame_ms: 50
|
||||
|
||||
attack_up:
|
||||
frames: [24, 25, 26, 27]
|
||||
frame_ms: 50
|
||||
|
||||
attack:
|
||||
frames: [20, 21, 22, 23]
|
||||
frame_ms: 50
|
||||
|
||||
# Animacions de cooldown post-swing (recovery): mentre dura
|
||||
# `whip_cooldown_ms` el jugador continua bloquejat però l'animació ja
|
||||
# no és la d'atac. Placeholder: reaprofiten els frames de `stand_*`
|
||||
# (la silueta no canvia respecte d'ells); quan arriben els frames
|
||||
# reals només cal canviar els índexs ací, sense tocar el codi.
|
||||
recover_down:
|
||||
frames: [1]
|
||||
|
||||
recover_up:
|
||||
frames: [5]
|
||||
|
||||
recover:
|
||||
frames: [9]
|
||||
|
||||
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
||||
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
||||
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||
@@ -127,3 +157,16 @@ jump:
|
||||
hurt:
|
||||
speed: 120
|
||||
brake_time_ms: 500
|
||||
|
||||
# Configuració del látigo. `range_tiles` és la longitud de la hitbox
|
||||
# abans de retallar contra murs/sarcs (3 tiles ≈ 48 px). `swing_ms` és
|
||||
# la durada de l'animació de l'atac, durant la qual la hitbox del whip
|
||||
# està viva. `cooldown_ms` allarga ATTACKING amb una recuperació en què
|
||||
# el jugador segueix bloquejat però la hitbox ja ha desaparegut: castiga
|
||||
# l'ús abusiu del látigo. `damage` queda llegit per a futur (els
|
||||
# objectius reben `enterHurt` directament; cap entitat té HP encara).
|
||||
whip:
|
||||
range_tiles: 3
|
||||
swing_ms: 200
|
||||
cooldown_ms: 200
|
||||
damage: 1
|
||||
@@ -0,0 +1,49 @@
|
||||
# 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/ui.yaml > pause +
|
||||
# notification). Si manca el fitxer o alguna clau, GameScene cau als
|
||||
# fallbacks compilats; veure `readSceneConfig` a game_scene.cpp.
|
||||
|
||||
room_intro:
|
||||
fade_in_ms: 500 # negre opac → món visible
|
||||
text_delay_ms: 500 # espera amb el món net abans de sortir el text
|
||||
text_enter_ms: 400 # text esquerra → centre
|
||||
text_hold_ms: 2000 # text quiet al centre
|
||||
text_exit_ms: 400 # text centre → fora per l'esquerra
|
||||
line_gap_px: -4 # px extra entre les dues línies (piràmide / sala)
|
||||
player_spawn_delay_ms: 2400 # ms des de l'inici d'INTRO al spawn aeri.
|
||||
player2_spawn_delay_ms: 2700 # només mode BOTH. Trenca la simultaneïtat.
|
||||
# Preset de reverb (catàleg a `data/sounds.yaml`) aplicat al primer
|
||||
# LAND de cada jugador durant la cinemàtica d'entrada. La sala dona
|
||||
# "més detall" sonor a l'INTRO; els salts del joc i els respawns
|
||||
# post-mort cauen al so sec. Buit o absent = sense reverb.
|
||||
land_reverb: stone_hall
|
||||
# So puntual disparat quan apareix el text d'INTRO (phase 2,
|
||||
# text_enter). Es resol contra `data/sounds/<text_sound>`. Buit o
|
||||
# absent = sense so. Es preloda al ctor de GameScene perquè el
|
||||
# primer dispar no faça disc.
|
||||
text_sound: intro_room.wav
|
||||
|
||||
room_outro:
|
||||
text_enter_ms: 400
|
||||
text_hold_ms: 2000
|
||||
text_exit_ms: 400
|
||||
fade_out_ms: 500 # món visible → negre opac
|
||||
line_gap_px: -4 # px extra entre les dues línies (fase / completada)
|
||||
black_hold_ms: 500 # pausa en negre abans d'entrar a la pròxima sala
|
||||
|
||||
text:
|
||||
# Noms d'easing tal com els resol UiConfig::curveByName (vegeu
|
||||
# `data/ui.yaml`). El text entra suau i surt accelerant.
|
||||
enter_curve: outCubic
|
||||
exit_curve: inCubic
|
||||
|
||||
# Panel translúcid pintat sota el rètol de transició (INTRO i OUTRO).
|
||||
# Va centrat verticalment al centre del playfield i ocupa tota
|
||||
# l'amplada del canvas. Creix d'altura amb la mateixa enter_curve del
|
||||
# text i decreix amb l'exit_curve. `color` són tres canals RGB (0..255);
|
||||
# `alpha` és la transparència del blend (0 = invisible, 255 = opac).
|
||||
text_panel:
|
||||
color: [36, 182, 170]
|
||||
alpha: 128
|
||||
height_px: 32
|
||||
@@ -0,0 +1,37 @@
|
||||
# Presets d'efectes per a sons puntuals. Les crides
|
||||
# Audio::playSoundWithEcho(name, preset, group) i
|
||||
# Audio::playSoundWithReverb(name, preset, group) referencien aquests
|
||||
# presets pel seu nom (camp `preset`). Si el nom no existeix, el so es
|
||||
# reprodueix sec amb un avís a stderr.
|
||||
#
|
||||
# Camps:
|
||||
# echo:
|
||||
# delay_ms → temps fins al primer rebot, en mil·lisegons.
|
||||
# feedback → reinjecció (0..0.95). >0.7 sona "metàl·lic".
|
||||
# wet → mescla del senyal humit (0..1). 1 = només eco.
|
||||
# reverb:
|
||||
# room_size → mida percebuda de la sala (0..1).
|
||||
# damping → atenuació de freqüències altes en cada rebot (0..1).
|
||||
# wet → mescla humida (0..1).
|
||||
#
|
||||
# Els processadors limiten la cua a 800 ms (echo) i 1500 ms (reverb).
|
||||
|
||||
echo:
|
||||
cave:
|
||||
delay_ms: 220
|
||||
feedback: 0.45
|
||||
wet: 0.35
|
||||
short:
|
||||
delay_ms: 80
|
||||
feedback: 0.30
|
||||
wet: 0.25
|
||||
|
||||
reverb:
|
||||
stone_hall:
|
||||
room_size: 0.78
|
||||
damping: 0.50
|
||||
wet: 0.40
|
||||
small_room:
|
||||
room_size: 0.40
|
||||
damping: 0.70
|
||||
wet: 0.30
|
||||
@@ -20,6 +20,12 @@ colors:
|
||||
hud_label: 58
|
||||
hud_value: 187
|
||||
hud_flash: 237
|
||||
# Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). Independents
|
||||
# de notification_* perquè la fase i les notificacions són dos canals
|
||||
# visuals distints: ací pots canviar el color del rètol sense tocar
|
||||
# els missatges del HUD.
|
||||
room_text: 160
|
||||
room_text_outline: 253
|
||||
|
||||
timings:
|
||||
notification_ms: 4000
|
||||
@@ -36,6 +42,12 @@ fonts:
|
||||
gif: "data/fonts/ari-w9500-condensed-display.gif"
|
||||
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
|
||||
kerning: 1
|
||||
# Font del rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO).
|
||||
room_text:
|
||||
name: "CaesarDressing-Regular"
|
||||
gif: "data/fonts/CaesarDressing-Regular.gif"
|
||||
fnt: "data/fonts/CaesarDressing-Regular.fnt"
|
||||
kerning: 1
|
||||
menu_title:
|
||||
name: "ari-w9500-display"
|
||||
gif: "data/fonts/ari-w9500-display.gif"
|
||||
@@ -48,7 +60,7 @@ fonts:
|
||||
kerning: 0
|
||||
|
||||
hud:
|
||||
bg_enabled: true
|
||||
bg_enabled: false
|
||||
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
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
#include <algorithm> // Per a clamp
|
||||
#include <cstdio> // Per a std::fprintf
|
||||
|
||||
#include "core/audio/audio_adapter.hpp" // Per a AudioResource::getMusic/getSound
|
||||
#include "core/audio/jail_audio.hpp" // Per a Ja::* (motor jailgames)
|
||||
#include "utils/defaults.hpp" // Per a Defaults::Audio::FREQUENCY
|
||||
#include "core/audio/audio_adapter.hpp" // Per a AudioResource::getMusic/getSound
|
||||
#include "core/audio/jail_audio.hpp" // Per a Ja::* (motor jailgames)
|
||||
#include "core/audio/sound_effects_config.hpp" // Per a SoundEffectsConfig
|
||||
#include "utils/defaults.hpp" // Per a Defaults::Audio::FREQUENCY
|
||||
|
||||
// Invariant compile-time: tots els valors d'Audio::Group han de cabre als slots
|
||||
// de volum per grup que manté l'engine. Si s'afegeix una nova entrada a Group
|
||||
@@ -128,6 +129,47 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (sound == nullptr) { return; }
|
||||
|
||||
const auto* params = SoundEffectsConfig::get().findEcho(preset_name);
|
||||
if (params == nullptr) {
|
||||
std::fprintf(stderr, "Audio: preset d'eco '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine_->playSoundWithEcho(sound, *params, static_cast<int>(group)) < 0) {
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (sound == nullptr) { return; }
|
||||
|
||||
const auto* params = SoundEffectsConfig::get().findReverb(preset_name);
|
||||
if (params == nullptr) {
|
||||
std::fprintf(stderr, "Audio: preset de reverb '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine_->playSoundWithReverb(sound, *params, static_cast<int>(group)) < 0) {
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Atura tots els sons
|
||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
||||
void Audio::stopAllSounds() {
|
||||
|
||||
@@ -80,7 +80,13 @@ class Audio {
|
||||
// --- Control de sons ---
|
||||
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
|
||||
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
|
||||
void stopAllSounds(); // Aturar tots els sons (muta globals de Ja)
|
||||
// Reprodueix un so processat per un efecte definit a data/sounds.yaml
|
||||
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix
|
||||
// o el motor està al cap de canals amb efecte, fa fallback a playSound
|
||||
// sec — l'usuari sent el so igualment, sense la cua.
|
||||
void playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
|
||||
void playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
|
||||
void stopAllSounds(); // Aturar tots els sons (muta globals de Ja)
|
||||
|
||||
// --- Control de volum (API interna: float 0.0..1.0) ---
|
||||
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
#include "core/audio/audio_effects.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include "core/audio/jail_audio.hpp"
|
||||
|
||||
namespace AudioEffects {
|
||||
|
||||
namespace {
|
||||
|
||||
// --- Caps de cua ---
|
||||
constexpr float ECHO_TAIL_MS = 800.0F;
|
||||
constexpr float REVERB_TAIL_MS = 1500.0F;
|
||||
|
||||
// --- Constants Freeverb ---
|
||||
// Delays de comb i allpass tunats per a 44.1 kHz; els reescalem per
|
||||
// freqüència real de la font.
|
||||
constexpr int COMB_REFERENCE_RATE = 44100;
|
||||
constexpr std::array<int, 8> COMB_DELAYS_L = {1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617};
|
||||
constexpr std::array<int, 4> ALLPASS_DELAYS_L = {556, 441, 341, 225};
|
||||
constexpr int STEREO_SPREAD = 23;
|
||||
|
||||
// Mapeig de Schroeder/Dattorro/Freeverb estàndard.
|
||||
constexpr float FIXED_GAIN = 0.015F;
|
||||
constexpr float SCALE_ROOM = 0.28F;
|
||||
constexpr float OFFSET_ROOM = 0.7F;
|
||||
constexpr float SCALE_DAMP = 0.4F;
|
||||
|
||||
// --- Decodificació a float -1..1 ---
|
||||
// Suporta U8/S16, mono/estèreo. Mono es duplica a L i R (la cadena
|
||||
// d'efectes treballa sempre amb dos canals per simplicitat).
|
||||
auto decodeToStereoFloat(const Ja::Sound& src, std::vector<float>& left, std::vector<float>& right) -> bool {
|
||||
const auto& spec = src.spec;
|
||||
const Uint8* buf = src.buffer.get();
|
||||
if (buf == nullptr || src.length == 0) { return false; }
|
||||
|
||||
int bytes_per_sample = 0;
|
||||
if (spec.format == SDL_AUDIO_S16) {
|
||||
bytes_per_sample = 2;
|
||||
} else if (spec.format == SDL_AUDIO_U8) {
|
||||
bytes_per_sample = 1;
|
||||
} else {
|
||||
std::cerr << "AudioEffects: format de so no suportat (només U8 o S16)\n";
|
||||
return false;
|
||||
}
|
||||
if (spec.channels < 1 || spec.channels > 2) {
|
||||
std::cerr << "AudioEffects: el so ha de ser mono o estèreo\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t TOTAL_FRAMES = src.length / static_cast<std::size_t>(bytes_per_sample * spec.channels);
|
||||
left.resize(TOTAL_FRAMES);
|
||||
right.resize(TOTAL_FRAMES);
|
||||
|
||||
for (std::size_t i = 0; i < TOTAL_FRAMES; ++i) {
|
||||
float sample_l = 0.0F;
|
||||
float sample_r = 0.0F;
|
||||
if (spec.format == SDL_AUDIO_S16) {
|
||||
const auto* p = reinterpret_cast<const std::int16_t*>(buf + (i * spec.channels * 2));
|
||||
sample_l = static_cast<float>(p[0]) / 32768.0F;
|
||||
sample_r = (spec.channels == 2) ? static_cast<float>(p[1]) / 32768.0F : sample_l;
|
||||
} else { // U8
|
||||
const Uint8* p = buf + (i * spec.channels);
|
||||
sample_l = (static_cast<float>(p[0]) - 128.0F) / 128.0F;
|
||||
sample_r = (spec.channels == 2) ? (static_cast<float>(p[1]) - 128.0F) / 128.0F : sample_l;
|
||||
}
|
||||
left[i] = sample_l;
|
||||
right[i] = sample_r;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Empaqueta dos canals float (-1..1) a S16 entrellaçat.
|
||||
void encodeStereoS16(const std::vector<float>& left, const std::vector<float>& right, std::vector<std::uint8_t>& out) {
|
||||
const std::size_t LEN = left.size();
|
||||
out.resize(LEN * 2 * sizeof(std::int16_t));
|
||||
auto* dst = reinterpret_cast<std::int16_t*>(out.data());
|
||||
for (std::size_t i = 0; i < LEN; ++i) {
|
||||
const float L = std::clamp(left[i], -1.0F, 1.0F);
|
||||
const float R = std::clamp(right[i], -1.0F, 1.0F);
|
||||
dst[(i * 2) + 0] = static_cast<std::int16_t>(std::lround(L * 32767.0F));
|
||||
dst[(i * 2) + 1] = static_cast<std::int16_t>(std::lround(R * 32767.0F));
|
||||
}
|
||||
}
|
||||
|
||||
// Reescala un delay de la taula de Freeverb per a la freqüència real.
|
||||
auto scaledDelay(int reference_delay, int rate) -> int {
|
||||
const long SCALED = std::lround(static_cast<double>(reference_delay) * static_cast<double>(rate) / static_cast<double>(COMB_REFERENCE_RATE));
|
||||
return std::max(1, static_cast<int>(SCALED));
|
||||
}
|
||||
|
||||
// --- Filtres bàsics ---
|
||||
struct Comb {
|
||||
std::vector<float> buf;
|
||||
std::size_t idx{0};
|
||||
float feedback{0.0F};
|
||||
float damp1{0.0F};
|
||||
float damp2{1.0F};
|
||||
float store{0.0F};
|
||||
|
||||
void init(int delay, float fb, float damping) {
|
||||
buf.assign(static_cast<std::size_t>(delay), 0.0F);
|
||||
idx = 0;
|
||||
feedback = fb;
|
||||
damp1 = damping;
|
||||
damp2 = 1.0F - damping;
|
||||
store = 0.0F;
|
||||
}
|
||||
auto tick(float in) -> float {
|
||||
const float OUT = buf[idx];
|
||||
store = (OUT * damp2) + (store * damp1);
|
||||
buf[idx] = in + (store * feedback);
|
||||
idx = (idx + 1) % buf.size();
|
||||
return OUT;
|
||||
}
|
||||
};
|
||||
|
||||
struct Allpass {
|
||||
std::vector<float> buf;
|
||||
std::size_t idx{0};
|
||||
|
||||
void init(int delay) {
|
||||
buf.assign(static_cast<std::size_t>(delay), 0.0F);
|
||||
idx = 0;
|
||||
}
|
||||
auto tick(float in) -> float {
|
||||
const float BUFOUT = buf[idx];
|
||||
const float OUT = -in + BUFOUT;
|
||||
buf[idx] = in + (BUFOUT * 0.5F);
|
||||
idx = (idx + 1) % buf.size();
|
||||
return OUT;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
auto applyEcho(const Ja::Sound& src, const Ja::EchoParams& params) -> std::optional<ProcessedSound> {
|
||||
std::vector<float> left;
|
||||
std::vector<float> right;
|
||||
if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; }
|
||||
|
||||
const int RATE = src.spec.freq;
|
||||
const int DELAY_SAMPLES = std::max(1, static_cast<int>(std::lround(params.delay_ms * 0.001F * static_cast<float>(RATE))));
|
||||
const auto TAIL_SAMPLES = static_cast<std::size_t>(std::lround(ECHO_TAIL_MS * 0.001F * static_cast<float>(RATE)));
|
||||
|
||||
const float FEEDBACK = std::clamp(params.feedback, 0.0F, 0.95F);
|
||||
const float WET = std::clamp(params.wet, 0.0F, 1.0F);
|
||||
const float DRY = 1.0F - WET;
|
||||
|
||||
const std::size_t INPUT_LEN = left.size();
|
||||
const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES;
|
||||
|
||||
std::vector<float> ring_l(static_cast<std::size_t>(DELAY_SAMPLES), 0.0F);
|
||||
std::vector<float> ring_r(static_cast<std::size_t>(DELAY_SAMPLES), 0.0F);
|
||||
std::size_t cursor = 0;
|
||||
|
||||
std::vector<float> out_l(TOTAL_LEN);
|
||||
std::vector<float> out_r(TOTAL_LEN);
|
||||
|
||||
for (std::size_t i = 0; i < TOTAL_LEN; ++i) {
|
||||
const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F;
|
||||
const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F;
|
||||
|
||||
const float DELAYED_L = ring_l[cursor];
|
||||
const float DELAYED_R = ring_r[cursor];
|
||||
|
||||
out_l[i] = (DRY * IN_L) + (WET * DELAYED_L);
|
||||
out_r[i] = (DRY * IN_R) + (WET * DELAYED_R);
|
||||
|
||||
ring_l[cursor] = IN_L + (DELAYED_L * FEEDBACK);
|
||||
ring_r[cursor] = IN_R + (DELAYED_R * FEEDBACK);
|
||||
cursor = (cursor + 1) % static_cast<std::size_t>(DELAY_SAMPLES);
|
||||
}
|
||||
|
||||
ProcessedSound result;
|
||||
result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE};
|
||||
encodeStereoS16(out_l, out_r, result.bytes);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto applyReverb(const Ja::Sound& src, const Ja::ReverbParams& params) -> std::optional<ProcessedSound> {
|
||||
std::vector<float> left;
|
||||
std::vector<float> right;
|
||||
if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; }
|
||||
|
||||
const int RATE = src.spec.freq;
|
||||
const auto TAIL_SAMPLES = static_cast<std::size_t>(std::lround(REVERB_TAIL_MS * 0.001F * static_cast<float>(RATE)));
|
||||
|
||||
const float ROOM_SIZE = std::clamp(params.room_size, 0.0F, 1.0F);
|
||||
const float DAMPING = std::clamp(params.damping, 0.0F, 1.0F);
|
||||
const float WET = std::clamp(params.wet, 0.0F, 1.0F);
|
||||
const float DRY = 1.0F - WET;
|
||||
|
||||
const float FEEDBACK = (ROOM_SIZE * SCALE_ROOM) + OFFSET_ROOM; // 0.7..0.98
|
||||
const float DAMP1 = DAMPING * SCALE_DAMP; // 0..0.4
|
||||
|
||||
// Inicialitza els 8 comb filters per cada canal i els 4 allpass.
|
||||
std::array<Comb, 8> comb_l;
|
||||
std::array<Comb, 8> comb_r;
|
||||
for (std::size_t i = 0; i < COMB_DELAYS_L.size(); ++i) {
|
||||
comb_l[i].init(scaledDelay(COMB_DELAYS_L[i], RATE), FEEDBACK, DAMP1);
|
||||
comb_r[i].init(scaledDelay(COMB_DELAYS_L[i] + STEREO_SPREAD, RATE), FEEDBACK, DAMP1);
|
||||
}
|
||||
std::array<Allpass, 4> allpass_l;
|
||||
std::array<Allpass, 4> allpass_r;
|
||||
for (std::size_t i = 0; i < ALLPASS_DELAYS_L.size(); ++i) {
|
||||
allpass_l[i].init(scaledDelay(ALLPASS_DELAYS_L[i], RATE));
|
||||
allpass_r[i].init(scaledDelay(ALLPASS_DELAYS_L[i] + STEREO_SPREAD, RATE));
|
||||
}
|
||||
|
||||
const std::size_t INPUT_LEN = left.size();
|
||||
const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES;
|
||||
std::vector<float> out_l(TOTAL_LEN);
|
||||
std::vector<float> out_r(TOTAL_LEN);
|
||||
|
||||
for (std::size_t i = 0; i < TOTAL_LEN; ++i) {
|
||||
const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F;
|
||||
const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F;
|
||||
const float MONO_INPUT = (IN_L + IN_R) * FIXED_GAIN;
|
||||
|
||||
// 8 comb filters en paral·lel, sumats.
|
||||
float wet_l = 0.0F;
|
||||
float wet_r = 0.0F;
|
||||
for (std::size_t k = 0; k < comb_l.size(); ++k) {
|
||||
wet_l += comb_l[k].tick(MONO_INPUT);
|
||||
wet_r += comb_r[k].tick(MONO_INPUT);
|
||||
}
|
||||
// 4 allpass en sèrie.
|
||||
for (std::size_t k = 0; k < allpass_l.size(); ++k) {
|
||||
wet_l = allpass_l[k].tick(wet_l);
|
||||
wet_r = allpass_r[k].tick(wet_r);
|
||||
}
|
||||
|
||||
out_l[i] = (DRY * IN_L) + (WET * wet_l);
|
||||
out_r[i] = (DRY * IN_R) + (WET * wet_r);
|
||||
}
|
||||
|
||||
ProcessedSound result;
|
||||
result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE};
|
||||
encodeStereoS16(out_l, out_r, result.bytes);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace AudioEffects
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
// Forward-declaració per no incloure jail_audio.hpp (cicle d'inclusió: aquest
|
||||
// header viu sota els params declarats a jail_audio.hpp, i alhora jail_audio
|
||||
// usa applyEcho/applyReverb).
|
||||
namespace Ja {
|
||||
struct Sound;
|
||||
struct EchoParams;
|
||||
struct ReverbParams;
|
||||
} // namespace Ja
|
||||
|
||||
// Processadors d'efectes per a sons puntuals. Reben un Ja::Sound (qualsevol
|
||||
// format suportat pel decodificador WAV: U8/S16, mono o estèreo) i tornen un
|
||||
// buffer PCM en S16 + el seu spec, llest per empenyer a un SDL_AudioStream.
|
||||
//
|
||||
// El buffer de sortida inclou la cua (decay) generada per l'efecte: per al
|
||||
// reverb, fins a 1500 ms; per a l'eco, fins a 800 ms. Aquests caps eviten
|
||||
// allargar indefinidament la reproducció quan els paràmetres reinjecten molt.
|
||||
//
|
||||
// Si el format del so d'origen no es pot processar, retornen std::nullopt
|
||||
// (el caller ha de fer fallback a reproducció seca).
|
||||
namespace AudioEffects {
|
||||
|
||||
struct ProcessedSound {
|
||||
std::vector<std::uint8_t> bytes; // PCM S16 entrellaçat (LRLRLR... si stereo)
|
||||
SDL_AudioSpec spec; // Format/canals/freqüència del buffer
|
||||
};
|
||||
|
||||
[[nodiscard]] auto applyEcho(const Ja::Sound& src, const Ja::EchoParams& params) -> std::optional<ProcessedSound>;
|
||||
[[nodiscard]] auto applyReverb(const Ja::Sound& src, const Ja::ReverbParams& params) -> std::optional<ProcessedSound>;
|
||||
|
||||
} // namespace AudioEffects
|
||||
@@ -4,6 +4,10 @@
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "core/audio/audio_effects.hpp"
|
||||
|
||||
// Només declaracions de stb_vorbis: STB_VORBIS_HEADER_ONLY omet el bloc
|
||||
// d'implementació. Les definicions les aporta source/external/stb_vorbis_impl.cpp
|
||||
@@ -397,13 +401,17 @@ namespace Ja {
|
||||
}
|
||||
|
||||
void Engine::stopChannel(const int channel) {
|
||||
forEachTargetChannel(channel, [](Channel& ch) {
|
||||
forEachTargetChannel(channel, [this](Channel& ch) {
|
||||
if (ch.state != ChannelState::FREE) {
|
||||
if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
|
||||
ch.stream = nullptr;
|
||||
ch.state = ChannelState::FREE;
|
||||
ch.pos = 0;
|
||||
ch.sound = nullptr;
|
||||
if (ch.has_effect) {
|
||||
ch.has_effect = false;
|
||||
if (effect_channels_active_ > 0) { --effect_channels_active_; }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -438,6 +446,55 @@ namespace Ja {
|
||||
}
|
||||
}
|
||||
|
||||
auto Engine::playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, const int group) -> int {
|
||||
// El cap de canals amb efecte es valida abans de reservar slot —
|
||||
// així evitem crear i destruir un stream només per descartar el play.
|
||||
if (effect_channels_active_ >= MAX_EFFECT_CHANNELS) { return -1; }
|
||||
|
||||
int channel = 0;
|
||||
while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { ++channel; }
|
||||
if (channel == MAX_SIMULTANEOUS_CHANNELS) { channel = 0; }
|
||||
|
||||
stopChannel(channel);
|
||||
|
||||
// El stream es crea contra l'spec del buffer processat (S16, ...)
|
||||
// perquè SDL faci el resampling cap a audio_spec_ del device.
|
||||
channels_[channel].stream = SDL_CreateAudioStream(&spec, &audio_spec_);
|
||||
if (channels_[channel].stream == nullptr) {
|
||||
std::fprintf(stderr, "Ja::Engine::playProcessedOnFreeChannel: Failed to create audio stream!\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
channels_[channel].sound = nullptr; // El buffer no és propietat de cap Ja::Sound.
|
||||
channels_[channel].times = 0;
|
||||
channels_[channel].pos = 0;
|
||||
const int CLAMPED_GROUP = (group >= 0 && group < MAX_GROUPS) ? group : 0;
|
||||
channels_[channel].group = CLAMPED_GROUP;
|
||||
channels_[channel].state = ChannelState::PLAYING;
|
||||
channels_[channel].has_effect = true;
|
||||
++effect_channels_active_;
|
||||
|
||||
SDL_PutAudioStreamData(channels_[channel].stream, bytes.data(), static_cast<int>(bytes.size()));
|
||||
SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[CLAMPED_GROUP]);
|
||||
SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream);
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
auto Engine::playSoundWithEcho(const Sound* sound, const EchoParams& params, const int group) -> int {
|
||||
if (sound == nullptr) { return -1; }
|
||||
auto processed = AudioEffects::applyEcho(*sound, params);
|
||||
if (!processed) { return -1; }
|
||||
return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
|
||||
}
|
||||
|
||||
auto Engine::playSoundWithReverb(const Sound* sound, const ReverbParams& params, const int group) -> int {
|
||||
if (sound == nullptr) { return -1; }
|
||||
auto processed = AudioEffects::applyReverb(*sound, params);
|
||||
if (!processed) { return -1; }
|
||||
return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
|
||||
}
|
||||
|
||||
// --- Factories i destructors (permanents) ---
|
||||
|
||||
auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
|
||||
|
||||
@@ -48,6 +48,26 @@ namespace Ja {
|
||||
// --- Constants ---
|
||||
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
|
||||
inline constexpr int MAX_GROUPS = 2;
|
||||
// Cap superior de canals que poden estar simultàniament reproduint un so
|
||||
// amb efecte (eco/reverb). Si està al límit, les noves crides amb efecte
|
||||
// cauen al camí sec — l'usuari sent el so igualment, sense la cua.
|
||||
inline constexpr int MAX_EFFECT_CHANNELS = 4;
|
||||
|
||||
// --- Paràmetres d'efectes ---
|
||||
// Els camps els fixa el caller (Audio) llegint sounds.yaml; el motor només
|
||||
// els passa a AudioEffects::applyEcho/applyReverb. Els defaults són
|
||||
// sensats però els presets els sobreescriuen.
|
||||
struct EchoParams {
|
||||
float delay_ms{220.0F}; // Temps fins al primer rebot.
|
||||
float feedback{0.45F}; // Reinjecció (0..0.95).
|
||||
float wet{0.35F}; // Mescla humida (0..1).
|
||||
};
|
||||
|
||||
struct ReverbParams {
|
||||
float room_size{0.7F}; // Mida percebuda (0..1).
|
||||
float damping{0.5F}; // Atenuació d'aguts per rebot (0..1).
|
||||
float wet{0.4F}; // Mescla humida (0..1).
|
||||
};
|
||||
|
||||
// Spec de fallback del dispositiu. S'aplica abans que l'Engine s'iniciï i
|
||||
// com a valor inicial de Sound/Music. L'spec real d'ús l'imposa el ctor
|
||||
@@ -72,6 +92,10 @@ namespace Ja {
|
||||
int times{0};
|
||||
int group{0};
|
||||
ChannelState state{ChannelState::FREE};
|
||||
// Marca si aquest canal va arrencar amb so processat per un efecte.
|
||||
// El motor compta canals actius amb efecte per fer complir
|
||||
// MAX_EFFECT_CHANNELS i alliberar el comptador en parar.
|
||||
bool has_effect{false};
|
||||
};
|
||||
|
||||
struct Music {
|
||||
@@ -147,6 +171,12 @@ namespace Ja {
|
||||
// --- So ---
|
||||
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
|
||||
auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
|
||||
// Reproducció amb so processat per un efecte. Retorna el canal
|
||||
// assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS).
|
||||
// El sound original només s'usa per consultar el spec/buffer; el
|
||||
// canal manipula el buffer ja processat (no reapunta a `sound`).
|
||||
auto playSoundWithEcho(const Sound* sound, const EchoParams& params, int group = 0) -> int;
|
||||
auto playSoundWithReverb(const Sound* sound, const ReverbParams& params, int group = 0) -> int;
|
||||
void pauseChannel(int channel);
|
||||
void resumeChannel(int channel);
|
||||
void stopChannel(int channel);
|
||||
@@ -161,6 +191,10 @@ namespace Ja {
|
||||
void updateIncomingFade();
|
||||
void updateCurrentMusic();
|
||||
void updateSoundChannels();
|
||||
// Empenta un buffer ja processat (S16) a un canal lliure i el deixa
|
||||
// sonar sense bucle. Camí comú dels dos overloads playSoundWith*.
|
||||
// Retorna el canal o -1 si no queden slots.
|
||||
auto playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, int group) -> int;
|
||||
|
||||
template <typename Fn>
|
||||
void forEachTargetChannel(int channel, Fn&& fn);
|
||||
@@ -174,6 +208,9 @@ namespace Ja {
|
||||
OutgoingMusic outgoing_music_;
|
||||
FadeState incoming_fade_;
|
||||
std::function<void()> on_music_ended_;
|
||||
// Comptador derivat de Channel::has_effect — evita haver-lo de
|
||||
// recalcular cada vegada que algú demana un play amb efecte.
|
||||
int effect_channels_active_{0};
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static amb sufix _
|
||||
static Engine* active_;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "core/audio/sound_effects_config.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Lector de camp amb fallback: deixa el destí intacte si la clau no
|
||||
// existeix (els defaults dels Ja::*Params s'inicialitzen al ctor del
|
||||
// struct, així que el comportament és "preset parcial = preset complet
|
||||
// amb defaults per als camps que falten").
|
||||
template <typename T>
|
||||
void readField(const fkyaml::node& node, const char* key, T& dst) {
|
||||
if (node.contains(key)) { dst = node[key].get_value<T>(); }
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto SoundEffectsConfig::get() -> SoundEffectsConfig& {
|
||||
static SoundEffectsConfig instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
void SoundEffectsConfig::load(const std::string& file_path) {
|
||||
auto bytes = Resource::Helper::loadFile(file_path);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "SoundEffectsConfig: no s'ha pogut obrir " << file_path << " — cap preset d'efecte disponible\n";
|
||||
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("echo") && yaml["echo"].is_mapping()) {
|
||||
for (auto it = yaml["echo"].begin(); it != yaml["echo"].end(); ++it) {
|
||||
const auto NAME = it.key().get_value<std::string>();
|
||||
const auto& node = it.value();
|
||||
Ja::EchoParams params{};
|
||||
readField(node, "delay_ms", params.delay_ms);
|
||||
readField(node, "feedback", params.feedback);
|
||||
readField(node, "wet", params.wet);
|
||||
echoes_[NAME] = params;
|
||||
}
|
||||
}
|
||||
|
||||
if (yaml.contains("reverb") && yaml["reverb"].is_mapping()) {
|
||||
for (auto it = yaml["reverb"].begin(); it != yaml["reverb"].end(); ++it) {
|
||||
const auto NAME = it.key().get_value<std::string>();
|
||||
const auto& node = it.value();
|
||||
Ja::ReverbParams params{};
|
||||
readField(node, "room_size", params.room_size);
|
||||
readField(node, "damping", params.damping);
|
||||
readField(node, "wet", params.wet);
|
||||
reverbs_[NAME] = params;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "SoundEffectsConfig: " << echoes_.size() << " preset(s) d'eco i " << reverbs_.size() << " de reverb des de " << file_path << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "SoundEffectsConfig: error parsejant " << file_path << ": " << e.what() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
auto SoundEffectsConfig::findEcho(const std::string& name) const -> const Ja::EchoParams* {
|
||||
const auto IT = echoes_.find(name);
|
||||
return (IT == echoes_.end()) ? nullptr : &IT->second;
|
||||
}
|
||||
|
||||
auto SoundEffectsConfig::findReverb(const std::string& name) const -> const Ja::ReverbParams* {
|
||||
const auto IT = reverbs_.find(name);
|
||||
return (IT == reverbs_.end()) ? nullptr : &IT->second;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "core/audio/jail_audio.hpp" // Per a Ja::EchoParams / Ja::ReverbParams
|
||||
|
||||
// Catàleg de presets d'efectes carregat des de data/sounds.yaml. La capa
|
||||
// Audio (playSoundWithEcho/playSoundWithReverb) hi accedeix per nom: si el
|
||||
// preset no existeix, el so es reprodueix sec amb un avís a stderr.
|
||||
//
|
||||
// Patró Meyers idèntic a UiConfig/Locale: un sol load() a l'arrencada, sense
|
||||
// hot-reload. Si el fitxer no existeix, el catàleg queda buit (cap preset
|
||||
// disponible) i tots els playSoundWith* es comporten com playSound dry.
|
||||
class SoundEffectsConfig {
|
||||
public:
|
||||
static auto get() -> SoundEffectsConfig&;
|
||||
|
||||
SoundEffectsConfig(const SoundEffectsConfig&) = delete;
|
||||
SoundEffectsConfig(SoundEffectsConfig&&) = delete;
|
||||
auto operator=(const SoundEffectsConfig&) -> SoundEffectsConfig& = delete;
|
||||
auto operator=(SoundEffectsConfig&&) -> SoundEffectsConfig& = delete;
|
||||
|
||||
void load(const std::string& file_path);
|
||||
|
||||
// Retorna nullptr si el preset no existeix.
|
||||
[[nodiscard]] auto findEcho(const std::string& name) const -> const Ja::EchoParams*;
|
||||
[[nodiscard]] auto findReverb(const std::string& name) const -> const Ja::ReverbParams*;
|
||||
|
||||
private:
|
||||
SoundEffectsConfig() = default;
|
||||
~SoundEffectsConfig() = default;
|
||||
|
||||
std::unordered_map<std::string, Ja::EchoParams> echoes_;
|
||||
std::unordered_map<std::string, Ja::ReverbParams> reverbs_;
|
||||
};
|
||||
@@ -38,7 +38,7 @@ class MovingSprite : public Sprite {
|
||||
void setAccelY(float value) { ay_ = value; }
|
||||
|
||||
void setFlip(SDL_FlipMode flip) { flip_ = flip; }
|
||||
auto getFlip() -> SDL_FlipMode { return flip_; }
|
||||
[[nodiscard]] auto getFlip() const -> SDL_FlipMode { return flip_; }
|
||||
void flip() { flip_ = (flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL; }
|
||||
|
||||
// Moviment eix-separat amb col·lisió: aplica `dx` (i si la nova
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/audio/sound_effects_config.hpp"
|
||||
#include "core/input/gamepad_manager.hpp"
|
||||
#include "core/input/key_config.hpp"
|
||||
#include "core/input/mouse.hpp"
|
||||
@@ -27,6 +28,7 @@
|
||||
#include "core/system/event_buffer.hpp"
|
||||
#include "core/ui/ui_config.hpp"
|
||||
#include "game/debug_config.hpp"
|
||||
#include "game/levels/level_manager.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "game/scenes/game_scene.hpp"
|
||||
#include "game/scenes/intro_scene.hpp"
|
||||
@@ -121,11 +123,24 @@ Director::Director() {
|
||||
const std::string PLACEHOLDER_FONT_GIF_KEY = UiConfig::get().placeholderFont().name + ".gif";
|
||||
Resource::Cache::get().loadSurface(PLACEHOLDER_FONT_GIF_KEY, UiConfig::get().placeholderFont().gif);
|
||||
Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
||||
// Font del rètol de transició de sala (la consumeix `GameScene`
|
||||
// tant per la línia "Piràmide N / Sala M" de l'INTRO com pel
|
||||
// "Fase completada" de l'OUTRO). Es preloda ací per consistència
|
||||
// amb la resta de fonts d'UI; sense això, la primera entrada a
|
||||
// GAME aniria a disc.
|
||||
const std::string ROOM_TEXT_FONT_GIF_KEY = UiConfig::get().roomTextFont().name + ".gif";
|
||||
Resource::Cache::get().loadSurface(ROOM_TEXT_FONT_GIF_KEY, UiConfig::get().roomTextFont().gif);
|
||||
Resource::Cache::get().getSurface(ROOM_TEXT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
||||
|
||||
// Música de la piràmide 1 (primer i únic track de moment). Carreguem ací
|
||||
// perquè forma part del preload d'assets, com la font i la paleta.
|
||||
Resource::Cache::get().loadMusic("pyramid_1.ogg", "data/music/pyramid_1.ogg");
|
||||
|
||||
// Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio
|
||||
// perquè els primers playSoundWith* (al començament del joc) ja
|
||||
// troben els presets disponibles.
|
||||
SoundEffectsConfig::get().load("data/sounds.yaml");
|
||||
|
||||
// Audio: singleton manual propietari d'un SDL audio device. Es destrueix
|
||||
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els
|
||||
// JA_Music_t que continga, així que no queden streams orfes.
|
||||
@@ -182,6 +197,13 @@ Director::Director() {
|
||||
DebugConfig::createDefaultIfMissing(DEBUG_YAML);
|
||||
const auto START = DebugConfig::resolveStartScene(DEBUG_YAML);
|
||||
const auto STARTING_PLAYERS = DebugConfig::resolveStartingPlayers(DEBUG_YAML);
|
||||
// `starting_level` (opcional) sobreescriu l'índex inicial del
|
||||
// LevelManager abans de crear cap GameScene. Si la clau no està o el
|
||||
// (pyramid, room) no apareix al levels.yaml, el LevelManager
|
||||
// continua a la primera entrada (P1-R1).
|
||||
if (const auto SL = DebugConfig::resolveStartingLevel(DEBUG_YAML); SL.has_value()) {
|
||||
Levels::LevelManager::get().setStartingPoint(SL->pyramid, SL->room);
|
||||
}
|
||||
#else
|
||||
const auto START = DebugConfig::StartScene::LOGO;
|
||||
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
|
||||
@@ -189,6 +211,12 @@ Director::Director() {
|
||||
std::cout << "Director: start_scene=" << DebugConfig::toString(START)
|
||||
<< " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n';
|
||||
|
||||
// El branch Release inicialitza STARTING_PLAYERS amb una constant
|
||||
// literal i clang considera la captura redundant; en Debug el valor és
|
||||
// runtime i la captura sí és imprescindible. La lambda viu dins de
|
||||
// IntroScene, més enllà del scope d'aquest ctor — capturar per
|
||||
// referència seria UB.
|
||||
// NOLINTNEXTLINE(clang-diagnostic-unused-lambda-capture)
|
||||
auto make_game = [STARTING_PLAYERS]() -> std::unique_ptr<Scene> {
|
||||
auto cfg = GameScene::defaultConfig();
|
||||
cfg.starting_players = STARTING_PLAYERS;
|
||||
|
||||
@@ -85,6 +85,8 @@ UiConfig::UiConfig()
|
||||
hud_label_(Defaults::UI::HUD_LABEL),
|
||||
hud_value_(Defaults::UI::HUD_VALUE),
|
||||
hud_flash_(Defaults::UI::HUD_FLASH),
|
||||
room_text_(Defaults::UI::ROOM_TEXT),
|
||||
room_text_outline_(Defaults::UI::ROOM_TEXT_OUTLINE),
|
||||
notification_ms_(Defaults::UI::NOTIFICATION_TIME_MS),
|
||||
exit_ms_(Defaults::UI::EXIT_TIME_MS),
|
||||
hud_entry_ms_(Defaults::UI::HUD_ENTRY_MS),
|
||||
@@ -96,6 +98,7 @@ UiConfig::UiConfig()
|
||||
pause_exit_curve_(curveByName(Defaults::UI::PAUSE_EXIT_CURVE)),
|
||||
hud_font_{.name = Defaults::UI::Font::HUD_NAME, .gif = Defaults::UI::Font::HUD_GIF, .fnt = Defaults::UI::Font::HUD_FNT, .kerning = Defaults::UI::Font::HUD_KERNING},
|
||||
placeholder_font_{.name = Defaults::UI::Font::PLACEHOLDER_NAME, .gif = Defaults::UI::Font::PLACEHOLDER_GIF, .fnt = Defaults::UI::Font::PLACEHOLDER_FNT, .kerning = Defaults::UI::Font::PLACEHOLDER_KERNING},
|
||||
room_text_font_{.name = Defaults::UI::Font::ROOM_TEXT_NAME, .gif = Defaults::UI::Font::ROOM_TEXT_GIF, .fnt = Defaults::UI::Font::ROOM_TEXT_FNT, .kerning = Defaults::UI::Font::ROOM_TEXT_KERNING},
|
||||
menu_title_font_{.name = Defaults::UI::Font::MENU_TITLE_NAME, .gif = Defaults::UI::Font::MENU_TITLE_GIF, .fnt = Defaults::UI::Font::MENU_TITLE_FNT, .kerning = Defaults::UI::Font::MENU_TITLE_KERNING},
|
||||
menu_items_font_{.name = Defaults::UI::Font::MENU_ITEMS_NAME, .gif = Defaults::UI::Font::MENU_ITEMS_GIF, .fnt = Defaults::UI::Font::MENU_ITEMS_FNT, .kerning = Defaults::UI::Font::MENU_ITEMS_KERNING} {
|
||||
}
|
||||
@@ -147,6 +150,8 @@ void UiConfig::load(const std::string& file_path) {
|
||||
readColorIndex(c, "hud_label", hud_label_);
|
||||
readColorIndex(c, "hud_value", hud_value_);
|
||||
readColorIndex(c, "hud_flash", hud_flash_);
|
||||
readColorIndex(c, "room_text", room_text_);
|
||||
readColorIndex(c, "room_text_outline", room_text_outline_);
|
||||
}
|
||||
|
||||
if (yaml.contains("timings")) {
|
||||
@@ -159,6 +164,7 @@ void UiConfig::load(const std::string& file_path) {
|
||||
const auto& f = yaml["fonts"];
|
||||
readFont(f, "hud", hud_font_);
|
||||
readFont(f, "placeholder", placeholder_font_);
|
||||
readFont(f, "room_text", room_text_font_);
|
||||
readFont(f, "menu_title", menu_title_font_);
|
||||
readFont(f, "menu_items", menu_items_font_);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ class UiConfig {
|
||||
[[nodiscard]] auto hudLabel() const -> Uint8 { return hud_label_; }
|
||||
[[nodiscard]] auto hudValue() const -> Uint8 { return hud_value_; }
|
||||
[[nodiscard]] auto hudFlash() const -> Uint8 { return hud_flash_; }
|
||||
[[nodiscard]] auto roomText() const -> Uint8 { return room_text_; }
|
||||
[[nodiscard]] auto roomTextOutline() const -> Uint8 { return room_text_outline_; }
|
||||
|
||||
// Signatura d'una funció d'easing: t ∈ [0, 1] → valor ∈ [0, 1].
|
||||
// Permet triar corbes des de data/ui.yaml per nom (cadena) i guardar
|
||||
@@ -77,6 +79,7 @@ class UiConfig {
|
||||
// Fonts de UI.
|
||||
[[nodiscard]] auto hudFont() const -> const Font& { return hud_font_; }
|
||||
[[nodiscard]] auto placeholderFont() const -> const Font& { return placeholder_font_; }
|
||||
[[nodiscard]] auto roomTextFont() const -> const Font& { return room_text_font_; }
|
||||
[[nodiscard]] auto menuTitleFont() const -> const Font& { return menu_title_font_; }
|
||||
[[nodiscard]] auto menuItemsFont() const -> const Font& { return menu_items_font_; }
|
||||
|
||||
@@ -105,6 +108,8 @@ class UiConfig {
|
||||
Uint8 hud_label_;
|
||||
Uint8 hud_value_;
|
||||
Uint8 hud_flash_;
|
||||
Uint8 room_text_;
|
||||
Uint8 room_text_outline_;
|
||||
|
||||
int notification_ms_;
|
||||
int exit_ms_;
|
||||
@@ -118,6 +123,7 @@ class UiConfig {
|
||||
|
||||
Font hud_font_;
|
||||
Font placeholder_font_;
|
||||
Font room_text_font_;
|
||||
Font menu_title_font_;
|
||||
Font menu_items_font_;
|
||||
|
||||
|
||||
@@ -103,6 +103,34 @@ namespace DebugConfig {
|
||||
return parseStartingPlayers(CONTENT);
|
||||
}
|
||||
|
||||
auto parseStartingLevel(const std::string& yaml_content) -> std::optional<StartingLevel> {
|
||||
if (yaml_content.empty()) { return std::nullopt; }
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||
if (!yaml.contains("starting_level")) { return std::nullopt; }
|
||||
const auto& sl = yaml["starting_level"];
|
||||
if (!sl.contains("pyramid") || !sl.contains("room")) {
|
||||
std::cerr << "debug.yaml: starting_level necessita els camps `pyramid` i `room` — ignorat\n";
|
||||
return std::nullopt;
|
||||
}
|
||||
return StartingLevel{
|
||||
.pyramid = sl["pyramid"].get_value<int>(),
|
||||
.room = sl["room"].get_value<int>(),
|
||||
};
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "debug.yaml: error parsejant starting_level: " << e.what() << " — ignorat\n";
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
auto resolveStartingLevel(const std::string& file_path) -> std::optional<StartingLevel> {
|
||||
std::error_code error;
|
||||
if (!std::filesystem::exists(file_path, error)) { return std::nullopt; }
|
||||
const auto CONTENT = readFile(file_path);
|
||||
if (CONTENT.empty()) { return std::nullopt; }
|
||||
return parseStartingLevel(CONTENT);
|
||||
}
|
||||
|
||||
auto toString(StartScene scene) -> const char* {
|
||||
switch (scene) {
|
||||
case StartScene::LOGO:
|
||||
@@ -140,6 +168,9 @@ namespace DebugConfig {
|
||||
file << "# debug.yaml — ajust personal de dev (no versionat).\n";
|
||||
file << "# start_scene: LOGO | TITLE | INTRO | GAME\n";
|
||||
file << "# starting_players: P1 | P2 | BOTH\n";
|
||||
file << "# starting_level (opcional): { pyramid: N, room: M } — salta a la sala\n";
|
||||
file << "# corresponent del levels.yaml. Sense la\n";
|
||||
file << "# clau, comença a la primera (P1-R1).\n";
|
||||
file << "start_scene: GAME\n";
|
||||
file << "starting_players: BOTH\n";
|
||||
std::cout << "debug.yaml: creat a " << file_path << " amb start_scene=GAME starting_players=BOTH\n";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
// Configuració personal de desenvolupament, carregada de `debug.yaml` dins la
|
||||
@@ -37,6 +38,18 @@ namespace DebugConfig {
|
||||
[[nodiscard]] auto parseStartingPlayers(const std::string& yaml_content) -> StartingPlayers;
|
||||
[[nodiscard]] auto resolveStartingPlayers(const std::string& file_path) -> StartingPlayers;
|
||||
|
||||
// Punt d'arrencada dins de la progressió de sales (consumit pel
|
||||
// `Levels::LevelManager`). YAML:
|
||||
// starting_level: { pyramid: 2, room: 3 }
|
||||
// L'absència de la clau és el cas habitual i deixa la progressió a
|
||||
// la primera entrada del `levels.yaml`.
|
||||
struct StartingLevel {
|
||||
int pyramid{0};
|
||||
int room{0};
|
||||
};
|
||||
[[nodiscard]] auto parseStartingLevel(const std::string& yaml_content) -> std::optional<StartingLevel>;
|
||||
[[nodiscard]] auto resolveStartingLevel(const std::string& file_path) -> std::optional<StartingLevel>;
|
||||
|
||||
// Nom canònic de l'enum (útil per a logs): "LOGO" / "TITLE" / "INTRO" / "GAME".
|
||||
[[nodiscard]] auto toString(StartScene scene) -> const char*;
|
||||
[[nodiscard]] auto toString(StartingPlayers players) -> const char*;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "core/system/event_buffer.hpp"
|
||||
#include "game/levels/level_manager.hpp"
|
||||
#include "game/map/tilemap.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
#include "utils/defines.hpp"
|
||||
@@ -76,11 +77,13 @@ void Editor::exit(Tilemap& tilemap) {
|
||||
if (!active_) { return; }
|
||||
active_ = false;
|
||||
picker_open_ = false;
|
||||
// Resolem la ruta via Resource::Helper perquè en Debug el root de
|
||||
// dades és AEE_SOURCE_DIR (ruta absoluta al repo), no el CWD. Sense
|
||||
// això, el writer obriria `data/maps/pyramid.yaml` relatiu al CWD
|
||||
// i fallaria sempre excepte si el binari es llança des del repo root.
|
||||
const auto ABS_PATH = Resource::Helper::resolve(Defaults::Editor::SAVE_PATH);
|
||||
// El writer salva sempre sobre la sala actualment carregada (segons
|
||||
// el LevelManager): així el "9" de l'editor desa el que tens davant,
|
||||
// no una constant fixa. Resolem la ruta via Resource::Helper perquè
|
||||
// en Debug el root de dades és AEE_SOURCE_DIR (ruta absoluta al repo),
|
||||
// no el CWD; sense això, el writer obriria el path relatiu al CWD i
|
||||
// fallaria si el binari no es llança des del repo root.
|
||||
const auto ABS_PATH = Resource::Helper::resolve(Levels::LevelManager::get().currentPath());
|
||||
const bool SAVED = tilemap.save(ABS_PATH);
|
||||
Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error"));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "game/entities/entity.hpp"
|
||||
|
||||
class Player;
|
||||
|
||||
// Base d'entitats d'atac: spawnejades pels jugadors, viuen poc temps i
|
||||
// poden danyar `Hazard`s i altres `Player`s (mai el seu propi `owner_`).
|
||||
//
|
||||
// Branca paral·lela a `Hazard` (no hereta d'ella) perquè la semàntica és
|
||||
// inversa: un `Hazard` és un perill estàtic/dinàmic que toca el jugador,
|
||||
// mentre que un `Attack` és el jugador qui colpeja la resta. Mantenir-les
|
||||
// separades permet, p. ex., que un atac no aparega al test de "què colpeja
|
||||
// al Player en el seu rect" (`EntityManager::checkPlayerCollisions`) perquè
|
||||
// el sentit del filtre és un altre.
|
||||
//
|
||||
// `Attack` no col·lisiona al test de moviment (els atacs no bloquegen
|
||||
// passos). El càlcul de quins objectius reben dany el fa cada subclasse
|
||||
// segons la seua geometria (rectangle direccional per al látigo, cercle
|
||||
// per a una explosió de bomba en el futur, etc.).
|
||||
class Attack : public Entity {
|
||||
public:
|
||||
Attack() {
|
||||
collides_ = false;
|
||||
interactable_ = false;
|
||||
}
|
||||
|
||||
void setOwner(Player* owner) { owner_ = owner; }
|
||||
[[nodiscard]] auto getOwner() const -> Player* { return owner_; }
|
||||
|
||||
protected:
|
||||
// Jugador que ha llançat l'atac. No rep dany del seu propi atac
|
||||
// (anti-self-friendly-fire). `nullptr` només a tests sintètics.
|
||||
Player* owner_{nullptr};
|
||||
};
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
#include "game/entities/player.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -65,21 +66,34 @@ void Door::update(float delta_time) {
|
||||
}
|
||||
|
||||
void Door::onPlayerContact(Player* player) {
|
||||
if (state_ != State::OPEN || level_complete_emitted_) { return; }
|
||||
// Només la porta OPEN despawna jugadors. CLOSED/OPENING ignoren el
|
||||
// contacte (i a més, en CLOSED, el `Map::collides` ja bloqueja el
|
||||
// moviment, així que el contacte no es dóna en condicions normals).
|
||||
if (state_ != State::OPEN || player == nullptr || player->isExited()) { return; }
|
||||
// Marquem el jugador com a EXITED i publiquem l'event d'agregació.
|
||||
// El check `isExited` de la guarda anterior garanteix que només
|
||||
// emetem l'event en la primera transició — `EntityManager` compta
|
||||
// emissions per decidir quan publicar `level_complete`.
|
||||
player->exit();
|
||||
if (bus_ != nullptr) {
|
||||
bus_->publish(Event{
|
||||
.name = "level_complete",
|
||||
.name = "player_exited",
|
||||
.payload = std::monostate{},
|
||||
.player_id = (player != nullptr) ? player->getId() : -1,
|
||||
.player_id = player->getId(),
|
||||
});
|
||||
}
|
||||
level_complete_emitted_ = true;
|
||||
}
|
||||
|
||||
void Door::open() {
|
||||
if (state_ != State::CLOSED) { return; }
|
||||
state_ = State::OPENING;
|
||||
setAnimation(anim_opening_);
|
||||
// Marquem el passadís com a transitable: a partir d'aquest tic
|
||||
// `Map::collides` deixa de bloquejar el rect del passadís. La
|
||||
// transició d'animació OPENING → OPEN encara dura el seu temps,
|
||||
// però la geometria ja és permeable — coherent amb el feel de
|
||||
// "la porta s'obri i el jugador pot avançar al moment".
|
||||
Map::door_open = true;
|
||||
// Edge case: si `opening` té ≤1 frame i és loop=true, mai es
|
||||
// completarà. Per força, forcem loop=false del frame únic aquí. Els
|
||||
// placeholders defineixen `loop: false` explícitament al YAML, així
|
||||
|
||||
@@ -7,11 +7,14 @@
|
||||
#include "game/entities/interactable.hpp"
|
||||
|
||||
// Porta de sortida del nivell. Tres estats:
|
||||
// CLOSED → inicial; bloqueja el pas.
|
||||
// CLOSED → inicial; el passadís de Map bloqueja el pas via `Map::collides`.
|
||||
// OPENING → animació d'obertura en curs. Quan el component acaba →
|
||||
// transita a OPEN.
|
||||
// OPEN → accessible. Al contacte amb el jugador publica
|
||||
// `level_complete` al bus (si n'hi ha).
|
||||
// transita a OPEN. El passadís ja és transitable (s'allibera
|
||||
// a `open()` amb `Map::door_open = true`).
|
||||
// OPEN → accessible. Al contacte amb un jugador, crida `Player::exit()`
|
||||
// — el jugador queda despawnat i emet `player_exited` al bus.
|
||||
// L'agregació "tots els jugadors han eixit → `level_complete`"
|
||||
// viu a EntityManager.
|
||||
//
|
||||
// El que decideix quan s'obri és extern (`EntityManager` comptant sarcs
|
||||
// oberts) via `open()`. L'animació `opening` es fa servir com a
|
||||
@@ -38,7 +41,6 @@ class Door : public Interactable {
|
||||
void setAnimation(const AnimationComponent& anim);
|
||||
|
||||
State state_{State::CLOSED};
|
||||
bool level_complete_emitted_{false};
|
||||
|
||||
// Les 3 animacions precalculades al ctor des del YAML. L'activa
|
||||
// s'assigna a `anim_` (heretat d'Entity) segons l'estat.
|
||||
|
||||
@@ -104,6 +104,22 @@ Mummy::Mummy(const fkyaml::node& yaml)
|
||||
} else if (initial == "down") {
|
||||
changeDirection(0.0F, 1.0F);
|
||||
}
|
||||
|
||||
// Configuració de l'estat HURT: opcional. Si el bloc no apareix, la
|
||||
// mòmia no entrarà mai a HURT efectiu (defensiu); el WhipAttack la
|
||||
// pot ignorar amb `isWhippable() == false` si volem que no reacció.
|
||||
if (yaml.contains("hurt")) {
|
||||
const auto& h = yaml["hurt"];
|
||||
if (h.contains("duration_ms")) {
|
||||
hurt_config_.duration_s = static_cast<float>(h["duration_ms"].get_value<int>()) / 1000.0F;
|
||||
}
|
||||
if (h.contains("speed")) {
|
||||
hurt_config_.speed_px_per_s = h["speed"].get_value<float>();
|
||||
}
|
||||
if (h.contains("whippable")) {
|
||||
whippable_ = h["whippable"].get_value<bool>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Mummy::changeDirection(float dx, float dy) {
|
||||
@@ -305,4 +321,118 @@ void Mummy::onPlayerContact(Player* player) {
|
||||
}
|
||||
}
|
||||
|
||||
void Mummy::enterHurt(Direction flee_dir) {
|
||||
state_ = State::HURT;
|
||||
hurt_remaining_s_ = hurt_config_.duration_s;
|
||||
hurt_speed_ = (hurt_config_.speed_px_per_s > 0.0F) ? hurt_config_.speed_px_per_s : speed_;
|
||||
|
||||
const Direction CHOSEN = chooseHurtDirection(flee_dir);
|
||||
if (CHOSEN == Direction::NONE) {
|
||||
// Cap direcció lliure: la mòmia queda quieta però en HURT durant
|
||||
// tot el timer. Posem dir a 0 perquè `hurtStep` no es mou.
|
||||
setDirection(0.0F, 0.0F);
|
||||
} else {
|
||||
applyChoice(CHOSEN);
|
||||
}
|
||||
}
|
||||
|
||||
auto Mummy::chooseHurtDirection(Direction preferred) const -> Direction {
|
||||
// Avenç ~1 frame a 60 FPS amb la velocitat de HURT. Tan curt com
|
||||
// perquè un canvi sub-tile no decideixe; suficient perquè un mur
|
||||
// adjacent (o sarc tocant la caja) es detecte.
|
||||
const float TEST_SPEED = (hurt_config_.speed_px_per_s > 0.0F) ? hurt_config_.speed_px_per_s : speed_;
|
||||
const float STEP = TEST_SPEED * (1.0F / 60.0F);
|
||||
|
||||
auto is_open = [this, STEP](Direction d) -> bool {
|
||||
float dx = 0.0F;
|
||||
float dy = 0.0F;
|
||||
switch (d) {
|
||||
case Direction::UP:
|
||||
dy = -STEP;
|
||||
break;
|
||||
case Direction::DOWN:
|
||||
dy = STEP;
|
||||
break;
|
||||
case Direction::LEFT:
|
||||
dx = -STEP;
|
||||
break;
|
||||
case Direction::RIGHT:
|
||||
dx = STEP;
|
||||
break;
|
||||
case Direction::NONE:
|
||||
return false;
|
||||
}
|
||||
const auto BOX = candidateBox(dx, dy);
|
||||
return Map::insidePlayfield(BOX) && !Map::collides(BOX);
|
||||
};
|
||||
|
||||
if (preferred != Direction::NONE && is_open(preferred)) { return preferred; }
|
||||
|
||||
// Perpendiculars segons la direcció preferida (ordre arbitrari però
|
||||
// consistent: "esquerra del whip" primer).
|
||||
Direction left = Direction::NONE;
|
||||
Direction right = Direction::NONE;
|
||||
switch (preferred) {
|
||||
case Direction::UP:
|
||||
left = Direction::LEFT;
|
||||
right = Direction::RIGHT;
|
||||
break;
|
||||
case Direction::DOWN:
|
||||
left = Direction::RIGHT;
|
||||
right = Direction::LEFT;
|
||||
break;
|
||||
case Direction::LEFT:
|
||||
left = Direction::DOWN;
|
||||
right = Direction::UP;
|
||||
break;
|
||||
case Direction::RIGHT:
|
||||
left = Direction::UP;
|
||||
right = Direction::DOWN;
|
||||
break;
|
||||
case Direction::NONE:
|
||||
return Direction::NONE;
|
||||
}
|
||||
if (is_open(left)) { return left; }
|
||||
if (is_open(right)) { return right; }
|
||||
|
||||
return Direction::NONE;
|
||||
}
|
||||
|
||||
void Mummy::hurtStep(float delta_time) {
|
||||
// Si està quieta (totes les direccions bloquejades a `enterHurt`), no
|
||||
// avança ni surt: l'únic camí de sortida és el timer.
|
||||
if (dir_x_ == 0.0F && dir_y_ == 0.0F) { return; }
|
||||
|
||||
const float DX = dir_x_ * hurt_speed_ * delta_time;
|
||||
const float DY = dir_y_ * hurt_speed_ * delta_time;
|
||||
const auto CANDIDATE = candidateBox(DX, DY);
|
||||
if (Map::insidePlayfield(CANDIDATE) && !Map::collides(CANDIDATE)) {
|
||||
pixel_rect_.x += DX;
|
||||
pixel_rect_.y += DY;
|
||||
return;
|
||||
}
|
||||
|
||||
// Topa amb mur o sarc: surt del HURT immediatament. La IA normal
|
||||
// re-decideix al pròxim step (no fem U-turn ni re-orientació aquí).
|
||||
state_ = State::ROAMING;
|
||||
hurt_remaining_s_ = 0.0F;
|
||||
}
|
||||
|
||||
void Mummy::update(float delta_time) {
|
||||
if (state_ == State::HURT) {
|
||||
// Avancem l'animació inline (no podem cridar `Entity::update` ni
|
||||
// `DynamicHazard::update`: el primer salta una capa virtual i el
|
||||
// segon dispararia `step()` amb la lògica d'IA). Volem només el
|
||||
// tick d'animació + el moviment recta del HURT.
|
||||
if (anim_.has_value()) { anim_->tick(delta_time); }
|
||||
hurtStep(delta_time);
|
||||
hurt_remaining_s_ -= delta_time;
|
||||
if (hurt_remaining_s_ <= 0.0F && state_ == State::HURT) {
|
||||
state_ = State::ROAMING;
|
||||
}
|
||||
return;
|
||||
}
|
||||
DynamicHazard::update(delta_time);
|
||||
}
|
||||
|
||||
REGISTER_ENTITY("mummy", Mummy)
|
||||
@@ -21,6 +21,21 @@ class Mummy : public DynamicHazard {
|
||||
LEFT,
|
||||
RIGHT };
|
||||
|
||||
// Estat d'alt nivell. ROAMING = IA normal (corridor_wanderer); HURT =
|
||||
// post-fuetada, fugint en línia recta durant `hurt_config_.duration_s`.
|
||||
// Mentre dura el HURT no decideix a creuaments ni canvia de direcció;
|
||||
// si topa amb mur o sarc, surt del HURT abans de l'expiració del timer.
|
||||
enum class State : std::uint8_t { ROAMING,
|
||||
HURT };
|
||||
|
||||
// Configuració de l'estat HURT, llegida del bloc `hurt:` del YAML.
|
||||
// Si `duration_s == 0` la mòmia mai entra a HURT efectiu (defensiu).
|
||||
// `speed_px_per_s == 0` ⇒ s'usa la velocitat normal de la mòmia.
|
||||
struct HurtConfig {
|
||||
float duration_s{0.0F};
|
||||
float speed_px_per_s{0.0F};
|
||||
};
|
||||
|
||||
explicit Mummy(const fkyaml::node& yaml);
|
||||
|
||||
// Hook d'IA. Donat un conjunt de direccions admissibles, decideix la
|
||||
@@ -39,9 +54,22 @@ class Mummy : public DynamicHazard {
|
||||
// En ambdós casos el frame de gir no consumeix avenç extra.
|
||||
void step(float delta_time) override;
|
||||
|
||||
// Update sencer: afegim la branca HURT (avanç del timer + step recta).
|
||||
// En estat ROAMING delega a `step()` com sempre via la base.
|
||||
void update(float delta_time) override;
|
||||
|
||||
// Contacte amb el jugador: publica `player_hit` amb el dany.
|
||||
void onPlayerContact(Player* player) override;
|
||||
|
||||
// Activa l'estat HURT amb la direcció de fuga preferida `flee_dir`.
|
||||
// Si està bloquejada, prova perpendiculars; si totes estan bloquejades,
|
||||
// la mòmia queda quieta però en HURT durant tot el timer.
|
||||
void enterHurt(Direction flee_dir);
|
||||
|
||||
[[nodiscard]] auto getState() const -> State { return state_; }
|
||||
[[nodiscard]] auto isWhippable() const -> bool { return whippable_; }
|
||||
[[nodiscard]] auto getHurtRemainingS() const -> float { return hurt_remaining_s_; }
|
||||
|
||||
private:
|
||||
// Caixa de col·lisió candidata si l'entitat es desplaçara (dx, dy).
|
||||
[[nodiscard]] auto candidateBox(float dx, float dy) const -> SDL_FRect;
|
||||
@@ -90,9 +118,28 @@ class Mummy : public DynamicHazard {
|
||||
// S'usa al pool de creuament per afegir la recta com a candidata.
|
||||
[[nodiscard]] auto currentDirection() const -> Direction;
|
||||
|
||||
// Avança recta a `hurt_speed_ * delta_time`. Si la caixa candidata
|
||||
// sortiria del playfield o trepitjaria un sarc, marca la sortida del
|
||||
// HURT abans de l'expiració del timer (la mòmia no rebota).
|
||||
void hurtStep(float delta_time);
|
||||
|
||||
// Tria la direcció definitiva del HURT donada la preferida (la del
|
||||
// whip). Si la preferida està bloquejada, prova perpendiculars; si
|
||||
// totes ho estan, retorna `NONE` (la mòmia queda quieta en HURT).
|
||||
[[nodiscard]] auto chooseHurtDirection(Direction preferred) const -> Direction;
|
||||
|
||||
// Animacions carregades del YAML, una per cada facing. Per a horitzontal
|
||||
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta.
|
||||
AnimationComponent anim_walk_down_;
|
||||
AnimationComponent anim_walk_up_;
|
||||
AnimationComponent anim_walk_lateral_;
|
||||
|
||||
// Estat HURT i la seua configuració. `hurt_speed_` és l'efectiva
|
||||
// (resultat d'aplicar el `speed` del bloc `hurt:` o, si val 0, la
|
||||
// velocitat normal de la mòmia).
|
||||
State state_{State::ROAMING};
|
||||
HurtConfig hurt_config_;
|
||||
bool whippable_{true};
|
||||
float hurt_remaining_s_{0.0F};
|
||||
float hurt_speed_{0.0F};
|
||||
};
|
||||
@@ -0,0 +1,342 @@
|
||||
#include "game/entities/concrete/whip_attack.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "game/entities/concrete/mummy.hpp"
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/entity_manager.hpp"
|
||||
#include "game/entities/player.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Llig una entrada `sprites.<dir>: { path, size: [w, h] }` del YAML.
|
||||
// Si manquen camps, retorna un DirSprite buit (queda invalidat al
|
||||
// configure() per `surface_key.empty()`).
|
||||
auto readDirSprite(const fkyaml::node& s) -> WhipAttack::DirSprite {
|
||||
WhipAttack::DirSprite sprite;
|
||||
if (s.contains("path")) {
|
||||
const auto REL = s["path"].get_value<std::string>();
|
||||
sprite.surface_key = std::filesystem::path(REL).filename().string();
|
||||
}
|
||||
if (s.contains("size")) {
|
||||
const auto& sz = s["size"];
|
||||
if (sz.is_sequence() && sz.size() == 2) {
|
||||
sprite.frame_w = static_cast<float>(sz[0].get_value<int>());
|
||||
sprite.frame_h = static_cast<float>(sz[1].get_value<int>());
|
||||
}
|
||||
}
|
||||
return sprite;
|
||||
}
|
||||
|
||||
// Construeix l'`AnimationComponent` del swing a partir del bloc
|
||||
// `animations.swing`. Convenció igual que la resta d'entitats:
|
||||
// - `frames` : llista d'índexs dins l'spritesheet (obligatori).
|
||||
// - `frame_ms` : durada uniforme per frame (ms). Alternativament `fps`.
|
||||
// - `loop` : per defecte true; al látigo el volem false (mor en
|
||||
// completar).
|
||||
auto buildSwingAnim(const fkyaml::node& yaml, std::vector<int>& active_out)
|
||||
-> AnimationComponent {
|
||||
std::vector<int> frames = {0};
|
||||
float fps = 8.0F;
|
||||
bool loop = false;
|
||||
if (yaml.contains("animations") && yaml["animations"].contains("swing")) {
|
||||
const auto& sw = yaml["animations"]["swing"];
|
||||
if (sw.contains("frames")) {
|
||||
frames.clear();
|
||||
for (const auto& f : sw["frames"]) {
|
||||
// cppcheck-suppress useStlAlgorithm
|
||||
frames.push_back(f.get_value<int>());
|
||||
}
|
||||
if (frames.empty()) { frames.push_back(0); }
|
||||
}
|
||||
if (sw.contains("frame_ms")) {
|
||||
const auto MS = static_cast<float>(sw["frame_ms"].get_value<int>());
|
||||
if (MS > 0.0F) { fps = 1000.0F / MS; }
|
||||
} else if (sw.contains("fps")) {
|
||||
fps = sw["fps"].get_value<float>();
|
||||
}
|
||||
if (sw.contains("loop")) { loop = sw["loop"].get_value<bool>(); }
|
||||
if (sw.contains("active_frames")) {
|
||||
for (const auto& f : sw["active_frames"]) {
|
||||
// cppcheck-suppress useStlAlgorithm
|
||||
active_out.push_back(f.get_value<int>());
|
||||
}
|
||||
}
|
||||
}
|
||||
return {frames, fps, loop};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WhipAttack::WhipAttack(const fkyaml::node& yaml) {
|
||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("whip_attack");
|
||||
collides_ = false;
|
||||
interactable_ = false;
|
||||
|
||||
if (yaml.contains("sprites")) {
|
||||
const auto& s = yaml["sprites"];
|
||||
if (s.contains("north")) { sprite_north_ = readDirSprite(s["north"]); }
|
||||
if (s.contains("south")) { sprite_south_ = readDirSprite(s["south"]); }
|
||||
if (s.contains("east")) { sprite_east_ = readDirSprite(s["east"]); }
|
||||
if (s.contains("west")) { sprite_west_ = readDirSprite(s["west"]); }
|
||||
}
|
||||
|
||||
anim_swing_ = buildSwingAnim(yaml, active_frames_);
|
||||
anim_ = anim_swing_;
|
||||
}
|
||||
|
||||
void WhipAttack::configure(Direction dir, const SDL_FRect& owner_box, float range_px) {
|
||||
direction_ = dir;
|
||||
range_px_ = range_px;
|
||||
|
||||
// Draw rect (= pixel_rect_): cobreix tot el sprite del látigo segons
|
||||
// la taula d'offsets del disseny:
|
||||
// E: (player.x - 16, player.y ), 80×32
|
||||
// W: (player.x - 48, player.y ), 80×32
|
||||
// N: (player.x, player.y - 48 ), 16×96
|
||||
// S: (player.x, player.y - 16 ), 16×96
|
||||
const auto& chosen = spriteFor(dir);
|
||||
SDL_FRect draw{0.0F, 0.0F, chosen.frame_w, chosen.frame_h};
|
||||
switch (dir) {
|
||||
case Direction::EAST:
|
||||
draw.x = owner_box.x - 16.0F;
|
||||
draw.y = owner_box.y;
|
||||
break;
|
||||
case Direction::WEST:
|
||||
draw.x = owner_box.x - 48.0F;
|
||||
draw.y = owner_box.y;
|
||||
break;
|
||||
case Direction::NORTH:
|
||||
draw.x = owner_box.x;
|
||||
draw.y = owner_box.y - 48.0F;
|
||||
break;
|
||||
case Direction::SOUTH:
|
||||
draw.x = owner_box.x;
|
||||
draw.y = owner_box.y - 16.0F;
|
||||
break;
|
||||
}
|
||||
setPixelRect(draw);
|
||||
|
||||
// Damage rect: 48×16 (E/W) o 16×48 (N/S) al nivell dels peus del
|
||||
// jugador (els últims 16 px del seu pixel_rect — convenció estàndard
|
||||
// del player AEE), pegat al borde en la direcció d'atac.
|
||||
// computeDamageRect rep la cantonada del rect MÀXIM (sense retallar)
|
||||
// i recorre internament la línia per topar amb el primer bloquejador.
|
||||
const float COLLISION_TOP_Y = owner_box.y + owner_box.h - 16.0F;
|
||||
const float OWNER_RIGHT = owner_box.x + owner_box.w;
|
||||
const float OWNER_BOTTOM = owner_box.y + owner_box.h;
|
||||
float spawn_x = 0.0F;
|
||||
float spawn_y = 0.0F;
|
||||
switch (dir) {
|
||||
case Direction::EAST:
|
||||
spawn_x = OWNER_RIGHT;
|
||||
spawn_y = COLLISION_TOP_Y;
|
||||
break;
|
||||
case Direction::WEST:
|
||||
spawn_x = owner_box.x - range_px;
|
||||
spawn_y = COLLISION_TOP_Y;
|
||||
break;
|
||||
case Direction::NORTH:
|
||||
spawn_x = owner_box.x;
|
||||
spawn_y = COLLISION_TOP_Y - range_px;
|
||||
break;
|
||||
case Direction::SOUTH:
|
||||
spawn_x = owner_box.x;
|
||||
spawn_y = OWNER_BOTTOM;
|
||||
break;
|
||||
}
|
||||
computeDamageRect(dir, spawn_x, spawn_y, range_px);
|
||||
|
||||
// Snapshot del feet_y del jugador. La convenció estàndard del Player
|
||||
// AEE té el seu feet_y a la base inferior del pixel_rect (collision
|
||||
// 16×16 als últims 16 px del sprite 16×32), així que owner_box.y +
|
||||
// owner_box.h reprodueix `Player::getFeetY()` exactament.
|
||||
owner_feet_y_ = owner_box.y + owner_box.h;
|
||||
|
||||
if (anim_.has_value()) { anim_->reset(); }
|
||||
}
|
||||
|
||||
auto WhipAttack::getFeetY() const -> float {
|
||||
return owner_feet_y_;
|
||||
}
|
||||
|
||||
void WhipAttack::setWorldContext(EntityManager* manager, const std::vector<Player*>* players) {
|
||||
entity_manager_ = manager;
|
||||
players_ = players;
|
||||
}
|
||||
|
||||
void WhipAttack::computeDamageRect(Direction dir, float spawn_x, float spawn_y, float range_px) {
|
||||
// Caminem píxel a píxel des de la vora del jugador en la direcció
|
||||
// d'atac, fins a topar amb un mur o sarc, o esgotar `range_px`.
|
||||
// L'amplada perpendicular és `tile_size` (els passadissos del joc
|
||||
// tenen exactament aquesta mida i l'atac és lateral).
|
||||
const auto TILE = static_cast<float>(Map::tile_size);
|
||||
const int MAX_PX = static_cast<int>(std::max(0.0F, range_px));
|
||||
const auto MAX = static_cast<float>(MAX_PX);
|
||||
float length = 0.0F;
|
||||
|
||||
auto try_probe = [&](float l) -> bool {
|
||||
SDL_FRect probe{0.0F, 0.0F, 0.0F, 0.0F};
|
||||
switch (dir) {
|
||||
case Direction::EAST:
|
||||
probe = {.x = spawn_x, .y = spawn_y, .w = l, .h = TILE};
|
||||
break;
|
||||
case Direction::WEST:
|
||||
// Per a W, la cara "del jugador" és la dreta; com creix
|
||||
// `l`, la vora esquerra retrocedeix. spawn_x apunta al
|
||||
// límit esquerre amb l = MAX (cas no retallat).
|
||||
probe = {.x = spawn_x + (MAX - l), .y = spawn_y, .w = l, .h = TILE};
|
||||
break;
|
||||
case Direction::SOUTH:
|
||||
probe = {.x = spawn_x, .y = spawn_y, .w = TILE, .h = l};
|
||||
break;
|
||||
case Direction::NORTH:
|
||||
probe = {.x = spawn_x, .y = spawn_y + (MAX - l), .w = TILE, .h = l};
|
||||
break;
|
||||
}
|
||||
return Map::insidePlayfield(probe) && !Map::collides(probe);
|
||||
};
|
||||
|
||||
for (int i = 1; i <= MAX_PX; ++i) {
|
||||
const auto L = static_cast<float>(i);
|
||||
if (try_probe(L)) {
|
||||
length = L;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (dir) {
|
||||
case Direction::EAST:
|
||||
damage_rect_ = {.x = spawn_x, .y = spawn_y, .w = length, .h = TILE};
|
||||
return;
|
||||
case Direction::WEST:
|
||||
damage_rect_ = {.x = spawn_x + (MAX - length), .y = spawn_y, .w = length, .h = TILE};
|
||||
return;
|
||||
case Direction::SOUTH:
|
||||
damage_rect_ = {.x = spawn_x, .y = spawn_y, .w = TILE, .h = length};
|
||||
return;
|
||||
case Direction::NORTH:
|
||||
damage_rect_ = {.x = spawn_x, .y = spawn_y + (MAX - length), .w = TILE, .h = length};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto WhipAttack::getSurfaceKey() const -> const std::string& {
|
||||
return spriteFor(direction_).surface_key;
|
||||
}
|
||||
|
||||
auto WhipAttack::spriteFor(Direction dir) const -> const DirSprite& {
|
||||
switch (dir) {
|
||||
case Direction::NORTH:
|
||||
return sprite_north_;
|
||||
case Direction::SOUTH:
|
||||
return sprite_south_;
|
||||
case Direction::EAST:
|
||||
return sprite_east_;
|
||||
case Direction::WEST:
|
||||
return sprite_west_;
|
||||
}
|
||||
return sprite_east_;
|
||||
}
|
||||
|
||||
void WhipAttack::update(float delta_time) {
|
||||
Entity::update(delta_time);
|
||||
if (isFrameActive()) { applyDamage(); }
|
||||
if (anim_.has_value() && anim_->completed()) { markDead(); }
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
auto whipDirToMummyDir(WhipAttack::Direction d) -> Mummy::Direction {
|
||||
switch (d) {
|
||||
case WhipAttack::Direction::NORTH:
|
||||
return Mummy::Direction::UP;
|
||||
case WhipAttack::Direction::SOUTH:
|
||||
return Mummy::Direction::DOWN;
|
||||
case WhipAttack::Direction::EAST:
|
||||
return Mummy::Direction::RIGHT;
|
||||
case WhipAttack::Direction::WEST:
|
||||
return Mummy::Direction::LEFT;
|
||||
}
|
||||
return Mummy::Direction::NONE;
|
||||
}
|
||||
|
||||
auto whipDirToPlayerCardinal(WhipAttack::Direction d) -> Player::Cardinal {
|
||||
switch (d) {
|
||||
case WhipAttack::Direction::NORTH:
|
||||
return Player::Cardinal::NORTH;
|
||||
case WhipAttack::Direction::SOUTH:
|
||||
return Player::Cardinal::SOUTH;
|
||||
case WhipAttack::Direction::EAST:
|
||||
return Player::Cardinal::EAST;
|
||||
case WhipAttack::Direction::WEST:
|
||||
return Player::Cardinal::WEST;
|
||||
}
|
||||
return Player::Cardinal::NONE;
|
||||
}
|
||||
|
||||
auto playerIsTargetable(const Player* p) -> bool {
|
||||
if (p == nullptr) { return false; }
|
||||
const auto STATE = p->getState();
|
||||
return STATE != Player::State::HURT && STATE != Player::State::EXITED && STATE != Player::State::DISABLED;
|
||||
}
|
||||
|
||||
// Tracta una mòmia candidata: si és whippable i la seua caja solapa
|
||||
// amb `damage_rect`, la posa a HURT i la marca al set. Extret per
|
||||
// acotar la complexitat cognitiva d'`applyDamage` (clang-tidy llindar).
|
||||
void hitMummyIfApplicable(
|
||||
Entity* e,
|
||||
const SDL_FRect& damage_rect,
|
||||
WhipAttack::Direction dir,
|
||||
std::unordered_set<void*>& already_hit) {
|
||||
if (e == nullptr || e->isDead()) { return; }
|
||||
if (already_hit.contains(static_cast<void*>(e))) { return; }
|
||||
auto* mummy = dynamic_cast<Mummy*>(e);
|
||||
if (mummy == nullptr || !mummy->isWhippable()) { return; }
|
||||
const auto BOX = mummy->getCollisionRect();
|
||||
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
|
||||
mummy->enterHurt(whipDirToMummyDir(dir));
|
||||
already_hit.insert(static_cast<void*>(e));
|
||||
}
|
||||
|
||||
void hitPlayerIfApplicable(
|
||||
Player* p,
|
||||
const Player* owner,
|
||||
const SDL_FRect& damage_rect,
|
||||
WhipAttack::Direction dir,
|
||||
std::unordered_set<void*>& already_hit) {
|
||||
if (p == owner || !playerIsTargetable(p)) { return; }
|
||||
if (already_hit.contains(static_cast<void*>(p))) { return; }
|
||||
const auto BOX = p->getBaseBox();
|
||||
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
|
||||
p->enterHurt(whipDirToPlayerCardinal(dir));
|
||||
already_hit.insert(static_cast<void*>(p));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void WhipAttack::applyDamage() {
|
||||
if (entity_manager_ != nullptr) {
|
||||
for (auto* e : entity_manager_->getEntities()) {
|
||||
hitMummyIfApplicable(e, damage_rect_, direction_, already_hit_);
|
||||
}
|
||||
}
|
||||
if (players_ != nullptr) {
|
||||
for (auto* p : *players_) {
|
||||
hitPlayerIfApplicable(p, owner_, damage_rect_, direction_, already_hit_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto WhipAttack::isFrameActive() const -> bool {
|
||||
if (!anim_.has_value()) { return false; }
|
||||
const int FRAME = anim_->currentFrame();
|
||||
return std::ranges::any_of(active_frames_, [FRAME](int f) { return f == FRAME; });
|
||||
}
|
||||
|
||||
REGISTER_ENTITY("whip_attack", WhipAttack)
|
||||
@@ -0,0 +1,162 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/animation_component.hpp"
|
||||
#include "game/entities/attack.hpp"
|
||||
|
||||
class Entity;
|
||||
class EntityManager;
|
||||
class Player;
|
||||
|
||||
// Atac de látigo: spawnejat pel jugador en entrar a `ATTACKING`. Viu la
|
||||
// durada de la seua animació (`swing`) i s'autoelimina al final.
|
||||
//
|
||||
// Sprites: 4 fitxers GIF distints, un per cardinal (sense flips). La
|
||||
// mida del frame canvia entre direccions (E/W: 80×32, N/S: 16×96),
|
||||
// cosa que el sistema d'animació actual no permet expressar amb una
|
||||
// sola surface. Per això el YAML llista les 4 entrades sota `sprites:`
|
||||
// i `configure()` tria la que toca segons direcció.
|
||||
//
|
||||
// Dos rectangles distints, intencionadament diferenciats:
|
||||
// - `pixel_rect_` (heretat d'`Entity`) = `draw_rect`. Cobreix tot el
|
||||
// sprite del látigo: 16 px de wind-up darrere del jugador, 16 px
|
||||
// de solapament amb el jugador, i 48 px d'atac frontal (per E/W;
|
||||
// proporcions equivalents per N/S amb 32 px de solapament). NO es
|
||||
// retalla per bloquejadors — només render visual.
|
||||
// - `damage_rect_` = caixa de dany al nivell dels peus del jugador
|
||||
// (centrada sobre la base 16×16). 48×16 (E/W) o 16×48 (N/S),
|
||||
// pegada al borde del jugador en la direcció d'atac. SÍ es retalla
|
||||
// pel primer bloquejador (mur extern del playfield o sarcòfag).
|
||||
//
|
||||
// Z-order: `getFeetY()` delega al `owner_` per garantir que el látigo
|
||||
// quede al mateix bucket d'y-sort que el jugador que l'ha llançat. Si
|
||||
// no, l'altura física del sprite (esp. en N/S, que té 96 px) crearia
|
||||
// solapaments raros amb mòmies a la trajectòria.
|
||||
class WhipAttack : public Attack {
|
||||
public:
|
||||
// Direcció cardinal del swing. Coincideix valors amb
|
||||
// `Player::Cardinal` (1=N, 2=S, 3=E, 4=W) per facilitar la
|
||||
// conversió a la frontera de l'event `whip_spawn`.
|
||||
enum class Direction : std::uint8_t {
|
||||
NORTH = 1,
|
||||
SOUTH = 2,
|
||||
EAST = 3,
|
||||
WEST = 4,
|
||||
};
|
||||
|
||||
// Definició estàtica d'una direcció: clau al `Resource::Cache` i mida
|
||||
// del frame en píxels. Llegida del YAML una vegada al ctor; usada per
|
||||
// configure() per omplir surface_ i pixel_rect_. Pública perquè el
|
||||
// helper d'anàlisi del YAML viu en un namespace anònim al .cpp.
|
||||
struct DirSprite {
|
||||
std::string surface_key;
|
||||
float frame_w{0.0F};
|
||||
float frame_h{0.0F};
|
||||
};
|
||||
|
||||
explicit WhipAttack(const fkyaml::node& yaml);
|
||||
|
||||
// Configura l'estat runtime de la instància. Cal cridar-lo abans del
|
||||
// primer `update`. A partir del `owner_box` (pixel_rect del jugador,
|
||||
// típicament 16×32) i la direcció d'atac, deriva:
|
||||
// - `pixel_rect_` (= draw_rect): cantonada i mida del sprite del
|
||||
// látigo segons la taula d'offsets (E: -16, 0, 80×32; etc.).
|
||||
// - `damage_rect_`: caixa de dany 48×16 (o 16×48) pegada al
|
||||
// borde del jugador, retallada contra murs i sarcs.
|
||||
// Convenció implícita: el jugador té collision rect 16×16 als
|
||||
// últims 16 px del seu pixel_rect (els "peus"); aquesta funció
|
||||
// n'usa la geometria per al damage_rect i el feet_y delegat.
|
||||
void configure(Direction dir, const SDL_FRect& owner_box, float range_px);
|
||||
|
||||
// Injecta el context que necessita la fase de dany: punter a
|
||||
// l'`EntityManager` (per iterar Hazards) i a la llista de jugadors
|
||||
// (per al friendly fire). Si no es crida, els frames actius no
|
||||
// apliquen dany — el whip queda merament visual. Cridar abans del
|
||||
// primer `update` (típicament just després de `configure`).
|
||||
void setWorldContext(EntityManager* manager, const std::vector<Player*>* players);
|
||||
|
||||
// Avança animació; durant frames actius, aplica dany; quan
|
||||
// l'animació es completa, marca dead.
|
||||
void update(float delta_time) override;
|
||||
|
||||
[[nodiscard]] auto getDirection() const -> Direction { return direction_; }
|
||||
|
||||
// True si el frame actual de l'animació és un dels `active_frames`
|
||||
// declarats al YAML (els frames durant els quals la hitbox aplica
|
||||
// dany). Wind-up i recovery retornen false.
|
||||
[[nodiscard]] auto isFrameActive() const -> bool;
|
||||
|
||||
// Llistat dels frames actius (lectura del YAML, immutable per
|
||||
// instància). Exposat per als smoke tests.
|
||||
[[nodiscard]] auto getActiveFrames() const -> const std::vector<int>& { return active_frames_; }
|
||||
|
||||
// Llargada de la hitbox abans de retallar contra murs/sarcs (en píxels).
|
||||
[[nodiscard]] auto getRangePx() const -> float { return range_px_; }
|
||||
|
||||
// Hitbox efectiva de dany (ja retallada pel primer bloquejador en la
|
||||
// línia d'atac). Exposada per als smoke tests; en producció només la
|
||||
// consumeix la pròpia `applyDamage`.
|
||||
[[nodiscard]] auto getDamageRect() const -> SDL_FRect { return damage_rect_; }
|
||||
|
||||
// Y dels peus delegada al `owner_box` capturat a `configure()`: el
|
||||
// látigo es z-ordena al mateix bucket que el jugador que l'ha
|
||||
// llançat (= base inferior del seu pixel_rect, que coincideix amb
|
||||
// el feet_y del jugador a la convenció estàndard del player AEE).
|
||||
// Snapshot, no consulta dinàmica: el jugador queda quiet durant
|
||||
// ATTACKING, així que snapshot ≡ live per a la vida del whip.
|
||||
[[nodiscard]] auto getFeetY() const -> float override;
|
||||
|
||||
// Clau al `Resource::Cache` per a la surface de la direcció actual
|
||||
// (la que ha triat `configure()`). El caller (típicament `GameScene`)
|
||||
// resol la surface i la passa via `setSurface()`. Aquesta separació
|
||||
// manté el codi de l'entitat sense dependència directa al
|
||||
// `Resource::Cache`, igual que la resta d'entitats del joc.
|
||||
[[nodiscard]] auto getSurfaceKey() const -> const std::string&;
|
||||
|
||||
private:
|
||||
[[nodiscard]] auto spriteFor(Direction dir) const -> const DirSprite&;
|
||||
|
||||
// Calcula la `damage_rect_` final: retalla la longitud de l'atac
|
||||
// contra el primer bloquejador (mur extern del playfield o sarc) en
|
||||
// la línia d'atac. Implementació iterativa píxel a píxel — el rang
|
||||
// típic és ~48 px, així que el cost és menyspreable.
|
||||
void computeDamageRect(Direction dir, float spawn_x, float spawn_y, float range_px);
|
||||
|
||||
// Aplica dany aquest frame: itera Hazards (Mummy whippable) i
|
||||
// jugadors (excepte `owner_`) que solapen amb `damage_rect_` i
|
||||
// estan en estat "atacable", crida `enterHurt` amb la direcció
|
||||
// adequada i els marca a `already_hit_` per garantir un sol
|
||||
// impacte per swing per objectiu.
|
||||
void applyDamage();
|
||||
|
||||
Direction direction_{Direction::EAST};
|
||||
AnimationComponent anim_swing_;
|
||||
std::vector<int> active_frames_;
|
||||
// Set d'objectius ja colpejats aquest swing: usem `void*` perquè
|
||||
// hi guardem tant `Entity*` (Mummy) com `Player*` (que NO hereta
|
||||
// d'Entity) sense `reinterpret_cast` semànticament dubtós.
|
||||
std::unordered_set<void*> already_hit_;
|
||||
float range_px_{0.0F};
|
||||
SDL_FRect damage_rect_{0.0F, 0.0F, 0.0F, 0.0F};
|
||||
|
||||
// Snapshot del feet_y de l'owner pres a `configure()` (= base
|
||||
// inferior del seu pixel_rect, que coincideix amb el `getFeetY()`
|
||||
// del Player en la convenció estàndard d'AEE). El retornem des
|
||||
// del nostre `getFeetY()` per delegar el bucket d'y-sort al jugador.
|
||||
float owner_feet_y_{0.0F};
|
||||
|
||||
EntityManager* entity_manager_{nullptr};
|
||||
const std::vector<Player*>* players_{nullptr};
|
||||
|
||||
DirSprite sprite_north_;
|
||||
DirSprite sprite_south_;
|
||||
DirSprite sprite_east_;
|
||||
DirSprite sprite_west_;
|
||||
};
|
||||
@@ -140,8 +140,10 @@ class Entity {
|
||||
|
||||
// Y dels peus: la vora inferior de la caixa de col·lisió. La usarà el
|
||||
// Z-order per ordenar entitats i jugador de menys a més profunditat
|
||||
// (REND-01, quan s'implemente).
|
||||
[[nodiscard]] auto getFeetY() const -> float;
|
||||
// (REND-01, quan s'implemente). Virtual perquè entitats efímeres
|
||||
// (com `WhipAttack`) poden delegar al seu owner per quedar al
|
||||
// mateix bucket d'y-sort que el jugador que les ha llançat.
|
||||
[[nodiscard]] virtual auto getFeetY() const -> float;
|
||||
|
||||
// El loader li assigna una caixa explícita llegida del YAML; sense
|
||||
// aquesta crida, l'entitat queda sense `CollisionBox` i la caixa de
|
||||
|
||||
@@ -32,11 +32,41 @@ EntityManager::EntityManager() {
|
||||
const auto* tile = std::get_if<TilePos>(&ev.payload);
|
||||
if (tile != nullptr) { notifyPlayerTilePainted(tile->col, tile->row); }
|
||||
});
|
||||
|
||||
// Subscripció a `player_exited` (publicat per Door en travessar la
|
||||
// porta oberta amb un jugador): comptem una vegada per jugador i,
|
||||
// quan tots els jugadors actius han eixit, emetem `level_complete`
|
||||
// — l'event d'agregat que GameScene escolta per disparar la
|
||||
// transició FSM a ROOM_OUTRO. La idempotència de Door (check
|
||||
// `isExited`) garanteix que cada jugador suma com a màxim un cop.
|
||||
exit_sub_id_ = bus_.subscribe("player_exited", [this](const Event& /*ev*/) {
|
||||
++exited_players_;
|
||||
if (level_complete_emitted_) { return; }
|
||||
if (expected_players_ > 0 && exited_players_ >= expected_players_) {
|
||||
level_complete_emitted_ = true;
|
||||
bus_.publish(Event{
|
||||
.name = "level_complete",
|
||||
.payload = std::monostate{},
|
||||
.player_id = -1,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
EntityManager::~EntityManager() {
|
||||
bus_.unsubscribe(sarc_sub_id_);
|
||||
bus_.unsubscribe(tile_sub_id_);
|
||||
bus_.unsubscribe(exit_sub_id_);
|
||||
}
|
||||
|
||||
void EntityManager::setExpectedPlayerCount(int n) {
|
||||
expected_players_ = n;
|
||||
exited_players_ = 0;
|
||||
level_complete_emitted_ = false;
|
||||
}
|
||||
|
||||
void EntityManager::forceOpenDoor() {
|
||||
if (door_ != nullptr) { door_->open(); }
|
||||
}
|
||||
|
||||
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
||||
|
||||
@@ -90,10 +90,34 @@ class EntityManager {
|
||||
// `Map::tile_size`, `tilemap_origin_*` i `getSarcRect`).
|
||||
void loadFromLevel(const std::string& level_yaml_path);
|
||||
|
||||
// Reset complet del nivell: destrueix totes les entitats, reseteja
|
||||
// els comptadors interns (sarcòfags oberts, total, exited, flag de
|
||||
// level_complete) i torna a cridar `loadFromLevel` per regenerar les
|
||||
// entitats des del YAML. El bus i les seues subscripcions
|
||||
// sobreviuen — això inclou les subscripcions externes (paint_layer_,
|
||||
// hud_, players, GameScene). Cridat des de GameScene a la transició
|
||||
// ROOM_OUTRO → ROOM_INTRO. `setExpectedPlayerCount` cal cridar-lo de
|
||||
// nou després si vols agregació de level_complete al nou cicle.
|
||||
void reload(const std::string& level_yaml_path);
|
||||
|
||||
// Fixa quants jugadors actius es consideren al recompte d'EXITED.
|
||||
// Cridat per GameScene un cop instanciats els jugadors. Si manca,
|
||||
// `expected_players_ = 0` i mai s'emet `level_complete` automàticament.
|
||||
// Cridar-lo de nou al respawn (futur reload) reseteja el comptador.
|
||||
void setExpectedPlayerCount(int n);
|
||||
|
||||
[[nodiscard]] auto getBus() -> EventBus& { return bus_; }
|
||||
[[nodiscard]] auto entityCount() const -> std::size_t { return entities_.size(); }
|
||||
[[nodiscard]] auto sarcophagusCount() const -> std::size_t { return total_sarcs_; }
|
||||
[[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; }
|
||||
[[nodiscard]] auto exitedPlayerCount() const -> std::size_t { return exited_players_; }
|
||||
|
||||
// Força l'obertura de la porta sense que els sarcòfags estiguin
|
||||
// oberts. Pensat com a hook de debug per a la GameScene: el flux
|
||||
// posterior (creuar la porta → `player_exited` → `level_complete`
|
||||
// → OUTRO) segueix exactament igual perquè depèn de Door, no de
|
||||
// sarcs. No-op si la porta ja està oberta o si el nivell no en té.
|
||||
void forceOpenDoor();
|
||||
|
||||
private:
|
||||
EventBus bus_;
|
||||
@@ -101,6 +125,14 @@ class EntityManager {
|
||||
Door* door_{nullptr};
|
||||
std::size_t total_sarcs_{0};
|
||||
std::size_t opened_sarcs_{0};
|
||||
// Recompte d'`player_exited`: incrementat a la subscripció al bus,
|
||||
// resetejat per `setExpectedPlayerCount`. Quan `exited_players_` arriba
|
||||
// a `expected_players_` (i tots dos > 0), publica `level_complete`
|
||||
// amb `player_id = -1` (event d'agregat, no atribuible a un jugador).
|
||||
int expected_players_{0};
|
||||
int exited_players_{0};
|
||||
bool level_complete_emitted_{false};
|
||||
EventBus::SubId sarc_sub_id_{0};
|
||||
EventBus::SubId tile_sub_id_{0};
|
||||
EventBus::SubId exit_sub_id_{0};
|
||||
};
|
||||
@@ -65,7 +65,7 @@ void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
|
||||
const auto BOX = entity->getCollisionRect();
|
||||
const int E_HEIGHT = entity->getHeight();
|
||||
for (auto* p : players) {
|
||||
if (p == nullptr) { continue; }
|
||||
if (p == nullptr || p->isOutOfPlay()) { continue; }
|
||||
// Filtre per altura: si el jugador està en l'aire i està per
|
||||
// damunt de l'alçada del cos de l'entitat, no hi ha contacte.
|
||||
// És el mecanisme del salt per esquivar momies, roques, etc.
|
||||
@@ -291,6 +291,34 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Col·loca la porta al mur trasser segons `Map::door_col`. La porta
|
||||
// viu sempre al `wall_north`; només la columna varia per nivell. El
|
||||
// sprite (típicament 48×32) es centra horitzontalment sobre la
|
||||
// columna transitable i s'alinea verticalment de manera que la seua
|
||||
// vora inferior coincideixi amb la base del strip del mur trasser
|
||||
// (= `playfield_y`). Amb sprite 48×32 i `back_wall_h = 24` això vol
|
||||
// dir que els 8 px superiors del sprite queden dins del HUD i hi
|
||||
// queden tapats — comportament intencional.
|
||||
auto placeDoor(EntityManager& manager, std::unique_ptr<Entity> entity, const SpriteInfo& sprite) -> bool {
|
||||
if (Map::door_col <= 0 || Map::tile_size <= 0) {
|
||||
std::cerr << "EntityManager: door requereix `geometry.door_col` definit al mapa\n";
|
||||
return false;
|
||||
}
|
||||
const float CENTER_X = static_cast<float>((Map::door_col * Map::tile_size) + Map::tilemap_origin_x) + (static_cast<float>(Map::tile_size) / 2.0F);
|
||||
const SDL_FRect RENDER{
|
||||
.x = CENTER_X - (sprite.width / 2.0F),
|
||||
.y = static_cast<float>(Map::playfield_y) - sprite.height,
|
||||
.w = sprite.width,
|
||||
.h = sprite.height,
|
||||
};
|
||||
entity->setPixelRect(RENDER);
|
||||
|
||||
auto* door_ptr = dynamic_cast<Door*>(entity.get());
|
||||
manager.add(std::move(entity));
|
||||
if (door_ptr != nullptr) { manager.setDoor(door_ptr); }
|
||||
return true;
|
||||
}
|
||||
|
||||
auto placeGeneric(EntityManager& manager, std::unique_ptr<Entity> entity, const std::string& type, const SpriteInfo& sprite, const std::string& anchor, const fkyaml::node& spawn) -> bool {
|
||||
if (!spawn.contains("position")) {
|
||||
std::cerr << "EntityManager: " << type << " sense position\n";
|
||||
@@ -322,17 +350,30 @@ namespace {
|
||||
}
|
||||
entity->setPixelRect({.x = rect_x, .y = rect_y, .w = sprite.width, .h = sprite.height});
|
||||
|
||||
if (type == "door") {
|
||||
auto* door_ptr = dynamic_cast<Door*>(entity.get());
|
||||
manager.add(std::move(entity));
|
||||
if (door_ptr != nullptr) { manager.setDoor(door_ptr); }
|
||||
} else {
|
||||
manager.add(std::move(entity));
|
||||
}
|
||||
manager.add(std::move(entity));
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void EntityManager::reload(const std::string& level_yaml_path) {
|
||||
// Destrucció ordenada: les entitats moren aquí; les seues
|
||||
// subscripcions implícites desapareixen amb la seua vida (cap d'elles
|
||||
// se subscriu directament al bus — només publiquen). El bus i les
|
||||
// subscripcions externes (PaintLayer, HUD, GameScene) no es toquen.
|
||||
entities_.clear();
|
||||
door_ = nullptr;
|
||||
total_sarcs_ = 0;
|
||||
opened_sarcs_ = 0;
|
||||
// No reset de `expected_players_`: GameScene el tornarà a fixar
|
||||
// (amb el mateix valor o un de nou) després del reload, com a part
|
||||
// del respawn dels jugadors. Reseteig dels comptadors d'EXITED i
|
||||
// del flag d'emissió perquè el cicle següent pugui reagregar net.
|
||||
exited_players_ = 0;
|
||||
level_complete_emitted_ = false;
|
||||
|
||||
loadFromLevel(level_yaml_path);
|
||||
}
|
||||
|
||||
void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
||||
const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path);
|
||||
int loaded = 0;
|
||||
@@ -346,9 +387,15 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
||||
auto entity = buildEntity(TYPE, sprite, anchor);
|
||||
if (entity == nullptr) { continue; }
|
||||
|
||||
const bool OK = (TYPE == "sarcophagus")
|
||||
? placeSarcophagus(*this, std::move(entity), sprite, spawn)
|
||||
: placeGeneric(*this, std::move(entity), TYPE, sprite, anchor, spawn);
|
||||
bool ok = false;
|
||||
if (TYPE == "sarcophagus") {
|
||||
ok = placeSarcophagus(*this, std::move(entity), sprite, spawn);
|
||||
} else if (TYPE == "door") {
|
||||
ok = placeDoor(*this, std::move(entity), sprite);
|
||||
} else {
|
||||
ok = placeGeneric(*this, std::move(entity), TYPE, sprite, anchor, spawn);
|
||||
}
|
||||
const bool OK = ok;
|
||||
if (OK) { ++loaded; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
@@ -12,6 +14,7 @@
|
||||
// Forward: els events poden dur un punter a `Entity` (p. ex. quin sarcòfag
|
||||
// s'ha obert). No cal incloure el header complet aquí.
|
||||
class Entity;
|
||||
class Player;
|
||||
|
||||
// Event pub/sub minimalista. Hi ha un únic `EventBus` per mundus (propietat
|
||||
// de l'`EntityManager`); les entitats que el necessiten en reben un punter
|
||||
@@ -49,9 +52,31 @@ struct WorldPos {
|
||||
float y{0.0F};
|
||||
};
|
||||
|
||||
// Payload de `whip_spawn`: el `Player` el publica quan entra a ATTACKING
|
||||
// i el `GameScene` el consumeix per crear un `WhipAttack` via
|
||||
// `EntityManager::add`. Així el Player no depèn ni de `EntityManager` ni de
|
||||
// `WhipAttack`, i el contracte queda dins del bus.
|
||||
// - `owner` : Player que ha llançat l'atac (no rep dany propi).
|
||||
// - `direction` : Player::Cardinal castat a int (1=N, 2=S, 3=E, 4=W) per
|
||||
// evitar dependència cíclica al header.
|
||||
// - `owner_box` : pixel_rect del jugador (típicament 16×32 a AEE). El
|
||||
// WhipAttack en deriva tant la `draw_rect_` (sprite del
|
||||
// látigo, més gran que el jugador per cobrir wind-up i
|
||||
// solapament) com la `damage_rect_` (caixa de dany al
|
||||
// nivell dels peus, retallada per murs/sarcs).
|
||||
// - `range_px` : longitud nominal de la hitbox de dany abans de
|
||||
// retallar contra murs i sarcòfags. Llegida del YAML
|
||||
// del jugador.
|
||||
struct WhipSpawnPayload {
|
||||
Player* owner{nullptr};
|
||||
int direction{0};
|
||||
SDL_FRect owner_box{0.0F, 0.0F, 0.0F, 0.0F};
|
||||
float range_px{0.0F};
|
||||
};
|
||||
|
||||
struct Event {
|
||||
std::string name;
|
||||
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile, WorldPos>;
|
||||
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile, WorldPos, WhipSpawnPayload>;
|
||||
Payload payload;
|
||||
// Jugador que ha originat (o provocat) l'event. -1 = sense jugador
|
||||
// associat (events del món, com `sarcophagus_opened` des de la pròpia
|
||||
@@ -101,6 +126,9 @@ class EventBus {
|
||||
void publish(const std::string& name, WorldPos pos) {
|
||||
publish(Event{.name = name, .payload = pos});
|
||||
}
|
||||
void publish(const std::string& name, const WhipSpawnPayload& whip) {
|
||||
publish(Event{.name = name, .payload = whip});
|
||||
}
|
||||
|
||||
private:
|
||||
struct Subscription {
|
||||
|
||||
@@ -68,6 +68,7 @@ Player::TurnConfig Player::turn_config;
|
||||
Player::InertiaConfig Player::inertia_config;
|
||||
Player::JumpConfig Player::jump_config;
|
||||
Player::HurtConfig Player::hurt_config;
|
||||
Player::WhipConfig Player::whip_config;
|
||||
|
||||
void Player::loadFrom(const std::string& yaml_path) {
|
||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||
@@ -114,6 +115,11 @@ void Player::loadFrom(const std::string& yaml_path) {
|
||||
// Bloc opcional `hurt:` — estat post-impacte. Si manca o el speed
|
||||
// és 0, l'estat HURT mai s'activa (cas degenerat).
|
||||
readHurtBlock(yaml);
|
||||
|
||||
// Bloc opcional `whip:` — paràmetres del látigo. Si manca, el
|
||||
// jugador no atacarà efectivament (ATTACKING dura 0 ms i el flanc
|
||||
// d'input es perd).
|
||||
readWhipBlock(yaml);
|
||||
}
|
||||
|
||||
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
|
||||
@@ -175,6 +181,16 @@ void Player::readHurtBlock(const fkyaml::node& yaml) {
|
||||
if (h.contains("brake_time_ms")) { hurt_config.brake_time_ms = h["brake_time_ms"].get_value<int>(); }
|
||||
}
|
||||
|
||||
void Player::readWhipBlock(const fkyaml::node& yaml) {
|
||||
whip_config = WhipConfig{};
|
||||
if (!yaml.contains("whip")) { return; }
|
||||
const auto& w = yaml["whip"];
|
||||
if (w.contains("range_tiles")) { whip_config.range_tiles = w["range_tiles"].get_value<float>(); }
|
||||
if (w.contains("swing_ms")) { whip_config.swing_ms = w["swing_ms"].get_value<int>(); }
|
||||
if (w.contains("cooldown_ms")) { whip_config.cooldown_ms = w["cooldown_ms"].get_value<int>(); }
|
||||
if (w.contains("damage")) { whip_config.damage = w["damage"].get_value<int>(); }
|
||||
}
|
||||
|
||||
void Player::applyCardinal(Cardinal c) {
|
||||
switch (c) {
|
||||
case Cardinal::NORTH:
|
||||
@@ -328,6 +344,10 @@ void Player::update(float delta_time) {
|
||||
}
|
||||
|
||||
auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
||||
// EXITED/DISABLED: jugador fora del joc. No xoca amb res ni res
|
||||
// xoca amb ell. També es filtren els altres jugadors fora de joc
|
||||
// al check actor↔actor més avall via el predicat `isOutOfPlay`.
|
||||
if (isOutOfPlay()) { return false; }
|
||||
if (map_.collides(box)) { return true; }
|
||||
if (all_players_ == nullptr) { return false; }
|
||||
// Per al contacte actor-actor usem la hit-box petita a banda i banda:
|
||||
@@ -353,6 +373,7 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
||||
const float MY_Z = jump_z_;
|
||||
return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT, MY_AIR, MY_HURT, MY_Z](const Player* p) {
|
||||
if (p == nullptr || p == this) { return false; }
|
||||
if (p->isOutOfPlay()) { return false; }
|
||||
if (MY_HURT || p->isHurt()) { return false; }
|
||||
if (MY_AIR != p->isAirborne()) { return false; }
|
||||
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
|
||||
@@ -398,7 +419,16 @@ void Player::integrateAxis(float& phys, float input, float delta_time, float mot
|
||||
}
|
||||
}
|
||||
|
||||
// Pipeline d'estats i input separats; extracció a helpers empitjoraria la
|
||||
// llegibilitat. La complexitat puja 1 per la guarda EXITED.
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
||||
void Player::tick(float delta_time) {
|
||||
// EXITED/DISABLED: jugador fora del joc, no fa res. Cap input, cap
|
||||
// física, cap bus event. Continua sent al vector de jugadors per
|
||||
// simplicitat (no s'esborra) però queda completament inert fins
|
||||
// que GameScene el reactiva (respawn / spawnFromAir).
|
||||
if (isOutOfPlay()) { return; }
|
||||
|
||||
// Reset per frame: el resolver del GameScene el tornarà a posar a
|
||||
// true si aquest frame hi ha push actiu. Així quan el push acaba
|
||||
// (un deixa d'empentar, l'altre comença a caminar), l'animació torna
|
||||
@@ -426,6 +456,7 @@ void Player::tick(float delta_time) {
|
||||
// durant el stun: així si l'usuari l'ha estat prement abans, no
|
||||
// se'l consumeix com a salt nou en sortir del stun.
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -435,11 +466,36 @@ void Player::tick(float delta_time) {
|
||||
// pulsació quan torne a terra es detecte com a flanc nou.
|
||||
if (state_ == State::AIRBORNE) {
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
setVelX(intended_vx_);
|
||||
setVelY(intended_vy_);
|
||||
return;
|
||||
}
|
||||
|
||||
// En ATTACKING el jugador queda bloquejat tot el `swing_ms +
|
||||
// cooldown_ms`: ni input direccional, ni salt, ni atac, ni canvi
|
||||
// de facing. El timer descompta amb `delta_time * 1000` per
|
||||
// mantenir la convenció de delta_ms de la resta d'estats temporals.
|
||||
// Mantenim els flancs dels botons actualitzats perquè una
|
||||
// tecla mantinguda no es comporte com a flanc nou en sortir.
|
||||
if (state_ == State::ATTACKING) {
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
attacking_remaining_ms_ -= delta_time * 1000.0F;
|
||||
if (attacking_remaining_ms_ <= 0.0F) {
|
||||
attacking_remaining_ms_ = 0.0F;
|
||||
state_ = State::GROUNDED;
|
||||
}
|
||||
input_vx_ = 0.0F;
|
||||
input_vy_ = 0.0F;
|
||||
intended_vx_ = 0.0F;
|
||||
intended_vy_ = 0.0F;
|
||||
motion_time_ms_ = 0.0F;
|
||||
setVelX(0.0F);
|
||||
setVelY(0.0F);
|
||||
return;
|
||||
}
|
||||
|
||||
// En HURT el jugador està projectat sense control: descomptem
|
||||
// velocitat per inèrcia (decaiment lineal fins a 0). Quan arriba a
|
||||
// zero, tornem a GROUNDED i el frame següent ja torna a llegir input.
|
||||
@@ -447,6 +503,7 @@ void Player::tick(float delta_time) {
|
||||
// estats), que també gestiona el xoc contra murs.
|
||||
if (state_ == State::HURT) {
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
const float DECEL = (hurt_config.brake_time_ms > 0)
|
||||
? hurt_config.initial_speed_px_per_s / (static_cast<float>(hurt_config.brake_time_ms) / 1000.0F)
|
||||
: 0.0F;
|
||||
@@ -521,6 +578,18 @@ void Player::tick(float delta_time) {
|
||||
// només disparen amb intenció explícita del jugador.
|
||||
updateFacingAndTurn(delta_time, input_vx_, input_vy_);
|
||||
|
||||
// Detecció de flanc del botó d'atac. Es processa ABANS del salt
|
||||
// perquè ATTACKING i AIRBORNE són mútuament excloents i, davant un
|
||||
// mateix-frame, l'atac té prioritat (millor castigar la pulsació
|
||||
// doble per excés que perdre l'atac per un toc accidental al salt).
|
||||
// En entrar a ATTACKING, descartem el flanc del salt per no que
|
||||
// queda penjat per a quan es desbloque.
|
||||
if (consumeAttackEdge() && whip_config.swing_ms > 0) {
|
||||
enterAttacking(currentFacingCardinal());
|
||||
(void)consumeJumpEdge();
|
||||
return;
|
||||
}
|
||||
|
||||
// Detecció de flanc del botó de salt. Es processa DESPRÉS d'aplicar
|
||||
// el facing perquè el sprite del salt usa el facing del moment del
|
||||
// despegue. `being_pushed_` no ho pot bloquejar ací (encara no s'ha
|
||||
@@ -551,6 +620,81 @@ auto Player::consumeJumpEdge() -> bool {
|
||||
return edge;
|
||||
}
|
||||
|
||||
auto Player::consumeAttackEdge() -> bool {
|
||||
// Mateix patró que `consumeJumpEdge` però per al botó d'atac. Mando:
|
||||
// SDL_GAMEPAD_BUTTON_WEST (X a Xbox / Quadrat a PlayStation), oposat
|
||||
// a SOUTH per evitar topades semàntiques.
|
||||
const bool* keys = InputState::getKeyboardState();
|
||||
const bool KEY_NOW = keys[controls_.attack];
|
||||
bool edge = (KEY_NOW && !attack_held_keyboard_);
|
||||
attack_held_keyboard_ = KEY_NOW;
|
||||
|
||||
SDL_Gamepad* pad = GamepadManager::get().getForPlayer(id_);
|
||||
const bool PAD_NOW = (pad != nullptr) && InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_WEST);
|
||||
if (PAD_NOW && !attack_held_gamepad_) { edge = true; }
|
||||
attack_held_gamepad_ = PAD_NOW;
|
||||
|
||||
return edge;
|
||||
}
|
||||
|
||||
auto Player::currentFacingCardinal() const -> Cardinal {
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return Cardinal::NORTH;
|
||||
case Facing::DOWN:
|
||||
return Cardinal::SOUTH;
|
||||
case Facing::SIDE:
|
||||
break;
|
||||
}
|
||||
// Lateral: el flip discrimina E/W. Sense flip = mira a la dreta
|
||||
// (convenció estable a tots els assets actuals).
|
||||
return (getFlip() == SDL_FLIP_HORIZONTAL) ? Cardinal::WEST : Cardinal::EAST;
|
||||
}
|
||||
|
||||
void Player::enterAttacking(Cardinal facing) {
|
||||
state_ = State::ATTACKING;
|
||||
attacking_remaining_ms_ = static_cast<float>(whip_config.swing_ms + whip_config.cooldown_ms);
|
||||
|
||||
// Garantim que facing/flip i cardinal queden coherents amb la
|
||||
// direcció triada (per si l'atac s'ha disparat amb facing residual,
|
||||
// p.ex. just després d'un canvi d'input).
|
||||
applyCardinal(facing);
|
||||
|
||||
if (facing == Cardinal::NONE) { return; }
|
||||
|
||||
// Mapa de Cardinal → int al payload (vegeu `WhipSpawnPayload`).
|
||||
int dir_int = 0;
|
||||
switch (facing) {
|
||||
case Cardinal::NORTH:
|
||||
dir_int = 1;
|
||||
break;
|
||||
case Cardinal::SOUTH:
|
||||
dir_int = 2;
|
||||
break;
|
||||
case Cardinal::EAST:
|
||||
dir_int = 3;
|
||||
break;
|
||||
case Cardinal::WEST:
|
||||
dir_int = 4;
|
||||
break;
|
||||
case Cardinal::NONE:
|
||||
return;
|
||||
}
|
||||
|
||||
// Passem el pixel_rect (16×32 a sam/morcus) al payload; el WhipAttack
|
||||
// en deriva tant la draw_rect (sprite del látigo, més gran) com la
|
||||
// damage_rect (al nivell dels peus, retallada per bloquejadors).
|
||||
if (bus_ != nullptr) {
|
||||
const float RANGE_PX = whip_config.range_tiles * static_cast<float>(Map::tile_size);
|
||||
bus_->publish("whip_spawn", WhipSpawnPayload{
|
||||
.owner = this,
|
||||
.direction = dir_int,
|
||||
.owner_box = getPosition(),
|
||||
.range_px = RANGE_PX,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Player::startJump() {
|
||||
// No saltem si l'estat no ho permet. `being_pushed_` no és segur de
|
||||
// consultar ací (es pot haver marcat o no, segons l'ordre del frame),
|
||||
@@ -683,7 +827,15 @@ auto Player::integrateJump(float delta_time) -> bool {
|
||||
state_ = State::LANDING_STUN;
|
||||
landing_stun_remaining_ms_ = static_cast<float>(jump_config.landing_stun_ms);
|
||||
if (!jump_config.land_sound_key.empty() && Audio::get() != nullptr) {
|
||||
Audio::get()->playSound(jump_config.land_sound_key, Audio::Group::GAME);
|
||||
// Consumeix el preset armat per GameScene al spawn aeri
|
||||
// d'INTRO (single-shot): el primer LAND el sona reverberat,
|
||||
// els salts següents cauen al camí sec.
|
||||
if (next_land_reverb_preset_.empty()) {
|
||||
Audio::get()->playSound(jump_config.land_sound_key, Audio::Group::GAME);
|
||||
} else {
|
||||
Audio::get()->playSoundWithReverb(jump_config.land_sound_key, next_land_reverb_preset_, Audio::Group::GAME);
|
||||
next_land_reverb_preset_.clear();
|
||||
}
|
||||
}
|
||||
if (bus_ != nullptr) {
|
||||
bus_->publish(Event{
|
||||
@@ -706,6 +858,9 @@ void Player::resolveLandingOverlap() {
|
||||
if (all_players_ == nullptr) { return; }
|
||||
for (Player* other : *all_players_) {
|
||||
if (other == nullptr || other == this) { continue; }
|
||||
// Salta jugadors fora del joc: un caigut sobre un slot DISABLED
|
||||
// (o sobre un EXITED encara al vector) no els ha de projectar.
|
||||
if (other->isOutOfPlay()) { continue; }
|
||||
// Test amb la hit-box (8×8 de contacte precís), no la base-box.
|
||||
// Coherent amb la resta de col·lisions actor-actor del joc:
|
||||
// momies, fuetades futures, resolver PvP… tots usen hit-box. Un
|
||||
@@ -740,6 +895,9 @@ void Player::resolveLandingOverlap() {
|
||||
}
|
||||
|
||||
void Player::applyMovement(float delta_time) {
|
||||
// EXITED/DISABLED: cap moviment ni col·lisió. Idèntic al gating de `tick`.
|
||||
if (isOutOfPlay()) { return; }
|
||||
|
||||
// Integració del salt: si estem AIRBORNE, avança jump_z_/jump_vz_.
|
||||
// El facing i intended_v* es mantenen del moment del despegue (no es
|
||||
// recalculen). Si toquem el sòl aquest frame, transicionem a
|
||||
@@ -760,6 +918,15 @@ void Player::applyMovement(float delta_time) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state_ == State::ATTACKING) {
|
||||
// Cap moviment durant ATTACKING. Animem (perquè avance la
|
||||
// seqüència `attack_*`) i actualitzem la clau d'animació segons
|
||||
// la sub-fase (swing vs cooldown).
|
||||
animate(delta_time);
|
||||
setCurrentAnimation(animationName(false));
|
||||
return;
|
||||
}
|
||||
|
||||
// El resolver del GameScene pot haver escalat `intended_vx_/vy_`
|
||||
// (cas push: velocitat * push_factor) o haver-les anul·lat (cas
|
||||
// head-on). Consumim-les aquí com a velocitat definitiva del frame.
|
||||
@@ -859,6 +1026,11 @@ void Player::publishTileEnteredIfChanged() {
|
||||
}
|
||||
|
||||
void Player::render() {
|
||||
// EXITED/DISABLED: invisible. La crida al render base es salta
|
||||
// sencera; el jugador no apareix al z-sort tot i seguir vivint al
|
||||
// vector.
|
||||
if (isOutOfPlay()) { return; }
|
||||
|
||||
// Mentre el jugador està enlaire, el sprite es desplaça visualment
|
||||
// cap amunt `jump_z_` píxels. Modifiquem `pos_.y` (la posició entera
|
||||
// de render) abans de cridar el render base, i la restaurem després
|
||||
@@ -956,6 +1128,37 @@ auto Player::animationName(bool moving) const -> const char* {
|
||||
}
|
||||
return "hurt";
|
||||
}
|
||||
// ATTACKING té dues sub-fases: durant `swing_ms` el sprite és
|
||||
// `attack_*` (animació del braç llançant el látigo); durant
|
||||
// `cooldown_ms` posterior canviem a `recover_*` per a frames
|
||||
// pròpis de recuperació. Mentre l'art no estiga dibuixat, els
|
||||
// YAMLs apunten `recover_*` als mateixos frames que `stand_*` —
|
||||
// visualment és idèntic, però la separació deixa l'arquitectura
|
||||
// preparada perquè en afegir frames reals només calga tocar el
|
||||
// YAML, no el codi.
|
||||
if (state_ == State::ATTACKING) {
|
||||
const bool IN_SWING = (attacking_remaining_ms_ > static_cast<float>(whip_config.cooldown_ms));
|
||||
if (IN_SWING) {
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return "attack_up";
|
||||
case Facing::DOWN:
|
||||
return "attack_down";
|
||||
case Facing::SIDE:
|
||||
break;
|
||||
}
|
||||
return "attack";
|
||||
}
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return "recover_up";
|
||||
case Facing::DOWN:
|
||||
return "recover_down";
|
||||
case Facing::SIDE:
|
||||
break;
|
||||
}
|
||||
return "recover";
|
||||
}
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return moving ? "walk_up" : "stand_up";
|
||||
@@ -987,11 +1190,99 @@ void Player::clampToPlayfield() {
|
||||
const auto MIN_X = static_cast<float>(map_.playfield_x - collision_x_offset);
|
||||
const auto MAX_X = static_cast<float>(
|
||||
map_.playfield_x + map_.playfield_w - collision_x_offset - collision_size);
|
||||
const auto MIN_Y = static_cast<float>(map_.playfield_y - collision_y_offset);
|
||||
auto min_y = static_cast<float>(map_.playfield_y - collision_y_offset);
|
||||
const auto MAX_Y = static_cast<float>(
|
||||
map_.playfield_y + map_.playfield_h - collision_y_offset - collision_size);
|
||||
|
||||
// Passadís de sortida: si la base box queda enterament dins del rang
|
||||
// X del passadís de la porta, deixem que pugi al strip del mur trasser
|
||||
// (fins a `door_corridor.y`). Fora del rang, MIN_Y es queda al
|
||||
// playfield. La regla "enterament dins" exigeix alinear-se al passadís
|
||||
// abans de pujar — la base box (12 px) hi cap amb 4 px de marge.
|
||||
if (map_.door_corridor_w > 0) {
|
||||
const float BASE_X = getPosX() + static_cast<float>(collision_x_offset);
|
||||
const float BASE_RIGHT = BASE_X + static_cast<float>(collision_size);
|
||||
const auto CORRIDOR_LEFT = static_cast<float>(map_.door_corridor_x);
|
||||
const auto CORRIDOR_RIGHT = static_cast<float>(map_.door_corridor_x + map_.door_corridor_w);
|
||||
if (BASE_X >= CORRIDOR_LEFT && BASE_RIGHT <= CORRIDOR_RIGHT) {
|
||||
min_y = static_cast<float>(map_.door_corridor_y - collision_y_offset);
|
||||
}
|
||||
}
|
||||
|
||||
setPosX(std::clamp(getPosX(), MIN_X, MAX_X));
|
||||
setPosY(std::clamp(getPosY(), MIN_Y, MAX_Y));
|
||||
setPosY(std::clamp(getPosY(), min_y, MAX_Y));
|
||||
}
|
||||
|
||||
void Player::respawn(float x, float y) {
|
||||
state_ = State::GROUNDED;
|
||||
intended_vx_ = 0.0F;
|
||||
intended_vy_ = 0.0F;
|
||||
input_vx_ = 0.0F;
|
||||
input_vy_ = 0.0F;
|
||||
being_pushed_ = false;
|
||||
jump_z_ = 0.0F;
|
||||
jump_vz_ = 0.0F;
|
||||
landing_stun_remaining_ms_ = 0.0F;
|
||||
turning_remaining_ms_ = 0.0F;
|
||||
pending_cardinal_ = Cardinal::NONE;
|
||||
intermediate_cardinal_ = Cardinal::NONE;
|
||||
last_input_cardinal_ = Cardinal::NONE;
|
||||
// Reseteig del facing al d'spawn original (mirant cap a la càmera):
|
||||
// sense això, un EXITED que entrava per la porta amb facing UP
|
||||
// re-aparix d'esquena al pròxim cicle. Mateixa lògica que el ctor.
|
||||
facing_ = Facing::DOWN;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
setCurrentAnimation("stand_down");
|
||||
setPosX(x);
|
||||
setPosY(y);
|
||||
}
|
||||
|
||||
void Player::disable() {
|
||||
if (state_ == State::DISABLED) { return; }
|
||||
state_ = State::DISABLED;
|
||||
intended_vx_ = 0.0F;
|
||||
intended_vy_ = 0.0F;
|
||||
input_vx_ = 0.0F;
|
||||
input_vy_ = 0.0F;
|
||||
being_pushed_ = false;
|
||||
jump_z_ = 0.0F;
|
||||
jump_vz_ = 0.0F;
|
||||
landing_stun_remaining_ms_ = 0.0F;
|
||||
turning_remaining_ms_ = 0.0F;
|
||||
pending_cardinal_ = Cardinal::NONE;
|
||||
intermediate_cardinal_ = Cardinal::NONE;
|
||||
last_input_cardinal_ = Cardinal::NONE;
|
||||
}
|
||||
|
||||
void Player::spawnFromAir(float x, float y) {
|
||||
// Net total: equival a un `respawn` seguit d'una entrada en l'aire.
|
||||
// Així el flag de gir, l'animació, els flancs del salt i les
|
||||
// velocitats residuals queden a un estat conegut abans de promocionar
|
||||
// a AIRBORNE. Si hi havia un cardinal d'input penjat de la sessió
|
||||
// anterior, no contamina la pròxima caiguda.
|
||||
respawn(x, y);
|
||||
|
||||
// Altura derivada de la geometria: prou per arrencar amb el sprite
|
||||
// sencer per damunt del bord superior del canvas (recordem que
|
||||
// visualment es pinta a `pos_.y - jump_z_`). Afegim un xicotet marge
|
||||
// perquè el primer frame ja siga clarament off-screen, no just al ras.
|
||||
constexpr float OFFSCREEN_MARGIN_PX = 8.0F;
|
||||
const float SPAWN_HEIGHT = y + static_cast<float>(sprite_h) + OFFSCREEN_MARGIN_PX;
|
||||
|
||||
state_ = State::AIRBORNE;
|
||||
jump_z_ = SPAWN_HEIGHT;
|
||||
// Caiguda recta: sense impuls inicial, deixem que la gravetat (la
|
||||
// mateixa que la del salt normal) faça la feina. Velocitat horitzontal
|
||||
// a zero perquè no es desviï del passadís d'spawn.
|
||||
jump_vz_ = 0.0F;
|
||||
intended_vx_ = 0.0F;
|
||||
intended_vy_ = 0.0F;
|
||||
|
||||
// Mirant cap avall durant la caiguda — `animationName(AIRBORNE)` amb
|
||||
// facing DOWN ja resol a `jump_down`. `respawn` ja ha posat facing a
|
||||
// DOWN; ho deixem explícit ací per claredat de la cinemàtica.
|
||||
facing_ = Facing::DOWN;
|
||||
setCurrentAnimation("jump_down");
|
||||
}
|
||||
|
||||
auto Player::getBaseBox() const -> SDL_FRect {
|
||||
@@ -1084,6 +1375,7 @@ auto Player::tryPushSnapBlockers(
|
||||
const SDL_FRect MY_HIT = getHitBox();
|
||||
for (Player* p : *all_players_) {
|
||||
if (p == nullptr || p == this) { continue; }
|
||||
if (p->isOutOfPlay()) { continue; }
|
||||
const SDL_FRect OTHER_HIT = p->getHitBox();
|
||||
if (SDL_HasRectIntersectionFloat(&MY_HIT, &OTHER_HIT)) {
|
||||
blockers.push_back(p);
|
||||
|
||||
@@ -24,6 +24,7 @@ class Player : public AnimatedSprite {
|
||||
SDL_Scancode left;
|
||||
SDL_Scancode right;
|
||||
SDL_Scancode jump;
|
||||
SDL_Scancode attack;
|
||||
};
|
||||
|
||||
// Estat d'alt nivell del jugador. Ortogonal a `Facing`/`Cardinal`
|
||||
@@ -44,6 +45,29 @@ class Player : public AnimatedSprite {
|
||||
AIRBORNE,
|
||||
LANDING_STUN,
|
||||
HURT,
|
||||
// Atacant amb látigo. Estat mútuament exclusiu amb AIRBORNE: no
|
||||
// es pot atacar saltant, ni saltar atacant. Cobreix swing +
|
||||
// cooldown internament: durant `whip_swing_ms` es spawneja
|
||||
// l'entitat WhipAttack i corre l'animació `attack_*`; durant
|
||||
// `whip_cooldown_ms` posterior, el jugador segueix bloquejat
|
||||
// (no input, no salt, no atac) però visualment torna a
|
||||
// `stand_*`. En total, ATTACKING dura `swing_ms + cooldown_ms`;
|
||||
// en acabar, torna a GROUNDED.
|
||||
ATTACKING,
|
||||
// Jugador que ha eixit per la porta de la sala. Despawn lògic:
|
||||
// no es renderitza, no processa input, no es mou, no col·lisiona
|
||||
// amb res ni rep contacte d'entitats. Idempotent: les crides
|
||||
// posteriors a `exit()` són no-op. El reset el porta `respawn()`
|
||||
// (cridada per GameScene en transició OUTRO → INTRO).
|
||||
EXITED,
|
||||
// Slot de jugador inactiu: instanciat però fora de partida. Mateix
|
||||
// perfil físic que EXITED (invisible, sense input, sense col·lisió,
|
||||
// ignorat pel resolver PvP), però amb semàntica diferent: no
|
||||
// compta com a "ha completat la sala" ni dispara `level_complete`.
|
||||
// Usat per (a) el slot del segon jugador en single-player, (b)
|
||||
// tots els slots durant les transicions de sala abans del spawn
|
||||
// aeri, i (c) en el futur, l'estat post-mort abans del respawn.
|
||||
DISABLED,
|
||||
};
|
||||
|
||||
// Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`).
|
||||
@@ -177,12 +201,83 @@ class Player : public AnimatedSprite {
|
||||
[[nodiscard]] auto getState() const -> State { return state_; }
|
||||
[[nodiscard]] auto isAirborne() const -> bool { return state_ == State::AIRBORNE; }
|
||||
[[nodiscard]] auto isHurt() const -> bool { return state_ == State::HURT; }
|
||||
[[nodiscard]] auto isExited() const -> bool { return state_ == State::EXITED; }
|
||||
[[nodiscard]] auto isDisabled() const -> bool { return state_ == State::DISABLED; }
|
||||
// Resum dels estats "fora del món": EXITED i DISABLED comparteixen el
|
||||
// mateix perfil físic (no es pinten, no llegeixen input, no
|
||||
// col·lisionen, no participen al resolver PvP). Tots dos gates de
|
||||
// l'update i el render comproven aquesta combinació amb una sola
|
||||
// crida per evitar oblits si demà afegim un tercer estat-fora-del-món.
|
||||
[[nodiscard]] auto isOutOfPlay() const -> bool {
|
||||
return state_ == State::EXITED || state_ == State::DISABLED;
|
||||
}
|
||||
// Tangible per a contactes jugador↔jugador: només els estats a terra
|
||||
// amb cos sòlid. AIRBORNE i HURT són intangibles (es travessen).
|
||||
// EXITED tampoc — el jugador ha eixit lògicament i ja no és al món.
|
||||
[[nodiscard]] auto isPvpTangible() const -> bool {
|
||||
return state_ == State::GROUNDED || state_ == State::LANDING_STUN;
|
||||
}
|
||||
|
||||
// Despawn lògic: el jugador acaba de creuar la porta de sortida.
|
||||
// Idempotent — només la primera crida fa la transició; les
|
||||
// posteriors són no-op. La publicació del `player_exited` al bus
|
||||
// la fa el caller (típicament `Door::onPlayerContact`) per
|
||||
// mantenir aquesta capçalera lleugera (sense incloure event_bus).
|
||||
// Definit inline perquè els consumidors de baix nivell (test
|
||||
// d'entitats) puguen enllaçar Door sense arrossegar player.cpp.
|
||||
void exit() {
|
||||
if (state_ == State::EXITED) { return; }
|
||||
state_ = State::EXITED;
|
||||
intended_vx_ = 0.0F;
|
||||
intended_vy_ = 0.0F;
|
||||
// Important: també resetegem `input_v*` perquè el resolver
|
||||
// jugador↔jugador (`resolvePlayerVsPlayer`) consulta
|
||||
// `getInputVX/Y` per decidir si un jugador "es mou". Un EXITED
|
||||
// amb input fantasma del moment d'entrar a la porta dispararia
|
||||
// la branca head-on i bloquejaria l'altre jugador en sec.
|
||||
input_vx_ = 0.0F;
|
||||
input_vy_ = 0.0F;
|
||||
being_pushed_ = false;
|
||||
}
|
||||
|
||||
// Reset complet a l'estat inicial: posició a (x, y), velocitats a 0,
|
||||
// estat GROUNDED, qualsevol flag de control reinicialitzat. Cridat
|
||||
// per GameScene a la transició ROOM_OUTRO → ROOM_INTRO. Idempotent
|
||||
// (es pot cridar des de qualsevol estat). Es manté disponible per a
|
||||
// qualsevol cas futur on calgui aparèixer el jugador ja a terra
|
||||
// sense la cinemàtica de caiguda; el flux estàndard d'arrencada de
|
||||
// sala usa `disable()` + `spawnFromAir()`.
|
||||
void respawn(float x, float y);
|
||||
|
||||
// Posa el jugador en el slot inactiu (DISABLED). Idempotent: la
|
||||
// segona crida és no-op. Mateix patró que `exit()` per simetria.
|
||||
// No publica res al bus — el GameScene decideix quan disparar el
|
||||
// `spawnFromAir` posterior, i la transició DISABLED↔jugant és
|
||||
// ortogonal a `level_complete`. Útil per:
|
||||
// - el slot del segon jugador en single-player,
|
||||
// - el període entre setState(ROOM_INTRO) i el trigger del spawn,
|
||||
// - el futur estat post-mort (abans del respawn temporitzat).
|
||||
void disable();
|
||||
|
||||
// Cinemàtica d'aparició: posiciona el jugador al seu tile final
|
||||
// (x, y al sistema de coordenades del sprite) i el deixa en
|
||||
// AIRBORNE a una altura derivada de la geometria del playfield —
|
||||
// prou per arrencar fora del marc i caure recte fins al sòl. La
|
||||
// velocitat horitzontal queda a zero (caiguda perfectament
|
||||
// vertical). En tocar terra, el flux normal d'AIRBORNE→
|
||||
// LANDING_STUN→GROUNDED el reactiva amb input. Reutilitzable per
|
||||
// al respawn post-mort: mateixa entrada cinemàtica.
|
||||
void spawnFromAir(float x, float y);
|
||||
|
||||
// Arma un preset de reverb (catàleg a `data/sounds.yaml`) per al
|
||||
// pròxim aterratge d'aquest jugador. Pensat per a la cinemàtica
|
||||
// d'INTRO de sala: GameScene crida aquesta funció just després de
|
||||
// `spawnFromAir`, i el primer LAND consumeix el preset (queda buit
|
||||
// immediatament). Els salts posteriors del joc, els respawns
|
||||
// post-mort i el reset general no rearmen res. `preset` buit
|
||||
// desactiva el reverb del proper LAND (no afecta cap LAND posterior).
|
||||
void armNextLandReverb(std::string preset) { next_land_reverb_preset_ = std::move(preset); }
|
||||
|
||||
// Alçada actual del salt (píxels visuals per damunt del sòl).
|
||||
// 0 quan està a terra. L'usa el render i el sistema de col·lisions
|
||||
// per altura.
|
||||
@@ -314,6 +409,14 @@ class Player : public AnimatedSprite {
|
||||
// gestiona `intended_vx_/vy_` escalats pel resolver.
|
||||
bool being_pushed_{false};
|
||||
|
||||
// Preset de reverb (catàleg de `data/sounds.yaml`) que el pròxim
|
||||
// LAND ha de fer servir; buit = so sec. L'arma `armNextLandReverb`
|
||||
// (cridada per GameScene al spawn aeri d'INTRO) i el consumeix
|
||||
// `integrateJump` quan el jugador toca terra. Cap altre punt
|
||||
// (respawn, exit, disable) el toca: així només sona reverberat el
|
||||
// primer LAND de la cinemàtica d'entrada.
|
||||
std::string next_land_reverb_preset_;
|
||||
|
||||
// Escala d'empenta usada per `snapToLane` quan descobreix un blocker
|
||||
// (mateixa del resolver). 1.0 = sense escalar (comportament si
|
||||
// GameScene no ha cridat setPushFactor).
|
||||
@@ -338,12 +441,16 @@ class Player : public AnimatedSprite {
|
||||
// Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°.
|
||||
// El parell (Facing, flip) només té 3+1 estats; aquesta enum n'afegeix
|
||||
// la 4a sense reorganitzar el sistema d'animacions.
|
||||
// Pública perquè el `WhipAttack` la passa a `enterHurt` quan colpeja
|
||||
// un altre jugador (friendly fire).
|
||||
public:
|
||||
enum class Cardinal : std::uint8_t { NONE,
|
||||
NORTH,
|
||||
SOUTH,
|
||||
EAST,
|
||||
WEST };
|
||||
|
||||
private:
|
||||
// Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb
|
||||
// `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i
|
||||
// el comportament és instantani: soltar input para el jugador en sec.
|
||||
@@ -410,6 +517,37 @@ class Player : public AnimatedSprite {
|
||||
// Llig el bloc `hurt:` del YAML i n'omple `hurt_config`.
|
||||
static void readHurtBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Configuració del látigo del jugador llegida del bloc `whip:` del
|
||||
// YAML. `range_tiles` defineix la longitud de la hitbox abans de
|
||||
// retallar; `swing_ms` és la durada de l'animació d'atac (des
|
||||
// d'`enterAttacking` fins a desbloquejar el cooldown); `cooldown_ms`
|
||||
// afegeix una fase posterior durant la qual el jugador segueix
|
||||
// bloquejat però la hitbox ja no fa dany. `damage` queda llegit per
|
||||
// a futur; per ara els objectius reben sempre `enterHurt`.
|
||||
struct WhipConfig {
|
||||
float range_tiles{0.0F};
|
||||
int swing_ms{0};
|
||||
int cooldown_ms{0};
|
||||
int damage{0};
|
||||
};
|
||||
static WhipConfig whip_config;
|
||||
|
||||
// Llig el bloc `whip:` del YAML i n'omple `whip_config`. Si manca el
|
||||
// bloc, els camps queden a 0 i el jugador no pot atacar (el flanc
|
||||
// d'input es perd silenciosament).
|
||||
static void readWhipBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Activa l'estat ATTACKING en la direcció cardinal indicada (la
|
||||
// direcció cap on mira el jugador) i publica `whip_spawn` al bus
|
||||
// perquè el GameScene cree el `WhipAttack` corresponent. Posa el
|
||||
// timer `attacking_remaining_ms_` a `swing_ms + cooldown_ms`.
|
||||
void enterAttacking(Cardinal facing);
|
||||
|
||||
// Cardinal que correspon al `(facing_, flip_)` actual del sprite —
|
||||
// la direcció cap on mira el jugador. Si `facing_ == SIDE` el
|
||||
// resultat depèn del flip; UP/DOWN són trivials. Mai retorna NONE.
|
||||
[[nodiscard]] auto currentFacingCardinal() const -> Cardinal;
|
||||
|
||||
// Activa l'estat HURT: el jugador queda projectat amb velocitat
|
||||
// inicial `hurt_config.initial_speed_px_per_s`. La direcció es tria
|
||||
// així:
|
||||
@@ -420,8 +558,12 @@ class Player : public AnimatedSprite {
|
||||
// i tria el que dóna més recorregut lliure.
|
||||
// Decelera linealment fins a parar i, en eixe instant, torna a
|
||||
// GROUNDED. Sense col·lisió amb altres jugadors mentre dura.
|
||||
// Pública: la dispara tant la cinemàtica de "caure damunt" com el
|
||||
// `WhipAttack` quan colpeja un altre jugador (friendly fire).
|
||||
public:
|
||||
void enterHurt(Cardinal preferred = Cardinal::NONE);
|
||||
|
||||
private:
|
||||
// Tria la direcció definitiva del HURT. Si `preferred` cabe el
|
||||
// recorregut natural complet, la retorna; d'altra manera tria la
|
||||
// direcció amb més recorregut lliure entre els 4 cardinals.
|
||||
@@ -456,10 +598,24 @@ class Player : public AnimatedSprite {
|
||||
bool jump_held_keyboard_{false};
|
||||
bool jump_held_gamepad_{false};
|
||||
|
||||
// Mateixa idea per al botó d'atac: edge-detection independent
|
||||
// teclat/mando, perquè mantenir la tecla no genere atacs en cadena.
|
||||
bool attack_held_keyboard_{false};
|
||||
bool attack_held_gamepad_{false};
|
||||
|
||||
// Temps que queda dins de l'estat ATTACKING (swing + cooldown). Quan
|
||||
// arriba a 0, transició a GROUNDED. Mentre dura, el jugador queda
|
||||
// bloquejat i no rep input direccional ni de salt ni d'atac.
|
||||
float attacking_remaining_ms_{0.0F};
|
||||
|
||||
// Llig el botó de salt aquest frame (teclat + mando assignat) i,
|
||||
// amb edge-detection, retorna true si s'ha de despegar el salt.
|
||||
[[nodiscard]] auto consumeJumpEdge() -> bool;
|
||||
|
||||
// Anàleg per a l'atac: edge-detection del botó d'atac (teclat
|
||||
// `controls_.attack` + `SDL_GAMEPAD_BUTTON_WEST`).
|
||||
[[nodiscard]] auto consumeAttackEdge() -> bool;
|
||||
|
||||
// Inicia un salt: copia `intended_v*` com a velocitat fixada del salt,
|
||||
// posa `jump_vz_ = impulse`, transiciona a AIRBORNE.
|
||||
void startJump();
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "game/levels/level_manager.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
|
||||
namespace Levels {
|
||||
|
||||
auto LevelManager::get() -> LevelManager& {
|
||||
static LevelManager instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
LevelManager::LevelManager() { loadIndex(); }
|
||||
|
||||
void LevelManager::loadIndex() {
|
||||
const std::string PATH = Defaults::Pyramid::LEVELS_YAML;
|
||||
auto bytes = Resource::Helper::loadFile(PATH);
|
||||
if (bytes.empty()) {
|
||||
throw std::runtime_error("LevelManager: no s'ha pogut obrir " + PATH);
|
||||
}
|
||||
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("sequence") || !yaml["sequence"].is_sequence()) {
|
||||
throw std::runtime_error("LevelManager: manca o és invàlid `sequence:` a " + PATH);
|
||||
}
|
||||
const auto& seq = yaml["sequence"];
|
||||
entries_.clear();
|
||||
entries_.reserve(seq.size());
|
||||
for (const auto& e : seq) {
|
||||
Entry entry{
|
||||
.pyramid = e["pyramid"].get_value<int>(),
|
||||
.room = e["room"].get_value<int>(),
|
||||
.file = e["file"].get_value<std::string>(),
|
||||
};
|
||||
entries_.push_back(std::move(entry));
|
||||
}
|
||||
if (entries_.empty()) {
|
||||
throw std::runtime_error("LevelManager: la seqüència de " + PATH + " és buida");
|
||||
}
|
||||
index_ = 0;
|
||||
std::cout << "LevelManager: " << entries_.size() << " sales carregades de "
|
||||
<< PATH << " (començant a " << currentLabel() << ")\n";
|
||||
}
|
||||
|
||||
auto LevelManager::currentPath() const -> const std::string& {
|
||||
return entries_[index_].file;
|
||||
}
|
||||
|
||||
auto LevelManager::currentPyramid() const -> int { return entries_[index_].pyramid; }
|
||||
auto LevelManager::currentRoom() const -> int { return entries_[index_].room; }
|
||||
|
||||
auto LevelManager::currentLabel() const -> std::string {
|
||||
return "P" + std::to_string(currentPyramid()) + "-R" + std::to_string(currentRoom());
|
||||
}
|
||||
|
||||
auto LevelManager::index() const -> int { return index_; }
|
||||
auto LevelManager::count() const -> int { return static_cast<int>(entries_.size()); }
|
||||
|
||||
auto LevelManager::advance() -> bool {
|
||||
if (index_ + 1 >= count()) { return false; }
|
||||
++index_;
|
||||
std::cout << "LevelManager: avança a " << currentLabel()
|
||||
<< " (" << currentPath() << ")\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
void LevelManager::restart() {
|
||||
index_ = 0;
|
||||
std::cout << "LevelManager: restart a " << currentLabel() << '\n';
|
||||
}
|
||||
|
||||
void LevelManager::setIndex(int new_index) {
|
||||
const int CLAMPED = std::clamp(new_index, 0, count() - 1);
|
||||
if (CLAMPED != index_) {
|
||||
index_ = CLAMPED;
|
||||
std::cout << "LevelManager: setIndex(" << new_index << ") → "
|
||||
<< currentLabel() << " (" << currentPath() << ")\n";
|
||||
}
|
||||
}
|
||||
|
||||
auto LevelManager::setStartingPoint(int pyramid, int room) -> bool {
|
||||
for (std::size_t i = 0; i < entries_.size(); ++i) {
|
||||
if (entries_[i].pyramid == pyramid && entries_[i].room == room) {
|
||||
index_ = static_cast<int>(i);
|
||||
std::cout << "LevelManager: starting_point P" << pyramid << "-R" << room
|
||||
<< " trobat a l'índex " << i << " (" << currentPath() << ")\n";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
std::cerr << "LevelManager: starting_point P" << pyramid << "-R" << room
|
||||
<< " no trobat a levels.yaml — es manté l'índex actual ("
|
||||
<< currentLabel() << ")\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Levels
|
||||
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Levels {
|
||||
|
||||
// Font de veritat de la progressió de sales del joc. Carrega
|
||||
// `data/maps/levels.yaml` a la primera crida a `get()` i exposa l'estat
|
||||
// "on sóc ara" + les operacions per moure'm. La GameScene, l'Editor i
|
||||
// els salts de debug consulten aquest singleton; ningú no manté el seu
|
||||
// propi índex de progressió.
|
||||
//
|
||||
// Meyers singleton: la destrucció estàtica només toca std::string i
|
||||
// std::vector, així que és segura sense ordenació explícita.
|
||||
class LevelManager {
|
||||
public:
|
||||
static auto get() -> LevelManager&;
|
||||
|
||||
LevelManager(const LevelManager&) = delete;
|
||||
LevelManager(LevelManager&&) = delete;
|
||||
auto operator=(const LevelManager&) -> LevelManager& = delete;
|
||||
auto operator=(LevelManager&&) -> LevelManager& = delete;
|
||||
|
||||
// Una entrada del `levels.yaml`. `file` és la ruta relativa al data
|
||||
// root (mateixa convenció que la resta de YAML del joc); pyramid i
|
||||
// room són purament informatius (logs i text d'intro).
|
||||
struct Entry {
|
||||
int pyramid{0};
|
||||
int room{0};
|
||||
std::string file;
|
||||
};
|
||||
|
||||
// Ruta del nivell actualment apuntat. Sempre vàlida un cop carregat
|
||||
// l'índex (entries_ no pot estar buit). Pensada per a `Map::loadFrom`,
|
||||
// `EntityManager::reload` i l'Editor.
|
||||
[[nodiscard]] auto currentPath() const -> const std::string&;
|
||||
[[nodiscard]] auto currentPyramid() const -> int;
|
||||
[[nodiscard]] auto currentRoom() const -> int;
|
||||
// "P1-R3" — útil per a logs i el text d'intro de sala.
|
||||
[[nodiscard]] auto currentLabel() const -> std::string;
|
||||
|
||||
// Posició dins de la seqüència. `count()` és el total de sales
|
||||
// carregades del YAML (no necessàriament 30: si afegeixes/treus
|
||||
// entrades de `levels.yaml`, el manager s'adapta).
|
||||
[[nodiscard]] auto index() const -> int;
|
||||
[[nodiscard]] auto count() const -> int;
|
||||
|
||||
// Avança al següent nivell. Retorna true si avança, false si ja
|
||||
// estava a l'últim. NO fa loop — la GameScene decideix la política
|
||||
// (loop a la primera, fi de joc, etc.) segons el context.
|
||||
auto advance() -> bool;
|
||||
|
||||
// Torna al primer nivell.
|
||||
void restart();
|
||||
|
||||
// Salta a un índex concret. Clamp als bordes [0, count()-1].
|
||||
// Pensat per als hotkeys debug.
|
||||
void setIndex(int new_index);
|
||||
|
||||
// Posiciona l'índex a la primera entrada que coincideix amb
|
||||
// (pyramid, room). Retorna true si trobada; si no, l'índex no
|
||||
// es modifica i el caller decideix què fer (típicament ignorar i
|
||||
// arrencar des de la primera sala). Pensat per al
|
||||
// `debug.yaml.starting_level` i per a futures lookups per coords.
|
||||
auto setStartingPoint(int pyramid, int room) -> bool;
|
||||
|
||||
private:
|
||||
LevelManager();
|
||||
~LevelManager() = default;
|
||||
|
||||
// Carrega `data/maps/levels.yaml` via `Resource::Helper::loadFile`.
|
||||
// Si manca, és invàlid o té la seqüència buida, llança — no té sentit
|
||||
// continuar sense progressió de sales.
|
||||
void loadIndex();
|
||||
|
||||
std::vector<Entry> entries_;
|
||||
int index_{0};
|
||||
};
|
||||
|
||||
} // namespace Levels
|
||||
@@ -75,9 +75,24 @@ namespace Map {
|
||||
playfield_w = board_w + (2 * outer_corridor);
|
||||
playfield_h = board_h + (2 * outer_corridor);
|
||||
|
||||
// Porta i passadís de sortida. `door_col` és la columna central
|
||||
// transitable; el passadís són exactament els 16 px d'eixa columna
|
||||
// dins del strip del mur trasser. Si manca la clau, `door_col=0`
|
||||
// i el passadís queda a x=0 amb mida zero — pràcticament inactiu
|
||||
// (el bloqueig del clamp continua sent el rectangle del playfield).
|
||||
door_col = g.contains("door_col") ? g["door_col"].get_value<int>() : 0;
|
||||
door_corridor_x = (door_col * tile_size) + tilemap_origin_x;
|
||||
door_corridor_y = hud_h;
|
||||
door_corridor_w = tile_size;
|
||||
door_corridor_h = back_wall_h;
|
||||
// Cada càrrega de nivell deixa la porta tancada; l'entitat Door
|
||||
// canviarà el flag a true quan transiti a OPEN.
|
||||
door_open = false;
|
||||
|
||||
std::cout << "Map: geometria " << grid_cols << "x" << grid_rows
|
||||
<< " sarcs " << sarc_w << "x" << sarc_h
|
||||
<< " pitch " << pitch_x << "x" << pitch_y
|
||||
<< " door_col " << door_col
|
||||
<< " des de " << yaml_path << '\n';
|
||||
}
|
||||
|
||||
@@ -99,6 +114,18 @@ namespace Map {
|
||||
if (overlapStrict(base_box, getSarcRect(col, row))) { return true; }
|
||||
}
|
||||
}
|
||||
// Porta tancada: el passadís de sortida actua com a paret. Quan la
|
||||
// porta passa a OPEN, l'entitat Door posa `door_open = true` i
|
||||
// aquesta clàusula deixa de bloquejar — el jugador pot creuar.
|
||||
if (!door_open && door_corridor_w > 0 && door_corridor_h > 0) {
|
||||
const SDL_FRect CORRIDOR{
|
||||
.x = static_cast<float>(door_corridor_x),
|
||||
.y = static_cast<float>(door_corridor_y),
|
||||
.w = static_cast<float>(door_corridor_w),
|
||||
.h = static_cast<float>(door_corridor_h),
|
||||
};
|
||||
if (overlapStrict(base_box, CORRIDOR)) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -136,6 +163,10 @@ namespace Map {
|
||||
.tile_size = tile_size,
|
||||
.tilemap_origin_x = tilemap_origin_x,
|
||||
.tilemap_origin_y = tilemap_origin_y,
|
||||
.door_corridor_x = door_corridor_x,
|
||||
.door_corridor_y = door_corridor_y,
|
||||
.door_corridor_w = door_corridor_w,
|
||||
.door_corridor_h = door_corridor_h,
|
||||
.collides = &collides,
|
||||
.inside_playfield = &insidePlayfield,
|
||||
};
|
||||
|
||||
@@ -62,6 +62,24 @@ namespace Map {
|
||||
inline int playfield_w = 0;
|
||||
inline int playfield_h = 0;
|
||||
|
||||
// Porta de sortida del nivell. La porta sempre viu al mur trasser
|
||||
// (`wall_north`); només la columna varia per nivell. `door_col` és la
|
||||
// columna central transitable: el sprite 48×32 es dibuixa centrat
|
||||
// sobre aquesta columna i els 16 px centrals (just `door_col`) són
|
||||
// l'únic forat del mur trasser per on el jugador surt. Derivat de
|
||||
// `geometry.door_col` al YAML.
|
||||
inline int door_col = 0;
|
||||
inline int door_corridor_x = 0;
|
||||
inline int door_corridor_y = 0;
|
||||
inline int door_corridor_w = 0;
|
||||
inline int door_corridor_h = 0;
|
||||
// Estat del passadís: si la porta està oberta, el `door_corridor` no
|
||||
// bloqueja el moviment del jugador (vegeu `Map::collides`). Qui muta
|
||||
// aquest flag és l'entitat `Door` quan transita CLOSED → OPEN; el
|
||||
// reset el porta `Map::loadFrom` (cada càrrega de nivell el torna a
|
||||
// posar a false).
|
||||
inline bool door_open = false;
|
||||
|
||||
// Carrega el bloc `geometry:` del YAML i recalcula tots els derivats.
|
||||
// Ha de cridar-se abans d'instanciar `Player` o consultar `collides()`:
|
||||
// GameScene ho fa al ctor.
|
||||
@@ -104,6 +122,13 @@ namespace Map {
|
||||
int tile_size{0};
|
||||
int tilemap_origin_x{0};
|
||||
int tilemap_origin_y{0};
|
||||
// Passadís de sortida (forat del mur trasser a la columna de la
|
||||
// porta). El Player l'usa des del clamp per permetre pujar al
|
||||
// strip del mur quan la base box queda dins del rang X.
|
||||
int door_corridor_x{0};
|
||||
int door_corridor_y{0};
|
||||
int door_corridor_w{0};
|
||||
int door_corridor_h{0};
|
||||
bool (*collides)(const SDL_FRect&){nullptr};
|
||||
bool (*inside_playfield)(const SDL_FRect&){nullptr};
|
||||
};
|
||||
|
||||
@@ -82,6 +82,13 @@ auto PaintLayer::isPainted(int col, int row) const -> bool {
|
||||
return painted_[static_cast<std::size_t>(row)][static_cast<std::size_t>(col)] >= 0;
|
||||
}
|
||||
|
||||
void PaintLayer::clear() {
|
||||
// Mateixa geometria que al ctor: matriu de tiles a -1 (no pintat) i
|
||||
// buffer ARGB a zero (alpha=0 = transparent → render no escriu res).
|
||||
painted_.assign(static_cast<std::size_t>(rows_), std::vector<int>(static_cast<std::size_t>(cols_), -1));
|
||||
std::ranges::fill(pixels_, 0U);
|
||||
}
|
||||
|
||||
auto PaintLayer::tryPaint(int col, int row, int tile_index, int player_id) -> bool {
|
||||
if (col < 0 || col >= cols_ || row < 0 || row >= rows_) { return false; }
|
||||
if (tile_index < 0) { return false; }
|
||||
|
||||
@@ -48,6 +48,13 @@ class PaintLayer {
|
||||
|
||||
[[nodiscard]] auto isPainted(int col, int row) const -> bool;
|
||||
|
||||
// Buida el pintat acumulat. Marca tots els tiles com a no pintats i
|
||||
// reseteja el buffer ARGB a alpha=0 (transparent). Cridat per
|
||||
// GameScene a la transició ROOM_OUTRO → ROOM_INTRO. La subscripció
|
||||
// al bus es manté — el PaintLayer continuarà reaccionant a
|
||||
// `on_tile_entered` un cop els jugadors hagin reaparegut.
|
||||
void clear();
|
||||
|
||||
private:
|
||||
void blitPaintTile(int col, int row, int tile_index);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
@@ -27,10 +28,13 @@
|
||||
#include "core/ui/ui_config.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/concrete/dust.hpp"
|
||||
#include "game/entities/concrete/whip_attack.hpp"
|
||||
#include "game/entities/entity.hpp"
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/entity_loader.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
#include "game/entities/player.hpp"
|
||||
#include "game/levels/level_manager.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "game/ui/hud.hpp"
|
||||
@@ -112,6 +116,12 @@ namespace {
|
||||
// surt projectat sense control i els dos es travessen. Coincideix
|
||||
// amb la regla d'`Player::collidesWithWorld`.
|
||||
if (a->isHurt() || b->isHurt()) { return; }
|
||||
// EXITED/DISABLED: jugadors fora del joc, no participen al resolver.
|
||||
// Si hi entrés un, la seua presència física frenaria l'altre (per
|
||||
// overlap predictiu) tot i que `applyMovement` és no-op. Sortir
|
||||
// d'aquí garanteix que el jugador actiu pot creuar la porta o
|
||||
// moure's lliurement sense interferència d'un slot inactiu.
|
||||
if (a->isOutOfPlay() || b->isOutOfPlay()) { return; }
|
||||
|
||||
// Aire ↔ tierra: el saltador es travessa net del caminant. Sense
|
||||
// resolver, sense head-on, sense push. Els dos continuen amb la
|
||||
@@ -214,8 +224,11 @@ namespace {
|
||||
} // namespace
|
||||
|
||||
auto GameScene::defaultConfig() -> Config {
|
||||
// El path del mapa el dicta el LevelManager (que ja ha llegit `levels.yaml`
|
||||
// a la primera invocació). Així `defaultConfig()` no coneix cap fitxer
|
||||
// concret de sala — només cap a on apunta la progressió ara mateix.
|
||||
return {
|
||||
.map_yaml = Defaults::Pyramid::MAP_YAML,
|
||||
.map_yaml = Levels::LevelManager::get().currentPath(),
|
||||
.player_sprite = Defaults::Pyramid::PLAYER_SPRITE,
|
||||
.player_yaml = Defaults::Pyramid::PLAYER_YAML,
|
||||
.player2_sprite = Defaults::Pyramid::PLAYER_SPRITE_P2,
|
||||
@@ -228,6 +241,12 @@ auto GameScene::defaultConfig() -> Config {
|
||||
GameScene::GameScene(Config config)
|
||||
: config_(std::move(config)),
|
||||
tilemap_(makeTilemap(config_.map_yaml)) {
|
||||
// El text d'INTRO i el HUD demanaran piràmide/sala directament al
|
||||
// LevelManager — `GameScene` ja no manté un mirror local. La
|
||||
// sincronització "una vegada per càrrega de sala" la fa `loadRoom`
|
||||
// (per al HUD) i `renderRoomText` (per al text d'INTRO, llegint el
|
||||
// valor a cada frame de la transició).
|
||||
|
||||
// Convenció: el Resource::Cache indexa per basename del fitxer, perquè
|
||||
// la mateixa clau l'usa el parseador d'animació (YAML > tileSetFile).
|
||||
// Lambda perquè tenim dos conjunts d'assets (sam/morcus) i l'alta al
|
||||
@@ -243,19 +262,22 @@ GameScene::GameScene(Config config)
|
||||
|
||||
const bool USE_P1 = (config_.starting_players == DebugConfig::StartingPlayers::P1 || config_.starting_players == DebugConfig::StartingPlayers::BOTH);
|
||||
const bool USE_P2 = (config_.starting_players == DebugConfig::StartingPlayers::P2 || config_.starting_players == DebugConfig::StartingPlayers::BOTH);
|
||||
players_starts_active_ = {USE_P1, USE_P2};
|
||||
|
||||
std::string anim_p1;
|
||||
std::string anim_p2;
|
||||
if (USE_P1) { anim_p1 = load_player_assets(config_.player_sprite, config_.player_yaml); }
|
||||
if (USE_P2) { anim_p2 = load_player_assets(config_.player2_sprite, config_.player2_yaml); }
|
||||
// Carreguem assets per als DOS jugadors sempre, encara que algun no
|
||||
// arranque actiu: l'slot existeix igualment al vector com a DISABLED
|
||||
// i podria reactivar-se amb un futur "join in progress". El cost de
|
||||
// tenir el sprite cachejat sense usar-se és negligible (cap render).
|
||||
const std::string ANIM_P1 = load_player_assets(config_.player_sprite, config_.player_yaml);
|
||||
const std::string ANIM_P2 = load_player_assets(config_.player2_sprite, config_.player2_yaml);
|
||||
|
||||
// Geometria de col·lisió del jugador (caixa 16×16, base 12): carregada
|
||||
// del mateix YAML que l'animació. Ha d'estar feta abans d'instanciar
|
||||
// Player perquè el spawn consulta `collision_*`. Els membres de Player
|
||||
// són `static inline`, compartits per tots els jugadors: cridem
|
||||
// `loadFrom` amb el YAML del primer jugador actiu (P1 o P2). Els dos
|
||||
// personatges usen la mateixa geometria (morcus és un repaletat de sam).
|
||||
Player::loadFrom(USE_P1 ? config_.player_yaml : config_.player2_yaml);
|
||||
// són `static inline`, compartits per tots els jugadors; carregar des
|
||||
// del YAML de P1 és suficient (els dos personatges comparteixen
|
||||
// geometria — morcus és un repaletat de sam).
|
||||
Player::loadFrom(config_.player_yaml);
|
||||
|
||||
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
||||
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
||||
@@ -263,11 +285,13 @@ GameScene::GameScene(Config config)
|
||||
// Les surfaces de les entitats es carreguen al Resource::Cache aquí.
|
||||
entity_manager_.loadFromLevel(config_.map_yaml);
|
||||
|
||||
// Subscripcions bàsiques al bus del nivell: de moment només logs, sense
|
||||
// tocar ni la vida del jugador ni l'estat de la escena. Serà la base
|
||||
// sobre la qual cablejar HUD i fi de nivell més endavant.
|
||||
entity_manager_.getBus().subscribe("level_complete", [](const Event& ev) {
|
||||
// Subscripcions bàsiques al bus del nivell. `level_complete` també
|
||||
// dispara la transició a ROOM_OUTRO si encara estem a PLAYING; les
|
||||
// re-emissions es filtren per estat per no re-entrar mai des d'una
|
||||
// transició ja en marxa.
|
||||
entity_manager_.getBus().subscribe("level_complete", [this](const Event& ev) {
|
||||
std::cout << "[LEVEL COMPLETE] player_id=" << ev.player_id << '\n';
|
||||
if (state_ == RoomFsm::State::PLAYING) { setState(RoomFsm::State::ROOM_OUTRO); }
|
||||
});
|
||||
entity_manager_.getBus().subscribe("player_hit", [](const Event& ev) {
|
||||
const auto* dmg = std::get_if<int>(&ev.payload);
|
||||
@@ -290,6 +314,18 @@ GameScene::GameScene(Config config)
|
||||
if (pos != nullptr) { spawnDust(pos->x, pos->y, true); }
|
||||
});
|
||||
|
||||
// Whip: precarreguem les 4 surfaces (una per cardinal) abans del
|
||||
// primer spawn perquè el Player no facte disc al primer atac.
|
||||
// Subscriu `whip_spawn` per crear el `WhipAttack` corresponent.
|
||||
Resource::Cache::get().loadSurface("whip_east.gif", "data/entities/whip_attack/whip_east.gif");
|
||||
Resource::Cache::get().loadSurface("whip_west.gif", "data/entities/whip_attack/whip_west.gif");
|
||||
Resource::Cache::get().loadSurface("whip_north.gif", "data/entities/whip_attack/whip_north.gif");
|
||||
Resource::Cache::get().loadSurface("whip_south.gif", "data/entities/whip_attack/whip_south.gif");
|
||||
entity_manager_.getBus().subscribe("whip_spawn", [this](const Event& ev) {
|
||||
const auto* p = std::get_if<WhipSpawnPayload>(&ev.payload);
|
||||
if (p != nullptr) { spawnWhip(*p); }
|
||||
});
|
||||
|
||||
// Controls per jugador: P1 amb fletxes, P2 amb WASD. Cada Player
|
||||
// escolta les seues pròpies tecles dins del seu `update`, no hi ha
|
||||
// estat global d'input.
|
||||
@@ -299,6 +335,7 @@ GameScene::GameScene(Config config)
|
||||
.left = Defaults::KeysGame::LEFT,
|
||||
.right = Defaults::KeysGame::RIGHT,
|
||||
.jump = Defaults::KeysGame::JUMP,
|
||||
.attack = Defaults::KeysGame::ATTACK,
|
||||
};
|
||||
const Player::Controls CONTROLS_P2{
|
||||
.up = Defaults::KeysGameP2::UP,
|
||||
@@ -306,24 +343,14 @@ GameScene::GameScene(Config config)
|
||||
.left = Defaults::KeysGameP2::LEFT,
|
||||
.right = Defaults::KeysGameP2::RIGHT,
|
||||
.jump = Defaults::KeysGameP2::JUMP,
|
||||
.attack = Defaults::KeysGameP2::ATTACK,
|
||||
};
|
||||
// Spawn centrat al passadís exterior superior: la caixa de col·lisió
|
||||
// queda dins del passadís vertical del mig (entre columnes 1 i 2) i
|
||||
// centrada en Y al passadís exterior superior. Així no naix dins de
|
||||
// cap sarcòfag.
|
||||
constexpr auto CORRIDOR_CENTER_X = static_cast<float>(GameCanvas::CENTER_X);
|
||||
const auto CORRIDOR_CENTER_Y =
|
||||
static_cast<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F);
|
||||
const auto SPAWN_X = CORRIDOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F);
|
||||
const auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F);
|
||||
|
||||
// En mode BOTH separem els jugadors un tile a banda i banda del
|
||||
// centre perquè no naixen superposats. En mode single-player
|
||||
// (P1 o P2 aïllats per a proves de dev) l'únic jugador arrenca al
|
||||
// centre exacte.
|
||||
const bool BOTH = (USE_P1 && USE_P2);
|
||||
const auto P1_SPAWN_X = BOTH ? SPAWN_X - static_cast<float>(Map::tile_size) : SPAWN_X;
|
||||
const auto P2_SPAWN_X = BOTH ? SPAWN_X + static_cast<float>(Map::tile_size) : SPAWN_X;
|
||||
// Spawn lligat a la porta: tots els jugadors apareixen davant del
|
||||
// passadís de sortida. `computeSpawn` deriva les coordenades de
|
||||
// `Map::door_col` perquè canviar la columna de la porta al YAML
|
||||
// moga automàticament l'spawn — no hi ha cap valor hardcoded.
|
||||
const auto SPAWN = computeSpawn();
|
||||
const bool BOTH_ACTIVE = (USE_P1 && USE_P2);
|
||||
|
||||
// Índex de tile amb què pinta cada jugador — llegit del YAML del mapa
|
||||
// (camps opcionals `player1_paint_tile` / `player2_paint_tile`). Si no
|
||||
@@ -333,14 +360,30 @@ GameScene::GameScene(Config config)
|
||||
// puntuació (veure `Event::player_id`).
|
||||
constexpr int ID_P1 = 0;
|
||||
constexpr int ID_P2 = 1;
|
||||
if (USE_P1) {
|
||||
const int P1_PAINT = readOptionalInt(config_.map_yaml, "player1_paint_tile", -1);
|
||||
players_.emplace_back(std::make_unique<Player>(anim_p1, ID_P1, P1_SPAWN_X, SPAWN_Y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
||||
}
|
||||
if (USE_P2) {
|
||||
const int P2_PAINT = readOptionalInt(config_.map_yaml, "player2_paint_tile", -1);
|
||||
players_.emplace_back(std::make_unique<Player>(anim_p2, ID_P2, P2_SPAWN_X, SPAWN_Y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
|
||||
const int P1_PAINT = readOptionalInt(config_.map_yaml, "player1_paint_tile", -1);
|
||||
const int P2_PAINT = readOptionalInt(config_.map_yaml, "player2_paint_tile", -1);
|
||||
|
||||
// Sempre instanciem els dos slots. Mode single-player: l'únic actiu
|
||||
// queda centrat sota la porta (= SPAWN.p1_x + tile_size); l'inactiu
|
||||
// ocupa la posició BOTH per simetria, però com es queda DISABLED tot
|
||||
// el cicle no es nota. Així `players_[0]` sempre és P1 i `players_[1]`
|
||||
// sempre és P2, independentment de quins juguen.
|
||||
const auto TILE_F = static_cast<float>(Map::tile_size);
|
||||
float p1_x = SPAWN.p1_x;
|
||||
float p2_x = SPAWN.p2_x;
|
||||
if (!BOTH_ACTIVE) {
|
||||
if (USE_P1) { p1_x = SPAWN.p1_x + TILE_F; }
|
||||
if (USE_P2) { p2_x = SPAWN.p2_x - TILE_F; }
|
||||
}
|
||||
players_.emplace_back(std::make_unique<Player>(ANIM_P1, ID_P1, p1_x, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
||||
players_.emplace_back(std::make_unique<Player>(ANIM_P2, ID_P2, p2_x, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
|
||||
|
||||
// Tots els slots arrenquen DISABLED. Els actius els reactivarà
|
||||
// `updateRoomTransition` quan s'esgote el delay corresponent dins
|
||||
// de ROOM_INTRO (`player_spawn_delay_ms` per al primer, i
|
||||
// `player2_spawn_delay_ms` per al segon en mode BOTH). Els inactius
|
||||
// es queden DISABLED tot el cicle de sala.
|
||||
for (auto& p : players_) { p->disable(); }
|
||||
|
||||
// PaintLayer: font única del pintat del nivell. Es crea després de
|
||||
// `loadFromLevel` perquè l'EntityManager ja ha de tenir els sarcòfags
|
||||
@@ -364,6 +407,13 @@ GameScene::GameScene(Config config)
|
||||
players_view_.reserve(players_.size());
|
||||
std::ranges::transform(players_, std::back_inserter(players_view_), [](const auto& p) { return p.get(); });
|
||||
for (auto& p : players_) { p->setOtherPlayers(&players_view_); }
|
||||
|
||||
// Comptador d'EXITED a EntityManager: publica `level_complete` quan
|
||||
// tots els jugadors *actius* han travessat la porta. Els slots DISABLED
|
||||
// permanents (single-player) NO compten — d'altra manera la sala mai
|
||||
// es completaria, perquè un slot DISABLED no pot eixir per la porta.
|
||||
const int ACTIVE_COUNT = std::ranges::count(players_starts_active_, true);
|
||||
entity_manager_.setExpectedPlayerCount(ACTIVE_COUNT);
|
||||
// El snap també empeny lateralment si l'altre jugador ocupa el carril:
|
||||
// passem el mateix factor perquè vaja al mateix ritme que el resolver.
|
||||
for (auto& p : players_) { p->setPushFactor(push_factor_); }
|
||||
@@ -381,12 +431,20 @@ GameScene::GameScene(Config config)
|
||||
// només P1). Mentre els events del bus no porten `player_id`, els hits
|
||||
// segueixen aplicant-se a P1 (veure TODO a Hud::onPlayerHit).
|
||||
if (USE_P2) { hud_->activatePlayer(2); }
|
||||
// Sincronització inicial dels comptadors de piràmide/sala del HUD amb
|
||||
// el LevelManager (després de cada `loadRoom` també es refresca; ací
|
||||
// és el primer ajust per a la sala d'arrencada).
|
||||
{
|
||||
const auto& mgr = Levels::LevelManager::get();
|
||||
hud_->setPyramidRoom(mgr.currentPyramid(), mgr.currentRoom());
|
||||
}
|
||||
|
||||
// Música de fons en bucle infinit. El track ja està al Resource::Cache
|
||||
// (preload al Director), així que playMusic només mou el punter cap a
|
||||
// jail_audio i obri l'stream. Audio::get() està garantit no-null:
|
||||
// Director::init crida Audio::init abans d'instanciar GameScene.
|
||||
Audio::get()->playMusic(config_.music_track, -1);
|
||||
// La música de fons no s'arrenca ací: la sala comença a ROOM_INTRO,
|
||||
// que és silenciós per disseny. La música la posa en marxa
|
||||
// `setState(PLAYING)` quan la cinemàtica acaba (i la torna a aturar
|
||||
// `enterRoomIntro` quan entrem a una nova sala). El track ja està
|
||||
// al Resource::Cache (preload al Director), per tant la primera
|
||||
// crida a playMusic és immediata.
|
||||
|
||||
// Font de l'overlay de pausa: reutilitzem la Surface ja cachejada al
|
||||
// Director (PLACEHOLDER_NAME + ".gif"). Text només parseja el .fnt a
|
||||
@@ -395,10 +453,422 @@ GameScene::GameScene(Config config)
|
||||
auto pause_surface = Resource::Cache::get().getSurface(PAUSE_FONT_KEY);
|
||||
pause_font_ = std::make_shared<Text>(pause_surface, UiConfig::get().placeholderFont().fnt);
|
||||
pause_font_->setDefaultKerning(UiConfig::get().placeholderFont().kerning);
|
||||
|
||||
// Font del rètol de transició de sala. Mateixa convenció que la pausa
|
||||
// (Surface preloaded pel Director, només construïm el `Text` ací).
|
||||
const std::string ROOM_TEXT_FONT_KEY = UiConfig::get().roomTextFont().name + ".gif";
|
||||
auto room_text_surface = Resource::Cache::get().getSurface(ROOM_TEXT_FONT_KEY);
|
||||
room_text_font_ = std::make_shared<Text>(room_text_surface, UiConfig::get().roomTextFont().fnt);
|
||||
room_text_font_->setDefaultKerning(UiConfig::get().roomTextFont().kerning);
|
||||
|
||||
// Timings i corbes de la FSM de sala. Llegits del YAML una sola
|
||||
// vegada; canvis requereixen reiniciar el joc.
|
||||
transition_config_ = RoomFsm::loadConfig(Defaults::GameScene::CONFIG_YAML);
|
||||
|
||||
// Preload del so del rètol d'INTRO (no hi ha manifest de sons; el
|
||||
// pack agafa tot `data/` automàticament i ací registrem la clau
|
||||
// al Resource::Cache perquè Audio::playSound la trobe). Si el camp
|
||||
// del YAML està buit, no carreguem res — el dispar consultarà el
|
||||
// cache i caurà al silenci sense crash.
|
||||
if (!transition_config_.intro_text_sound.empty()) {
|
||||
Resource::Cache::get().loadSound(
|
||||
transition_config_.intro_text_sound,
|
||||
"data/sounds/" + transition_config_.intro_text_sound);
|
||||
}
|
||||
|
||||
// Log d'estat inicial: la FSM arrenca a ROOM_INTRO. En aquesta fita
|
||||
// les transicions són instantànies (sense fades ni text) i el primer
|
||||
// tick d'`updateRoomTransition` ja saltarà a PLAYING.
|
||||
std::cout << "[ROOM] state=ROOM_INTRO (initial)" << '\n';
|
||||
}
|
||||
|
||||
GameScene::~GameScene() = default;
|
||||
|
||||
auto GameScene::computeSpawn() -> PlayerSpawn {
|
||||
// Centre de la columna del passadís en píxels canvas. La caixa de
|
||||
// col·lisió del jugador (12 px) cap dins del passadís (16 px) amb 2
|
||||
// px de marge a banda i banda; alinear el sprite al centre de la
|
||||
// columna garanteix que la base del jugador i la del passadís
|
||||
// coincideixin verticalment.
|
||||
const float DOOR_CENTER_X = static_cast<float>((Map::door_col * Map::tile_size) + Map::tilemap_origin_x) + (static_cast<float>(Map::tile_size) / 2.0F);
|
||||
const float CENTER_TL_X = DOOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F);
|
||||
|
||||
// Y: centre del corredor exterior superior (mateix valor que abans;
|
||||
// el corredor és simètric respecte a `playfield_y`).
|
||||
const float CORRIDOR_CENTER_Y = static_cast<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F);
|
||||
const float SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F);
|
||||
|
||||
// Mode BOTH: P1 a -tile_size, P2 a +tile_size respecte al centre.
|
||||
// Els callsites individuals decideixen si usar aquests valors o si
|
||||
// estem en mode single-player (un sol jugador → centrat exactament).
|
||||
return {
|
||||
.p1_x = CENTER_TL_X - static_cast<float>(Map::tile_size),
|
||||
.p2_x = CENTER_TL_X + static_cast<float>(Map::tile_size),
|
||||
.y = SPAWN_Y,
|
||||
};
|
||||
}
|
||||
|
||||
void GameScene::loadRoom(const std::string& yaml_path) {
|
||||
// Recarregar les entitats des del YAML donat: la porta torna a
|
||||
// CLOSED (amb l'open=false a Map al loadFrom), els sarcs tornen a
|
||||
// CLOSED amb visited buit, momies i altres actors a la posició inicial.
|
||||
config_.map_yaml = yaml_path;
|
||||
Map::loadFrom(yaml_path);
|
||||
entity_manager_.reload(yaml_path);
|
||||
// Tornem a fixar el comptador de "esperats" amb només els slots
|
||||
// actius — un slot DISABLED permanent (single-player) no compta.
|
||||
const int ACTIVE_COUNT = std::ranges::count(players_starts_active_, true);
|
||||
entity_manager_.setExpectedPlayerCount(ACTIVE_COUNT);
|
||||
if (paint_layer_) { paint_layer_->clear(); }
|
||||
|
||||
// Tornem tots els slots a DISABLED. Els actius els reactivarà el
|
||||
// trigger d'spawn aeri dins de `updateRoomTransition` quan torni a
|
||||
// disparar-se el delay del nou cicle d'INTRO. Així cada cicle de
|
||||
// sala repeteix la cinemàtica de caiguda de la mateixa manera que
|
||||
// el primer.
|
||||
for (auto& p : players_) { p->disable(); }
|
||||
|
||||
// Refresca el marcador (piràmide / sala) amb el LevelManager. El
|
||||
// GameScene no manté els valors a part: la lectura sempre és a
|
||||
// través del singleton, així no es desincronitzen mai entre
|
||||
// hotkeys de salt, debug.yaml i creuament natural de porta.
|
||||
if (hud_) {
|
||||
const auto& mgr = Levels::LevelManager::get();
|
||||
hud_->setPyramidRoom(mgr.currentPyramid(), mgr.currentRoom());
|
||||
}
|
||||
|
||||
std::cout << "[ROOM] load " << yaml_path << ": entities reloaded, paint cleared, "
|
||||
<< ACTIVE_COUNT << " active player(s) disabled (awaiting spawn cinematic)\n";
|
||||
}
|
||||
|
||||
void GameScene::advanceToNextRoom() {
|
||||
auto& mgr = Levels::LevelManager::get();
|
||||
if (!mgr.advance()) {
|
||||
// Política temporal mentre no hi haja pantalla de "Fi de joc":
|
||||
// arribar al final del levels.yaml fa loop a la primera sala.
|
||||
std::cout << "[ROOM] final del levels.yaml — bucle a la primera sala\n";
|
||||
mgr.restart();
|
||||
}
|
||||
loadRoom(mgr.currentPath());
|
||||
enterRoomIntro();
|
||||
}
|
||||
|
||||
void GameScene::enterRoomIntro() {
|
||||
// Anotem la transició al log si veníem d'un altre estat; si ja
|
||||
// estàvem a INTRO (cas típic d'un salt debug) també queda ben clar.
|
||||
if (state_ != RoomFsm::State::ROOM_INTRO) {
|
||||
std::cout << "[ROOM] " << RoomFsm::stateName(state_) << " -> ROOM_INTRO\n";
|
||||
} else {
|
||||
std::cout << "[ROOM] ROOM_INTRO restart (cinemàtica reiniciada)\n";
|
||||
}
|
||||
state_ = RoomFsm::State::ROOM_INTRO;
|
||||
transition_ = {};
|
||||
room_intro_elapsed_ms_ = 0;
|
||||
players_spawned_in_intro_.fill(false);
|
||||
intro_text_sound_played_ = false;
|
||||
// La cinemàtica d'entrada és silenciosa per disseny: és INTRO qui
|
||||
// posseeix l'aturada. L'OUTRO no toca la música — així el cicle
|
||||
// OUTRO → INTRO transmet la sensació de "tall" en entrar a la sala
|
||||
// nova, no de "fade-out" anterior. Si ja no sona res, stopMusic és
|
||||
// no-op.
|
||||
if (Audio::get() != nullptr) { Audio::get()->stopMusic(); }
|
||||
}
|
||||
|
||||
void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
||||
if (slot_index >= players_spawned_in_intro_.size()) { return; }
|
||||
if (players_spawned_in_intro_[slot_index]) { return; }
|
||||
players_spawned_in_intro_[slot_index] = true;
|
||||
|
||||
// Recalcula spawn per si door_col canvia entre sales (avui no, però
|
||||
// la lògica ja queda preparada per a l'itinerari).
|
||||
const auto SPAWN = computeSpawn();
|
||||
const bool BOTH_ACTIVE = (players_starts_active_[0] && players_starts_active_[1]);
|
||||
// Mode single-player: l'únic actiu queda centrat sota la porta. Els
|
||||
// valors de SPAWN.p1_x/p2_x estan fets per al cas BOTH (offset ±tile);
|
||||
// amb un sol actiu, el reposicionem al centre exacte.
|
||||
const float SINGLE_X = SPAWN.p1_x + static_cast<float>(Map::tile_size);
|
||||
|
||||
float x = SINGLE_X;
|
||||
if (BOTH_ACTIVE) { x = (slot_index == 0) ? SPAWN.p1_x : SPAWN.p2_x; }
|
||||
players_[slot_index]->spawnFromAir(x, SPAWN.y);
|
||||
// Arma el reverb del primer LAND com a part de la cinemàtica
|
||||
// d'INTRO. Una sola vegada per spawn aeri: el respawn post-mort
|
||||
// (que no passa per ací) cau al camí sec. Si el preset està buit
|
||||
// al YAML, l'arma queda buida i el LAND és sec igualment.
|
||||
players_[slot_index]->armNextLandReverb(transition_config_.intro_land_reverb_preset);
|
||||
|
||||
std::cout << "[ROOM] spawn cinematic P" << (slot_index + 1) << ": airborne entry\n";
|
||||
}
|
||||
|
||||
void GameScene::setState(RoomFsm::State new_state) {
|
||||
if (new_state == state_) { return; }
|
||||
std::cout << "[ROOM] " << RoomFsm::stateName(state_) << " -> " << RoomFsm::stateName(new_state) << '\n';
|
||||
state_ = new_state;
|
||||
transition_ = {};
|
||||
// En entrar a un nou cicle d'INTRO, resetegem el timer i el flag de
|
||||
// l'spawn aeri perquè es torne a disparar al delay configurat.
|
||||
if (new_state == RoomFsm::State::ROOM_INTRO) {
|
||||
room_intro_elapsed_ms_ = 0;
|
||||
players_spawned_in_intro_.fill(false);
|
||||
intro_text_sound_played_ = false;
|
||||
}
|
||||
// En sortir d'INTRO cap a PLAYING, la música arrenca: és el moment
|
||||
// en què la cinemàtica d'entrada acaba i el jugador agafa control.
|
||||
// L'OUTRO no atura la música; ho fa el següent `enterRoomIntro`.
|
||||
if (new_state == RoomFsm::State::PLAYING && Audio::get() != nullptr) {
|
||||
Audio::get()->playMusic(config_.music_track, -1);
|
||||
}
|
||||
// L'avanç de sala (OUTRO → INTRO) NO viu ací: el dispara
|
||||
// `updateRoomTransition` quan la FSM detecta el final de l'OUTRO,
|
||||
// cridant directament `advanceToNextRoom`. Això manté `setState`
|
||||
// com una transició pura, sense efectes col·laterals que
|
||||
// recarreguen sales — necessari per als salts debug que també
|
||||
// entren a INTRO sense passar per OUTRO.
|
||||
}
|
||||
|
||||
void GameScene::updateRoomTransition(int delta_ms) {
|
||||
// Disparador d'spawn aeri: timer propi (independent de les fases de
|
||||
// la FSM) que arrenca a l'entrada de ROOM_INTRO. La separació deixa
|
||||
// el delay desacoblat del fade-in i, demà, reutilitzable per al
|
||||
// respawn post-mort sense que la FSM de sala hi haja d'intervenir.
|
||||
if (state_ == RoomFsm::State::ROOM_INTRO) {
|
||||
room_intro_elapsed_ms_ += delta_ms;
|
||||
// Cada slot actiu pot caure en un moment distint quan els dos
|
||||
// estan actius (mode BOTH): P1 amb player_spawn_delay_ms i P2
|
||||
// amb player2_spawn_delay_ms. En single-player (P1 o P2 sols)
|
||||
// l'slot actiu fa servir player_spawn_delay_ms — el segon valor
|
||||
// queda inactiu, com ha demanat el disseny.
|
||||
const bool BOTH_ACTIVE = players_starts_active_[0] && players_starts_active_[1];
|
||||
for (std::size_t i = 0; i < players_starts_active_.size(); ++i) {
|
||||
if (!players_starts_active_[i]) { continue; }
|
||||
if (players_spawned_in_intro_[i]) { continue; }
|
||||
const int DELAY = (i == 1 && BOTH_ACTIVE)
|
||||
? transition_config_.player2_spawn_delay_ms
|
||||
: transition_config_.player_spawn_delay_ms;
|
||||
if (room_intro_elapsed_ms_ >= DELAY) {
|
||||
triggerPlayerSpawn(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto NEXT = RoomFsm::advance(state_, transition_, transition_config_, delta_ms);
|
||||
|
||||
// Disparador del so del rètol: la FSM acaba d'avançar; si estem a
|
||||
// INTRO i ja hem deixat enrere phase 0 (fade_in), reproduïm el so
|
||||
// una sola vegada. El so es dispara al final del fade-in, anticipant
|
||||
// el text que apareix a phase 2 (text_enter): així sembla que el so
|
||||
// "anuncia" l'arribada del rètol enlloc de coincidir amb la seva
|
||||
// entrada visual.
|
||||
if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase >= 1 && !intro_text_sound_played_ && !transition_config_.intro_text_sound.empty() && Audio::get() != nullptr) {
|
||||
Audio::get()->playSound(transition_config_.intro_text_sound, Audio::Group::GAME);
|
||||
intro_text_sound_played_ = true;
|
||||
}
|
||||
if (NEXT != state_) {
|
||||
// El final de l'OUTRO no es resol amb un setState normal: cal
|
||||
// demanar al LevelManager el següent nivell i carregar-lo abans
|
||||
// d'entrar a INTRO. `advanceToNextRoom` fa tot el bloc i acaba
|
||||
// amb la FSM a ROOM_INTRO (via `enterRoomIntro`), així que no
|
||||
// fa falta el setState d'aquesta branca.
|
||||
if (NEXT == RoomFsm::State::ROOM_INTRO && state_ == RoomFsm::State::ROOM_OUTRO) {
|
||||
advanceToNextRoom();
|
||||
} else {
|
||||
setState(NEXT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::renderRoomFade() {
|
||||
// Fade negre sobre el món, pintat un cop els actors ja estan al
|
||||
// canvas i abans del HUD. Tres casos:
|
||||
// ROOM_INTRO/fase 0: 255 → 0 (entrada de sala, món emergent)
|
||||
// ROOM_OUTRO/fase 3: 0 → 255 (sortida de sala, món s'apaga)
|
||||
// ROOM_OUTRO/fase 4: alpha=255 fix (pausa en negre opac abans del
|
||||
// rellançament de la pròxima sala)
|
||||
if (state_ == RoomFsm::State::ROOM_OUTRO && transition_.phase == 4) {
|
||||
Screen::get().blendRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, 0x00000000U, 255);
|
||||
return;
|
||||
}
|
||||
|
||||
int dur = 0;
|
||||
float t = 0.0F;
|
||||
bool fade_in = false;
|
||||
if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase == 0) {
|
||||
dur = transition_config_.intro_fade_in_ms;
|
||||
fade_in = true;
|
||||
} else if (state_ == RoomFsm::State::ROOM_OUTRO && transition_.phase == 3) {
|
||||
dur = transition_config_.outro_fade_out_ms;
|
||||
fade_in = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
t = (dur > 0)
|
||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur), 0.0F, 1.0F)
|
||||
: 1.0F;
|
||||
const float ALPHA_F = fade_in ? (1.0F - t) : t;
|
||||
const auto ALPHA = static_cast<std::uint8_t>(255.0F * ALPHA_F);
|
||||
if (ALPHA == 0) { return; }
|
||||
Screen::get().blendRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, 0x00000000U, ALPHA);
|
||||
}
|
||||
|
||||
void GameScene::renderRoomText() {
|
||||
// Orquestrador del rètol de transició de sala. La feina pesada
|
||||
// (animació de X, animació d'altura del panel i pintat) la fan
|
||||
// quatre helpers privats; aquesta funció només decideix quin
|
||||
// text dibuixar i amb quines fases segons l'estat.
|
||||
//
|
||||
// Mapatge per estat (només els estats amb text actiu hi entren):
|
||||
// ROOM_INTRO: phases 2..4 → enter / hold / exit; dues línies
|
||||
// (piràmide damunt, sala davall) que travessen la
|
||||
// pantalla en direccions oposades.
|
||||
// ROOM_OUTRO: phases 0..2 → enter / hold / exit; dues línies
|
||||
// oposades ("fase" / "completada"). Phase 3 és el
|
||||
// fade-out i 4 la pausa en negre — cap text en cap
|
||||
// de les dues.
|
||||
if (!room_text_font_) { return; }
|
||||
|
||||
constexpr int KERNING = 0;
|
||||
// Centre vertical de la zona de joc (canvas sense la HUD). Reemplaça
|
||||
// l'antic GameCanvas::CENTER_Y per descomptar la franja del marcador
|
||||
// i tenir el rètol equilibrat sobre el playfield.
|
||||
const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2;
|
||||
// Colors del rètol via UiConfig (`data/ui.yaml > colors.room_text` i
|
||||
// `room_text_outline`). Independents dels colors de notificació.
|
||||
const Uint8 FILL = UiConfig::get().roomText();
|
||||
const Uint8 OUTLINE = UiConfig::get().roomTextOutline();
|
||||
const int LINE_HEIGHT = room_text_font_->getLineHeight();
|
||||
|
||||
if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase >= 2 && transition_.phase <= 4) {
|
||||
const int DUR_ENTER = transition_config_.intro_text_enter_ms;
|
||||
const int DUR_EXIT = transition_config_.intro_text_exit_ms;
|
||||
// Panel primer (queda sota el text gràcies a l'ordre de blits).
|
||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
||||
computeRoomTextPanelHeight(2, 3, 4, DUR_ENTER, DUR_EXIT));
|
||||
|
||||
const auto& mgr = Levels::LevelManager::get();
|
||||
const std::string LINE_PYRAMID = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid());
|
||||
const std::string LINE_ROOM = RoomFsm::formatIntroRoomLine(mgr.currentRoom());
|
||||
// Direccions oposades: piràmide entra per l'esquerra i surt per
|
||||
// la dreta; sala fa el contrari.
|
||||
const int X_PYRAMID = computeRoomTextX(room_text_font_->length(LINE_PYRAMID, KERNING),
|
||||
2,
|
||||
4,
|
||||
DUR_ENTER,
|
||||
DUR_EXIT,
|
||||
/*from_right=*/false);
|
||||
const int X_ROOM = computeRoomTextX(room_text_font_->length(LINE_ROOM, KERNING),
|
||||
2,
|
||||
4,
|
||||
DUR_ENTER,
|
||||
DUR_EXIT,
|
||||
/*from_right=*/true);
|
||||
// Bloc de dues línies centrat al voltant del PLAY_CENTER_Y. El
|
||||
// gap (`intro_text_line_gap_px`) es reparteix simètricament:
|
||||
// mig amunt a la piràmide, mig avall a la sala.
|
||||
const int GAP = transition_config_.intro_text_line_gap_px;
|
||||
drawRoomTextLine(LINE_PYRAMID, X_PYRAMID, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
||||
drawRoomTextLine(LINE_ROOM, X_ROOM, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
||||
} else if (state_ == RoomFsm::State::ROOM_OUTRO && transition_.phase >= 0 && transition_.phase <= 2) {
|
||||
const int DUR_ENTER = transition_config_.outro_text_enter_ms;
|
||||
const int DUR_EXIT = transition_config_.outro_text_exit_ms;
|
||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
||||
computeRoomTextPanelHeight(0, 1, 2, DUR_ENTER, DUR_EXIT));
|
||||
|
||||
const std::string LINE_1 = Locale::get().text("game.room_complete_line1");
|
||||
const std::string LINE_2 = Locale::get().text("game.room_complete_line2");
|
||||
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
||||
0,
|
||||
2,
|
||||
DUR_ENTER,
|
||||
DUR_EXIT,
|
||||
/*from_right=*/false);
|
||||
const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING),
|
||||
0,
|
||||
2,
|
||||
DUR_ENTER,
|
||||
DUR_EXIT,
|
||||
/*from_right=*/true);
|
||||
const int GAP = transition_config_.outro_text_line_gap_px;
|
||||
drawRoomTextLine(LINE_1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
||||
drawRoomTextLine(LINE_2, X_2, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
||||
}
|
||||
}
|
||||
|
||||
auto GameScene::computeRoomTextX(int width, int phase_enter, int phase_exit, int dur_enter, int dur_exit, bool from_right) const -> int {
|
||||
// X animada d'una línia: entra des de fora de pantalla, s'atura al
|
||||
// CENTER_X durant el hold (fora del rang d'aquesta funció — el caller
|
||||
// garanteix que cau a la branca return final) i surt pel costat
|
||||
// oposat. `from_right=false` és el patró clàssic (esquerra → dreta);
|
||||
// `true` el flipa.
|
||||
const int OFF_LEFT = -width;
|
||||
const int OFF_RIGHT = GameCanvas::WIDTH;
|
||||
const int OFF_ENTER = from_right ? OFF_RIGHT : OFF_LEFT;
|
||||
const int OFF_EXIT = from_right ? OFF_LEFT : OFF_RIGHT;
|
||||
if (transition_.phase == phase_enter) {
|
||||
const float T = (dur_enter > 0)
|
||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_enter), 0.0F, 1.0F)
|
||||
: 1.0F;
|
||||
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
||||
return Easing::lerpInt(OFF_ENTER, GameCanvas::CENTER_X, CURVE(T));
|
||||
}
|
||||
if (transition_.phase == phase_exit) {
|
||||
const float T = (dur_exit > 0)
|
||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_exit), 0.0F, 1.0F)
|
||||
: 1.0F;
|
||||
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
||||
return Easing::lerpInt(GameCanvas::CENTER_X, OFF_EXIT, CURVE(T));
|
||||
}
|
||||
return GameCanvas::CENTER_X;
|
||||
}
|
||||
|
||||
auto GameScene::computeRoomTextPanelHeight(int phase_enter, int phase_hold, int phase_exit, int dur_enter, int dur_exit) const -> int {
|
||||
// Altura animada del panel translúcid sota el rètol. Creix de 0 a
|
||||
// `text_panel_height_px` amb `text_enter_curve` durant l'enter, es
|
||||
// manté plena durant el hold i decreix amb `text_exit_curve`
|
||||
// durant l'exit. Fora d'aquestes tres fases retorna 0.
|
||||
const int MAX = transition_config_.text_panel_height_px;
|
||||
if (transition_.phase == phase_enter) {
|
||||
const float T = (dur_enter > 0)
|
||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_enter), 0.0F, 1.0F)
|
||||
: 1.0F;
|
||||
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
||||
return Easing::lerpInt(0, MAX, CURVE(T));
|
||||
}
|
||||
if (transition_.phase == phase_hold) { return MAX; }
|
||||
if (transition_.phase == phase_exit) {
|
||||
const float T = (dur_exit > 0)
|
||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_exit), 0.0F, 1.0F)
|
||||
: 1.0F;
|
||||
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
||||
return Easing::lerpInt(MAX, 0, CURVE(T));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void GameScene::drawRoomTextPanel(int center_y, int panel_height) const {
|
||||
// No-op si l'altura és nul·la o l'alpha està a zero — així evitem
|
||||
// un blendRect inútil i deixem el caller cridar incondicionalment.
|
||||
if (panel_height <= 0 || transition_config_.text_panel_alpha == 0) { return; }
|
||||
Screen::get().blendRect(0,
|
||||
center_y - (panel_height / 2),
|
||||
GameCanvas::WIDTH,
|
||||
panel_height,
|
||||
transition_config_.text_panel_color,
|
||||
transition_config_.text_panel_alpha);
|
||||
}
|
||||
|
||||
void GameScene::drawRoomTextLine(const std::string& text, int x, int y, std::uint8_t fill_color, std::uint8_t outline_color) const {
|
||||
constexpr int KERNING = 0;
|
||||
constexpr Uint8 STROKE_DIST = 1;
|
||||
room_text_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE4_FLAG,
|
||||
x,
|
||||
y,
|
||||
text,
|
||||
KERNING,
|
||||
fill_color,
|
||||
STROKE_DIST,
|
||||
outline_color);
|
||||
}
|
||||
|
||||
void GameScene::spawnDust(float feet_x_center, float feet_y, bool is_landing) {
|
||||
// Mida del sprite de Dust llegida del seu YAML. Hardcoded per
|
||||
// simplificar — si canvia, només cal tocar dust.yaml + aquesta
|
||||
@@ -423,6 +893,26 @@ void GameScene::spawnDust(float feet_x_center, float feet_y, bool is_landing) {
|
||||
entity_manager_.add(std::move(dust));
|
||||
}
|
||||
|
||||
void GameScene::spawnWhip(const WhipSpawnPayload& payload) {
|
||||
// El YAML ja viu cacheat al `EntityLoader`; les 4 surfaces les hem
|
||||
// precarregat al ctor. `configure()` selecciona la mida segons
|
||||
// direcció i posa pixel_rect; aquí resolem la surface via
|
||||
// `Resource::Cache` (mateix patró que `spawnDust`) per evitar que
|
||||
// l'entitat depengui directament del cache.
|
||||
auto whip_yaml = EntityLoader::loadTemplate("whip_attack");
|
||||
auto whip = std::make_unique<WhipAttack>(whip_yaml);
|
||||
whip->setOwner(payload.owner);
|
||||
whip->configure(
|
||||
static_cast<WhipAttack::Direction>(payload.direction),
|
||||
payload.owner_box,
|
||||
payload.range_px);
|
||||
whip->setSurface(Resource::Cache::get().getSurface(whip->getSurfaceKey()));
|
||||
// Donem accés a Hazards i jugadors per a la fase de dany. Sense això
|
||||
// el whip queda merament visual.
|
||||
whip->setWorldContext(&entity_manager_, &players_view_);
|
||||
entity_manager_.add(std::move(whip));
|
||||
}
|
||||
|
||||
void GameScene::handlePauseInput() {
|
||||
#ifdef _DEBUG
|
||||
// Mutex amb l'editor: si està actiu, ignorem la tecla de pausa.
|
||||
@@ -509,6 +999,28 @@ namespace {
|
||||
return ev.type == SDL_EVENT_KEY_DOWN && ev.key.scancode == SC;
|
||||
});
|
||||
}
|
||||
|
||||
// Retorna -1 si s'ha premut prev_room, +1 si next_room, 0 si cap.
|
||||
// Si tots dos arriben en el mateix frame (poc probable amb teclat),
|
||||
// prev té prioritat — l'usuari sempre pot tornar a prémer next.
|
||||
auto detectRoomJump() -> int {
|
||||
const SDL_Scancode PREV_SC = KeyConfig::get().scancode("prev_room");
|
||||
const SDL_Scancode NEXT_SC = KeyConfig::get().scancode("next_room");
|
||||
for (const auto& ev : EventBuffer::events) {
|
||||
if (ev.type != SDL_EVENT_KEY_DOWN) { continue; }
|
||||
if (PREV_SC != SDL_SCANCODE_UNKNOWN && ev.key.scancode == PREV_SC) { return -1; }
|
||||
if (NEXT_SC != SDL_SCANCODE_UNKNOWN && ev.key.scancode == NEXT_SC) { return +1; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto detectOpenDoor() -> bool {
|
||||
const SDL_Scancode SC = KeyConfig::get().scancode("open_door");
|
||||
if (SC == SDL_SCANCODE_UNKNOWN) { return false; }
|
||||
return std::ranges::any_of(EventBuffer::events, [SC](const SDL_Event& ev) {
|
||||
return ev.type == SDL_EVENT_KEY_DOWN && ev.key.scancode == SC;
|
||||
});
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
@@ -516,6 +1028,11 @@ void GameScene::update(int delta_ms) {
|
||||
handlePauseInput();
|
||||
updatePauseTransition(delta_ms);
|
||||
|
||||
// Transició de sala: viu en temps real (no escalada per la pausa)
|
||||
// perquè és UI. Avança avans del bloc de món perquè quan PLAYING
|
||||
// arrenque ja sigui efectiu en el mateix frame.
|
||||
updateRoomTransition(delta_ms);
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Toggle de l'editor: només si no hi ha cap transició de pausa ni
|
||||
// pausa consolidada. L'editor mateix no consumeix la tecla — la
|
||||
@@ -523,6 +1040,28 @@ void GameScene::update(int delta_ms) {
|
||||
if (pause_.state == PauseState::IDLE && detectEditorToggle()) {
|
||||
editor_.toggle(tilemap_);
|
||||
}
|
||||
|
||||
// Salts debug entre sales (`[` / `]`). Mútuament excloents amb la
|
||||
// pausa i amb l'editor: si qualsevol està actiu, ignorem la tecla
|
||||
// perquè el salt recarrega entitats i invalidaria l'estat editat.
|
||||
if (pause_.state == PauseState::IDLE && !editor_.active()) {
|
||||
const int DELTA = detectRoomJump();
|
||||
if (DELTA != 0) {
|
||||
auto& mgr = Levels::LevelManager::get();
|
||||
mgr.setIndex(mgr.index() + DELTA);
|
||||
loadRoom(mgr.currentPath());
|
||||
enterRoomIntro();
|
||||
}
|
||||
}
|
||||
|
||||
// Força l'obertura de la porta (Keypad 1). Útil per saltar el
|
||||
// pintat/sarcs durant proves de progressió. La resta del flux
|
||||
// (creuar → player_exited → level_complete → OUTRO) es manté.
|
||||
// Mateixos gates que el salt entre sales.
|
||||
if (pause_.state == PauseState::IDLE && !editor_.active() && detectOpenDoor()) {
|
||||
std::cout << "[DEBUG] força obertura de la porta\n";
|
||||
entity_manager_.forceOpenDoor();
|
||||
}
|
||||
#endif
|
||||
|
||||
GlobalInputs::update(delta_ms);
|
||||
@@ -552,6 +1091,11 @@ void GameScene::update(int delta_ms) {
|
||||
// pusher faça `tryMove`, el pushed ja estiga fora de la
|
||||
// seua caixa. La callback també tracta l'altre jugador com
|
||||
// a sòlid: xarxa de seguretat davant de qualsevol inexactitud.
|
||||
// Cap gate per estat de sala: els jugadors fora del joc (DISABLED
|
||||
// durant l'INTRO abans del trigger d'spawn, o EXITED post-porta)
|
||||
// s'auto-salten a `tick`/`applyMovement`/`render`. Així la
|
||||
// cinemàtica de spawn aeri pot transcorrer mentre la fase d'INTRO
|
||||
// encara es mou.
|
||||
for (auto& player : players_) { player->tick(DT); }
|
||||
resolvePlayerVsPlayer(players_view_, DT, push_factor_);
|
||||
for (auto* player : orderedForMovement(players_view_)) { player->applyMovement(DT); }
|
||||
@@ -562,9 +1106,10 @@ void GameScene::update(int delta_ms) {
|
||||
// aplicar la transició OPENING → OPEN en el mateix frame.
|
||||
entity_manager_.update(DT);
|
||||
|
||||
// Detecció de contacte player↔entitat després que totes les entitats
|
||||
// hagin mogut aquest frame. Reutilitzem la vista plana que ja
|
||||
// manté el GameScene per al resolver player↔player.
|
||||
// Detecció de contacte player↔entitat. `checkPlayerCollisions` ja
|
||||
// filtra jugadors fora del joc (`isOutOfPlay`), així que durant
|
||||
// l'INTRO previ a l'spawn no es disparen contactes amb les
|
||||
// entitats que ja s'animen al fons.
|
||||
entity_manager_.checkPlayerCollisions(players_view_);
|
||||
}
|
||||
|
||||
@@ -608,7 +1153,9 @@ void GameScene::render() {
|
||||
// pintat (al damunt) però per darrere de TOTS els actors —
|
||||
// entitats i jugadors, inclòs el saltador mateix— així la sombra
|
||||
// queda al sòl sense tapar res. `renderShadow` és no-op si el
|
||||
// jugador està a terra, així que no cal filtrar al callsite.
|
||||
// jugador no està a l'aire (i, com `render()`, també no-op si
|
||||
// `isOutOfPlay`). Cap gate per estat de sala: durant la
|
||||
// cinemàtica d'spawn aeri ja volem que la sombra es vegi.
|
||||
for (const auto& p : players_) { p->renderShadow(); }
|
||||
// Z-order interleaving entitats ↔ players per `getFeetY()` ascendent:
|
||||
// els actors amb els peus més amunt queden dibuixats primer (al fons),
|
||||
@@ -625,6 +1172,10 @@ void GameScene::render() {
|
||||
std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) {
|
||||
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
||||
});
|
||||
// Tots els jugadors entren al z-sort. Els fora de joc fan no-op a
|
||||
// `render()`; els actius (inclosos AIRBORNE durant l'spawn aeri)
|
||||
// es pinten amb el seu feet_y normal. La sombra que afecta la
|
||||
// sombra del salt s'ha pintat ja a la passada anterior.
|
||||
std::ranges::transform(players_, std::back_inserter(order), [](const auto& p) {
|
||||
return Renderable{.feet_y = p->getFeetY(), .entity = nullptr, .player = p.get()};
|
||||
});
|
||||
@@ -638,11 +1189,21 @@ void GameScene::render() {
|
||||
r.player->render();
|
||||
}
|
||||
}
|
||||
// HUD: a sobre dels actors perquè la franja superior tape qualsevol
|
||||
// sprite que (durant l'animació d'entrada) puga sortir més amunt
|
||||
// del playfield. S'amaga en mode editor (l'editor edita el tilemap;
|
||||
// el HUD distrauria) però segueix actualitzant-se per fora.
|
||||
|
||||
// Fade del món: pintat ENTRE actors i HUD perquè el marcador, les
|
||||
// notificacions i el service menu queden per damunt sense afectar-se.
|
||||
// Inactiu si l'estat és PLAYING.
|
||||
renderRoomFade();
|
||||
|
||||
// HUD: a sobre dels actors (i del fade) perquè la franja superior
|
||||
// tape qualsevol sprite que (durant l'animació d'entrada) puga
|
||||
// sortir més amunt del playfield. S'amaga en mode editor però
|
||||
// segueix actualitzant-se per fora.
|
||||
if (hud_) { hud_->render(); }
|
||||
|
||||
// Text de la transició de sala (entrada o sortida): per damunt del
|
||||
// HUD perquè és UI de transició i ha de ser sempre llegible.
|
||||
renderRoomText();
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -11,6 +12,7 @@
|
||||
#include "game/entities/entity_manager.hpp"
|
||||
#include "game/map/paint_layer.hpp"
|
||||
#include "game/map/tilemap.hpp"
|
||||
#include "game/scenes/room_fsm.hpp"
|
||||
|
||||
#ifdef _DEBUG
|
||||
#include "game/editor/editor.hpp"
|
||||
@@ -45,6 +47,17 @@ class GameScene : public Scene {
|
||||
void render() override;
|
||||
|
||||
private:
|
||||
// Punt centralitzat per canviar d'estat de sala. Imprimeix la
|
||||
// transició i reinicialitza els acumuladors de la `RoomFsm::Transition`.
|
||||
// Cap estat s'ha de canviar fora d'aquí.
|
||||
void setState(RoomFsm::State new_state);
|
||||
|
||||
// Avança la transició activa via `RoomFsm::advance`. S'executa cada
|
||||
// frame amb delta real (no escalat per la pausa) perquè la transició
|
||||
// viu en temps de UI. Si l'estat retornat és diferent de l'actual,
|
||||
// crida `setState` (que reseteja la `Transition`).
|
||||
void updateRoomTransition(int delta_ms);
|
||||
|
||||
// Consumeix events d'aquest frame (F11 del KeyConfig "pause" o botó
|
||||
// START del gamepad primari) per disparar les transicions de pausa.
|
||||
// No alterna un flag binari: inicia (o inverteix mid-flight) un
|
||||
@@ -62,6 +75,12 @@ class GameScene : public Scene {
|
||||
// `is_landing` selecciona la variant de l'animació al `Dust`.
|
||||
void spawnDust(float feet_x_center, float feet_y, bool is_landing);
|
||||
|
||||
// Spawna un `WhipAttack` a partir del payload publicat al bus quan
|
||||
// un Player entra a `ATTACKING`. Llig la plantilla cacheada del YAML
|
||||
// i delega la mida + surface al `WhipAttack::configure()` segons
|
||||
// direcció.
|
||||
void spawnWhip(const struct WhipSpawnPayload& payload);
|
||||
|
||||
Config config_;
|
||||
Tilemap tilemap_;
|
||||
|
||||
@@ -72,8 +91,43 @@ class GameScene : public Scene {
|
||||
|
||||
// Vector des del principi: AEE suportarà multijugador local; cada Player
|
||||
// té els seus propis Controls i entra al vector amb una crida extra.
|
||||
// Sempre conté **dos** entrades (P1 a [0], P2 a [1]) tant en mode BOTH
|
||||
// com en single-player: l'slot inactiu es queda en estat DISABLED de
|
||||
// manera permanent, deixant el camí preparat per al "join in progress"
|
||||
// futur (el segon jugador podrà entrar a meitat de partida sense
|
||||
// recrear estructures). Veure `players_starts_active_`.
|
||||
std::vector<std::unique_ptr<Player>> players_;
|
||||
|
||||
// Quins slots juguen aquesta partida segons `config_.starting_players`.
|
||||
// Index = id del jugador (0 = P1, 1 = P2). Determina a qui aplica el
|
||||
// spawn aeri al final de la fase 0 d'INTRO; un slot amb `false` es
|
||||
// queda DISABLED tot el cicle de sala.
|
||||
std::array<bool, 2> players_starts_active_{false, false};
|
||||
|
||||
// Mil·lisegons des de l'entrada en ROOM_INTRO, per disparar la
|
||||
// cinemàtica de spawn aeri quan s'arriba al delay corresponent al
|
||||
// slot. Es reseteja a `setState(ROOM_INTRO)`. Cada slot té el seu
|
||||
// propi flag de "ja he caigut" — així podem caure en moments
|
||||
// distints en mode BOTH (`player_spawn_delay_ms` per a P1 i
|
||||
// `player2_spawn_delay_ms` per a P2).
|
||||
int room_intro_elapsed_ms_{0};
|
||||
std::array<bool, 2> players_spawned_in_intro_{false, false};
|
||||
|
||||
// One-shot del so del rètol d'INTRO (`text_sound` a game.yaml).
|
||||
// Es disparen quan la FSM passa a phase 2 (text_enter); el flag
|
||||
// evita repeticions si phase 2 s'estén multi-frame. Es reseteja a
|
||||
// `enterRoomIntro` / `setState(ROOM_INTRO)`. Si la clau del so és
|
||||
// buida o el cache no l'ha carregat, queda inutilitzat.
|
||||
bool intro_text_sound_played_{false};
|
||||
|
||||
// Cinemàtica de spawn d'un slot concret. Posa el slot indicat en
|
||||
// AIRBORNE des d'una altura fora de pantalla. Cridat per
|
||||
// `updateRoomTransition` quan `room_intro_elapsed_ms_` supera el
|
||||
// delay específic de l'slot. Idempotent (consulta el flag corresponent
|
||||
// a `players_spawned_in_intro_`). També és el punt al qual es
|
||||
// connectarà el respawn post-mort en el futur.
|
||||
void triggerPlayerSpawn(std::size_t slot_index);
|
||||
|
||||
// Vista plana de punters crus — la fa servir Player com a llista
|
||||
// d'obstacles mòbils a la callback de col·lisió. Es construeix un sol
|
||||
// cop al ctor (els punters són estables perquè `players_` no es
|
||||
@@ -114,6 +168,13 @@ class GameScene : public Scene {
|
||||
// abans — ja cachejada al Director.
|
||||
std::shared_ptr<Text> pause_font_;
|
||||
|
||||
// Font del rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO).
|
||||
// Distinta de `pause_font_` perquè el rètol de fase té estètica
|
||||
// pròpia (CaesarDressing per defecte, configurable via
|
||||
// `data/ui.yaml > fonts.room_text`). La pausa segueix amb la
|
||||
// placeholder.
|
||||
std::shared_ptr<Text> room_text_font_;
|
||||
|
||||
enum class PauseState : std::uint8_t {
|
||||
IDLE, // Joc corrent; cap transició en marxa. scale == 1.
|
||||
ENTERING, // Frenant cap a la pausa. scale decreix a 0.
|
||||
@@ -131,6 +192,72 @@ class GameScene : public Scene {
|
||||
UiConfig::CurveFn curve{nullptr}; // Corba activa.
|
||||
} pause_;
|
||||
|
||||
// Configuració de la FSM de sala (intro/outro). Carregada al ctor des
|
||||
// de `data/scenes/game.yaml`; valors per defecte si manca el YAML.
|
||||
RoomFsm::Config transition_config_{};
|
||||
|
||||
// Estat actual de la FSM de sala. S'inicialitza a ROOM_INTRO al ctor
|
||||
// perquè cada vegada que entres al joc passes per la presentació de
|
||||
// la sala. La transició a PLAYING l'orquestra `updateRoomTransition`.
|
||||
RoomFsm::State state_{RoomFsm::State::ROOM_INTRO};
|
||||
|
||||
// Sub-fase i temps acumulat dins de l'estat actual. `setState` el
|
||||
// reseteja a {0, 0}.
|
||||
RoomFsm::Transition transition_{};
|
||||
|
||||
// Píxels del fade negre i del text de la transició de sala.
|
||||
// Cridats des de `render()` entre el bloc d'actors i el HUD —
|
||||
// així marcador, notificacions i service menu queden per damunt.
|
||||
void renderRoomFade();
|
||||
void renderRoomText();
|
||||
|
||||
// Helpers de `renderRoomText`. Tots consulten `transition_` i
|
||||
// `transition_config_` directament a través de `this` — així les
|
||||
// signatures es queden netes i la funció pública s'orquestra amb
|
||||
// poques línies. Tres tasques separades:
|
||||
// - X animada d'una línia (entra per un costat, surt per l'altre).
|
||||
// - Altura animada del panel translúcid (creix, manté, decreix).
|
||||
// - Pintat efectiu del panel i d'una línia de text.
|
||||
[[nodiscard]] auto computeRoomTextX(int width, int phase_enter, int phase_exit, int dur_enter, int dur_exit, bool from_right) const -> int;
|
||||
[[nodiscard]] auto computeRoomTextPanelHeight(int phase_enter, int phase_hold, int phase_exit, int dur_enter, int dur_exit) const -> int;
|
||||
void drawRoomTextPanel(int center_y, int panel_height) const;
|
||||
void drawRoomTextLine(const std::string& text, int x, int y, std::uint8_t fill_color, std::uint8_t outline_color) const;
|
||||
|
||||
// Posició inicial dels jugadors derivada de `Map::door_col`. Tots
|
||||
// els jugadors comparteixen Y (centre del corredor exterior superior);
|
||||
// X depèn del nombre actiu: amb un jugador queda centrat sota la
|
||||
// porta; amb dos, separats ±tile_size respecte al centre. Estructura
|
||||
// exposada perquè els tests puguen verificar-la sense entrar al ctor
|
||||
// sencer.
|
||||
struct PlayerSpawn {
|
||||
float p1_x{0.0F};
|
||||
float p2_x{0.0F};
|
||||
float y{0.0F};
|
||||
};
|
||||
[[nodiscard]] static auto computeSpawn() -> PlayerSpawn;
|
||||
|
||||
// Recarrega la sala apuntada per `yaml_path`: refresca Map (geometria,
|
||||
// porta tancada), entitats (sarcs/porta/momies des de zero), neteja
|
||||
// el PaintLayer i deixa els jugadors a DISABLED esperant la cinemàtica
|
||||
// d'spawn aeri del nou cicle d'INTRO. També actualitza
|
||||
// `config_.map_yaml` perquè futurs consumidors (l'Editor en guardar,
|
||||
// logs, etc.) sàpiguen quina sala està viva.
|
||||
void loadRoom(const std::string& yaml_path);
|
||||
|
||||
// Demana al LevelManager el següent nivell i el carrega via
|
||||
// `loadRoom`, deixant la FSM al començament d'un nou cicle d'INTRO.
|
||||
// Si ja era a l'última entrada, fa loop a la primera (política
|
||||
// temporal mentre no hi haja pantalla de fi de joc). Cridat des de
|
||||
// `updateRoomTransition` quan l'OUTRO arriba a la fase final.
|
||||
void advanceToNextRoom();
|
||||
|
||||
// Inicia (o reinicia) el cicle d'entrada de sala: posa l'estat a
|
||||
// ROOM_INTRO i reseteja `transition_`, `room_intro_elapsed_ms_` i el
|
||||
// flag d'spawn aeri. Idempotent quan ja s'estava a INTRO (els
|
||||
// timers tornen a començar des de zero — útil per als salts debug
|
||||
// que recarreguen la sala estant ja en INTRO).
|
||||
void enterRoomIntro();
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Editor modal del tilemap. Mútuament excloent amb la pausa: si la
|
||||
// pausa no està IDLE la tecla de toggle de l'editor s'ignora, i
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#include "game/scenes/room_fsm.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Llegeix un sub-camp sencer d'un node YAML. Si manca, retorna el
|
||||
// fallback. Dissenyat per al consum puntual de `loadConfig`.
|
||||
auto readChildInt(const fkyaml::node& parent, const char* key, int fallback) -> int {
|
||||
if (!parent.contains(key)) { return fallback; }
|
||||
return parent[key].get_value<int>();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace RoomFsm {
|
||||
|
||||
auto loadConfig(const std::string& yaml_path) -> Config {
|
||||
Config cfg{};
|
||||
cfg.text_enter_curve = UiConfig::curveByName("outCubic");
|
||||
cfg.text_exit_curve = UiConfig::curveByName("inCubic");
|
||||
|
||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||
if (bytes.empty()) {
|
||||
std::cout << "RoomFsm: " << yaml_path << " absent — uso fallbacks compilats\n";
|
||||
return cfg;
|
||||
}
|
||||
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("room_intro")) {
|
||||
const auto& n = yaml["room_intro"];
|
||||
cfg.intro_fade_in_ms = readChildInt(n, "fade_in_ms", cfg.intro_fade_in_ms);
|
||||
cfg.intro_text_delay_ms = readChildInt(n, "text_delay_ms", cfg.intro_text_delay_ms);
|
||||
cfg.intro_text_enter_ms = readChildInt(n, "text_enter_ms", cfg.intro_text_enter_ms);
|
||||
cfg.intro_text_hold_ms = readChildInt(n, "text_hold_ms", cfg.intro_text_hold_ms);
|
||||
cfg.intro_text_exit_ms = readChildInt(n, "text_exit_ms", cfg.intro_text_exit_ms);
|
||||
cfg.player_spawn_delay_ms = readChildInt(n, "player_spawn_delay_ms", cfg.player_spawn_delay_ms);
|
||||
// Per defecte el segon jugador cau alhora que el primer.
|
||||
// El YAML pot trencar la simultaneïtat fixant un valor propi.
|
||||
cfg.player2_spawn_delay_ms = readChildInt(n, "player2_spawn_delay_ms", cfg.player_spawn_delay_ms);
|
||||
cfg.intro_text_line_gap_px = readChildInt(n, "line_gap_px", cfg.intro_text_line_gap_px);
|
||||
if (n.contains("land_reverb")) {
|
||||
cfg.intro_land_reverb_preset = n["land_reverb"].get_value<std::string>();
|
||||
}
|
||||
if (n.contains("text_sound")) {
|
||||
cfg.intro_text_sound = n["text_sound"].get_value<std::string>();
|
||||
}
|
||||
}
|
||||
if (yaml.contains("room_outro")) {
|
||||
const auto& n = yaml["room_outro"];
|
||||
cfg.outro_text_enter_ms = readChildInt(n, "text_enter_ms", cfg.outro_text_enter_ms);
|
||||
cfg.outro_text_hold_ms = readChildInt(n, "text_hold_ms", cfg.outro_text_hold_ms);
|
||||
cfg.outro_text_exit_ms = readChildInt(n, "text_exit_ms", cfg.outro_text_exit_ms);
|
||||
cfg.outro_fade_out_ms = readChildInt(n, "fade_out_ms", cfg.outro_fade_out_ms);
|
||||
cfg.outro_text_line_gap_px = readChildInt(n, "line_gap_px", cfg.outro_text_line_gap_px);
|
||||
cfg.outro_black_hold_ms = readChildInt(n, "black_hold_ms", cfg.outro_black_hold_ms);
|
||||
}
|
||||
if (yaml.contains("text")) {
|
||||
const auto& n = yaml["text"];
|
||||
if (n.contains("enter_curve")) {
|
||||
cfg.text_enter_curve = UiConfig::curveByName(n["enter_curve"].get_value<std::string>());
|
||||
}
|
||||
if (n.contains("exit_curve")) {
|
||||
cfg.text_exit_curve = UiConfig::curveByName(n["exit_curve"].get_value<std::string>());
|
||||
}
|
||||
}
|
||||
if (yaml.contains("text_panel")) {
|
||||
const auto& n = yaml["text_panel"];
|
||||
if (n.contains("color") && n["color"].is_sequence() && n["color"].size() == 3) {
|
||||
const auto R = n["color"][0].get_value<std::uint32_t>() & 0xFFU;
|
||||
const auto G = n["color"][1].get_value<std::uint32_t>() & 0xFFU;
|
||||
const auto B = n["color"][2].get_value<std::uint32_t>() & 0xFFU;
|
||||
cfg.text_panel_color = (R << 16) | (G << 8) | B;
|
||||
}
|
||||
if (n.contains("alpha")) {
|
||||
cfg.text_panel_alpha = static_cast<std::uint8_t>(n["alpha"].get_value<std::uint32_t>() & 0xFFU);
|
||||
}
|
||||
cfg.text_panel_height_px = readChildInt(n, "height_px", cfg.text_panel_height_px);
|
||||
}
|
||||
|
||||
std::cout << "RoomFsm: timings carregats des de " << yaml_path
|
||||
<< " (intro fade " << cfg.intro_fade_in_ms
|
||||
<< "ms, outro fade " << cfg.outro_fade_out_ms << "ms)\n";
|
||||
return cfg;
|
||||
}
|
||||
|
||||
auto advance(State current, Transition& t, const Config& cfg, int delta_ms) -> State {
|
||||
if (current == State::PLAYING) { return current; }
|
||||
|
||||
t.elapsed_ms += delta_ms;
|
||||
|
||||
if (current == State::ROOM_INTRO) {
|
||||
const std::array<int, 5> DURS = {
|
||||
cfg.intro_fade_in_ms,
|
||||
cfg.intro_text_delay_ms,
|
||||
cfg.intro_text_enter_ms,
|
||||
cfg.intro_text_hold_ms,
|
||||
cfg.intro_text_exit_ms,
|
||||
};
|
||||
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
||||
t.elapsed_ms -= DURS[t.phase];
|
||||
++t.phase;
|
||||
}
|
||||
if (t.phase >= static_cast<int>(DURS.size())) { return State::PLAYING; }
|
||||
return current;
|
||||
}
|
||||
|
||||
// ROOM_OUTRO
|
||||
const std::array<int, 5> DURS = {
|
||||
cfg.outro_text_enter_ms,
|
||||
cfg.outro_text_hold_ms,
|
||||
cfg.outro_text_exit_ms,
|
||||
cfg.outro_fade_out_ms,
|
||||
// Pausa final en negre, després del fade-out i abans del
|
||||
// canvi a INTRO. Si és 0 ms, el while-loop la consumeix
|
||||
// sense esperar — comportament idèntic a l'històric.
|
||||
cfg.outro_black_hold_ms,
|
||||
};
|
||||
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
||||
t.elapsed_ms -= DURS[t.phase];
|
||||
++t.phase;
|
||||
}
|
||||
// Final de l'OUTRO → ROOM_INTRO de la sala SEGÜENT. La FSM no
|
||||
// sap quina és — només avisa del canvi d'estat. La GameScene,
|
||||
// dins `setState(ROOM_INTRO)` amb PREV=OUTRO, demana al
|
||||
// LevelManager el següent nivell i recarrega tilemap/entitats.
|
||||
if (t.phase >= static_cast<int>(DURS.size())) { return State::ROOM_INTRO; }
|
||||
return current;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Substitueix la primera ocurrència de `placeholder` dins de `tpl`
|
||||
// pel valor d'`n` convertit a string. Utilitzat per les dues
|
||||
// formatadores d'INTRO; si la clau del Locale no porta el
|
||||
// placeholder (traducció buida o ja substituïda), retorna la
|
||||
// plantilla intacta.
|
||||
auto substitute(std::string tpl, std::string_view placeholder, int n) -> std::string {
|
||||
auto pos = tpl.find(placeholder);
|
||||
if (pos != std::string::npos) {
|
||||
tpl.replace(pos, placeholder.size(), std::to_string(n));
|
||||
}
|
||||
return tpl;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
auto formatIntroPyramidLine(int pyramid) -> std::string {
|
||||
return substitute(Locale::get().text("game.room_intro_pyramid"), "{p}", pyramid);
|
||||
}
|
||||
|
||||
auto formatIntroRoomLine(int room) -> std::string {
|
||||
return substitute(Locale::get().text("game.room_intro_room"), "{r}", room);
|
||||
}
|
||||
|
||||
auto stateName(State s) -> const char* {
|
||||
switch (s) {
|
||||
case State::ROOM_INTRO:
|
||||
return "ROOM_INTRO";
|
||||
case State::PLAYING:
|
||||
return "PLAYING";
|
||||
case State::ROOM_OUTRO:
|
||||
return "ROOM_OUTRO";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
} // namespace RoomFsm
|
||||
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "core/ui/ui_config.hpp"
|
||||
|
||||
// FSM de sala (intro/outro) extreta de GameScene per a testabilitat. La
|
||||
// lògica que viu aquí és pura: no toca SDL, ni Audio, ni cap singleton
|
||||
// excepte `Locale::get()` (per a la formatació del text). El consumidor
|
||||
// actual és `GameScene`, que cridarà `RoomFsm::advance` cada frame.
|
||||
namespace RoomFsm {
|
||||
|
||||
enum class State : std::uint8_t {
|
||||
ROOM_INTRO, // Entrada de sala: fade in + text d'entrada.
|
||||
PLAYING, // Joc normal.
|
||||
ROOM_OUTRO, // Sortida de sala: text de sortida + fade out.
|
||||
};
|
||||
|
||||
// Sub-fase i temps acumulat dins de l'estat actual. Es reseteja a
|
||||
// {0, 0} quan es canvia d'estat. Significat de `phase` per estat:
|
||||
// ROOM_INTRO: 0=fade_in, 1=text_delay, 2=text_enter, 3=text_hold, 4=text_exit
|
||||
// ROOM_OUTRO: 0=text_enter, 1=text_hold, 2=text_exit, 3=fade_out, 4=black_hold
|
||||
// PLAYING: no s'usa.
|
||||
struct Transition {
|
||||
int phase{0};
|
||||
int elapsed_ms{0};
|
||||
};
|
||||
|
||||
// Timings i corbes carregats de `data/scenes/game.yaml`. Els valors
|
||||
// per defecte coincideixen amb els del YAML versionat: si manca el
|
||||
// fitxer, la FSM funciona igualment amb aquests fallbacks.
|
||||
struct Config {
|
||||
int intro_fade_in_ms{500};
|
||||
int intro_text_delay_ms{500};
|
||||
int intro_text_enter_ms{500};
|
||||
int intro_text_hold_ms{1000};
|
||||
int intro_text_exit_ms{500};
|
||||
|
||||
int outro_text_enter_ms{500};
|
||||
int outro_text_hold_ms{1000};
|
||||
int outro_text_exit_ms{500};
|
||||
int outro_fade_out_ms{500};
|
||||
// Pausa en negre opac entre el final de l'OUTRO i el començament
|
||||
// de l'INTRO de la sala següent. 0 = sense pausa (transició
|
||||
// immediata, comportament històric). El YAML pot sobreescriure
|
||||
// sense recompilar — útil per donar respir abans del nou rètol.
|
||||
int outro_black_hold_ms{0};
|
||||
|
||||
// Mil·lisegons des de l'inici de ROOM_INTRO en què cau la
|
||||
// cinemàtica d'spawn aeri dels jugadors actius. Específic per a
|
||||
// l'entrada de sala — el respawn post-mort tindrà el seu propi
|
||||
// timing quan toqui dissenyar-lo. Un valor habitual: la suma de
|
||||
// fade_in + text_delay + text_enter + text_hold, perquè els
|
||||
// players caiguin just quan el rètol comença a sortir. El
|
||||
// fallback compilat (500 ms) deixa el spawn al final del fade-in
|
||||
// si el YAML no defineix res — només pensat com a degradació.
|
||||
int player_spawn_delay_ms{500};
|
||||
|
||||
// Delay independent per al segon jugador, **només quan els dos
|
||||
// slots estan actius** (mode BOTH). En single-player (P1 o P2
|
||||
// sols) s'usa sempre `player_spawn_delay_ms` per al jugador
|
||||
// actiu. Si el YAML no fixa aquest valor, el `loadConfig` el
|
||||
// copia de `player_spawn_delay_ms` perquè els dos players caiguin
|
||||
// alhora — comportament històric del joc.
|
||||
int player2_spawn_delay_ms{500};
|
||||
|
||||
// Píxels extra entre les dues línies del text d'INTRO (piràmide
|
||||
// damunt, sala davall). Es sumen al `lineHeight` natural del
|
||||
// font per separar visualment les dues línies. 0 = stack
|
||||
// immediat (com l'antic single-line). El YAML pot afinar-lo
|
||||
// sense recompilar.
|
||||
int intro_text_line_gap_px{8};
|
||||
|
||||
// Equivalent per a l'OUTRO ("fase" damunt, "completada" davall).
|
||||
// Es manté independent del de l'INTRO per si el ritme visual
|
||||
// demana un espaiat distint en cada banda de la transició.
|
||||
int outro_text_line_gap_px{8};
|
||||
|
||||
UiConfig::CurveFn text_enter_curve{nullptr};
|
||||
UiConfig::CurveFn text_exit_curve{nullptr};
|
||||
|
||||
// Preset de reverb (catàleg a `data/sounds.yaml`) que GameScene
|
||||
// arma al jugador al spawn aeri d'INTRO. Es consumeix al primer
|
||||
// aterratge cinemàtic; els salts posteriors del joc i els
|
||||
// respawns post-mort cauen al so sec. Buit = sense reverb (la
|
||||
// cinemàtica d'entrada sona igual que un salt qualsevol).
|
||||
std::string intro_land_reverb_preset;
|
||||
|
||||
// Clau d'un so puntual (data/sounds/<key>) que GameScene
|
||||
// dispara just quan la cinemàtica d'INTRO arriba a la fase
|
||||
// d'entrada del text (phase == 2). Buit o absent = sense so.
|
||||
// Convenció Resource::Cache: el basename val tant de clau com
|
||||
// de path (loadSound ho fa servir igual que el land_sound).
|
||||
std::string intro_text_sound;
|
||||
|
||||
// Panel translúcid sota el rètol de transició (INTRO i OUTRO).
|
||||
// Centrat verticalment al centre del playfield, ocupa tota
|
||||
// l'amplada del canvas. Creix d'altura 0 → height_px durant la
|
||||
// fase d'enter del text (amb `text_enter_curve`) i decreix
|
||||
// durant la fase d'exit (amb `text_exit_curve`). 0 d'alpha o
|
||||
// 0 d'alçada el deixa invisible. Color RGB (sense alpha — la
|
||||
// transparència viu al camp `alpha`).
|
||||
std::uint32_t text_panel_color{0x000000U};
|
||||
std::uint8_t text_panel_alpha{128};
|
||||
int text_panel_height_px{40};
|
||||
};
|
||||
|
||||
// Llegeix la configuració des del YAML. Si el fitxer manca o falten
|
||||
// claus, els camps no presents conserven els seus fallbacks. Les
|
||||
// corbes es resolen amb `UiConfig::curveByName` (mateix mecanisme
|
||||
// que la pausa); si el nom és desconegut cau a `Easing::linear`.
|
||||
[[nodiscard]] auto loadConfig(const std::string& yaml_path) -> Config;
|
||||
|
||||
// Avança la transició consumint `delta_ms` des del temps real.
|
||||
// Retorna l'estat resultant (potser el mateix que l'entrant si
|
||||
// encara queden sub-fases). Si retorna un estat diferent, el caller
|
||||
// ha de resetejar la `Transition` a {0, 0} abans del següent frame
|
||||
// (això ho fa `GameScene::setState`). Funció pura sobre l'argument.
|
||||
[[nodiscard]] auto advance(State current, Transition& t, const Config& cfg, int delta_ms) -> State;
|
||||
|
||||
// Substitueix `{p}` de la clau "game.room_intro_pyramid" del Locale
|
||||
// per l'enter passat. Pensat per a la primera línia del text d'INTRO.
|
||||
// Si la clau no està traduïda, retorna la pròpia clau (fallback de
|
||||
// Locale::text).
|
||||
[[nodiscard]] auto formatIntroPyramidLine(int pyramid) -> std::string;
|
||||
|
||||
// Equivalent per a `game.room_intro_room` amb el placeholder `{r}`.
|
||||
// Es renderitza com a segona línia del text d'INTRO.
|
||||
[[nodiscard]] auto formatIntroRoomLine(int room) -> std::string;
|
||||
|
||||
// Nom textual per a logs i tests. No es fa servir per a lògica.
|
||||
[[nodiscard]] auto stateName(State s) -> const char*;
|
||||
|
||||
} // namespace RoomFsm
|
||||
@@ -78,6 +78,7 @@ namespace Defaults {
|
||||
constexpr SDL_Scancode LEFT = SDL_SCANCODE_LEFT;
|
||||
constexpr SDL_Scancode RIGHT = SDL_SCANCODE_RIGHT;
|
||||
constexpr SDL_Scancode JUMP = SDL_SCANCODE_M;
|
||||
constexpr SDL_Scancode ATTACK = SDL_SCANCODE_N;
|
||||
constexpr SDL_Scancode EXIT = SDL_SCANCODE_ESCAPE;
|
||||
} // namespace KeysGame
|
||||
|
||||
@@ -91,6 +92,7 @@ namespace Defaults {
|
||||
constexpr SDL_Scancode LEFT = SDL_SCANCODE_A;
|
||||
constexpr SDL_Scancode RIGHT = SDL_SCANCODE_D;
|
||||
constexpr SDL_Scancode JUMP = SDL_SCANCODE_V;
|
||||
constexpr SDL_Scancode ATTACK = SDL_SCANCODE_C;
|
||||
} // namespace KeysGameP2
|
||||
|
||||
namespace Player {
|
||||
@@ -130,6 +132,15 @@ namespace Defaults {
|
||||
constexpr const char* PLACEHOLDER_FNT = "data/fonts/ari-w9500-condensed-display.fnt";
|
||||
constexpr int PLACEHOLDER_KERNING = 1;
|
||||
|
||||
// Text de transició de sala (ROOM_INTRO "Piràmide N / Sala M" i
|
||||
// ROOM_OUTRO "Fase completada"). Font temàtica diferenciada de la
|
||||
// del HUD i de la dels placeholders perquè el rètol de fase tinga
|
||||
// pes visual propi. La pausa segueix amb la PLACEHOLDER.
|
||||
constexpr const char* ROOM_TEXT_NAME = "CaesarDressing-Regular";
|
||||
constexpr const char* ROOM_TEXT_GIF = "data/fonts/CaesarDressing-Regular.gif";
|
||||
constexpr const char* ROOM_TEXT_FNT = "data/fonts/CaesarDressing-Regular.fnt";
|
||||
constexpr int ROOM_TEXT_KERNING = 1;
|
||||
|
||||
// Menú de servei (F9): títol en display normal, items en la
|
||||
// variant condensed-display (més compacta, perquè caben més
|
||||
// files sense encongir la caixa). Kerning 0 a totes dues perquè
|
||||
@@ -182,6 +193,13 @@ namespace Defaults {
|
||||
constexpr Uint8 HUD_VALUE = 187; // Valors (score, vides, tresors).
|
||||
constexpr Uint8 HUD_FLASH = 237; // Color temporal del valor en flash.
|
||||
|
||||
// Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO).
|
||||
// Independent dels colors de notificació perquè el rètol de
|
||||
// fase és un canal visual diferent. `ROOM_TEXT_OUTLINE` és el
|
||||
// contorn que abans estava hardcodejat al `renderRoomText`.
|
||||
constexpr Uint8 ROOM_TEXT = 232;
|
||||
constexpr Uint8 ROOM_TEXT_OUTLINE = 255;
|
||||
|
||||
// Timings d'animació del HUD. L'entrada són els tres blocs lliscant
|
||||
// a la seua posició; flash i shake acompanyen un `player_hit`.
|
||||
constexpr int HUD_ENTRY_MS = 400;
|
||||
@@ -206,8 +224,6 @@ namespace Defaults {
|
||||
namespace Editor {
|
||||
// Color del rectangle 1 px que envolta el canvas durant l'edició.
|
||||
constexpr std::uint32_t BORDER_ARGB = 0xFF2244FFU;
|
||||
// Ruta de persistència: writer surgical sobre la font del repo.
|
||||
constexpr const char* SAVE_PATH = "data/maps/pyramid.yaml";
|
||||
// Píxels entre el canvas i el rectangle del TilePicker per totes
|
||||
// les vores. Prou perquè el border blau de l'editor no es
|
||||
// superpose amb la graella del picker.
|
||||
@@ -220,11 +236,23 @@ namespace Defaults {
|
||||
} // namespace Editor
|
||||
#endif
|
||||
|
||||
// Configuració de l'escena de joc (timings i corbes de la FSM de sala).
|
||||
// Independent de la piràmide concreta — quan un dia hi haja diferents
|
||||
// ritmes per piràmide, aquest YAML pot adoptar variants per nivell.
|
||||
namespace GameScene {
|
||||
constexpr const char* CONFIG_YAML = "data/scenes/game.yaml";
|
||||
} // namespace GameScene
|
||||
|
||||
// Rutes d'assets per piràmide. Centralitzats ací perquè `GameScene` no
|
||||
// en contingui cap literal; afegir una piràmide nova serà un altre
|
||||
// conjunt de constants + un Config distint passat al ctor.
|
||||
namespace Pyramid {
|
||||
constexpr const char* MAP_YAML = "data/maps/pyramid.yaml";
|
||||
// Índex ordenat de les 30 sales (5 piràmides × 6 sales). El consumeix
|
||||
// `Levels::LevelManager` a la primera invocació; cada entrada apunta
|
||||
// a un `data/maps/p<N>/r<M>.yaml`. Cap altre punt del codi hauria de
|
||||
// hardcodejar el path d'una sala concreta — pregunteu-li al
|
||||
// `LevelManager` (`currentPath()`) i deixeu que ell decidisca.
|
||||
constexpr const char* LEVELS_YAML = "data/maps/levels.yaml";
|
||||
constexpr const char* PLAYER_SPRITE = "data/player/sam.gif";
|
||||
constexpr const char* PLAYER_YAML = "data/player/sam.yaml";
|
||||
// Personatge del segon jugador. Clonat de sam amb repaletat (roig→blau);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Texts {
|
||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||
constexpr const char* VERSION = "0.23.0"; // Single source of truth — Makefile el grep
|
||||
constexpr const char* VERSION = "0.29.0"; // Single source of truth — Makefile el grep
|
||||
} // namespace Texts
|
||||
|
||||
namespace GameCanvas {
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
// T4 sarcòfag — perímetre computat correctament; notifyTilePainted
|
||||
// passa a OPENING quan es visiten tots els tiles.
|
||||
// T5 manager — 2 sarcs + 1 door: forçant l'obertura dels sarcs, la porta
|
||||
// s'obri sola; contactar la porta OPEN emet `level_complete`.
|
||||
// s'obri sola. El contacte de la porta amb un jugador
|
||||
// nul·l és no-op segur (la porta ja no emet `level_complete`
|
||||
// directament; ho fa EntityManager comptant `player_exited`).
|
||||
// T6 animation — 3 frames @ 10 fps, `tick(0.15)` → frame índex == 1.
|
||||
// T7 plantilles — carrega els 7 YAML d'entitat sense excepcions.
|
||||
// T8 decoration — Torch no col·lisiona ni és interactable.
|
||||
@@ -20,6 +22,10 @@
|
||||
// sprite sencer (pixel_rect).
|
||||
// T13 mummy step — camina en línia recta pel playfield i gira en topar
|
||||
// un mur (insidePlayfield=false) o sarc (Map::collides).
|
||||
// T14 exited aggregator — EntityManager compta `player_exited` i emet
|
||||
// `level_complete` quan iguala `expected_players`. Verifica
|
||||
// idempotència (3r event no re-emet) i reset del comptador
|
||||
// via `setExpectedPlayerCount` (cicle següent torna a emetre).
|
||||
//
|
||||
// Execució (des de la ruta del repo):
|
||||
// make build && cd build && ctest --output-on-failure
|
||||
@@ -173,12 +179,6 @@ namespace {
|
||||
manager.add(std::move(door));
|
||||
manager.setDoor(door_ptr);
|
||||
|
||||
// Subscriptor local per capturar `level_complete`.
|
||||
bool level_complete_fired = false;
|
||||
manager.getBus().subscribe("level_complete", [&](const Event&) {
|
||||
level_complete_fired = true;
|
||||
});
|
||||
|
||||
// Forcem l'obertura dels dos sarcs visitant els seus perímetres.
|
||||
auto forceOpen = [](Sarcophagus* s) {
|
||||
const int COL0 = s->getTilePos().col;
|
||||
@@ -206,12 +206,16 @@ namespace {
|
||||
REQUIRE(manager.openedSarcophagusCount() == 2);
|
||||
REQUIRE(door_ptr->getState() == Door::State::OPEN);
|
||||
|
||||
// Contacte amb la porta oberta → level_complete.
|
||||
door_ptr->onPlayerContact(nullptr);
|
||||
REQUIRE(level_complete_fired);
|
||||
|
||||
// Un segon contacte NO torna a disparar (idempotent).
|
||||
level_complete_fired = false;
|
||||
// Contacte sense jugador (nullptr): no-op segur. La porta ja no
|
||||
// emet `level_complete` directament — ara és EntityManager qui
|
||||
// l'emet quan tots els jugadors han eixit. La cobertura
|
||||
// d'aquest agregat viu al test específic d'EntityManager
|
||||
// (test_game_fsm) perquè requereix instanciar Player, fora
|
||||
// d'abast d'aquest smoke purament d'entitats.
|
||||
bool level_complete_fired = false;
|
||||
manager.getBus().subscribe("level_complete", [&](const Event&) {
|
||||
level_complete_fired = true;
|
||||
});
|
||||
door_ptr->onPlayerContact(nullptr);
|
||||
REQUIRE(!level_complete_fired);
|
||||
}
|
||||
@@ -427,6 +431,77 @@ namespace {
|
||||
REQUIRE(mummy.getDirX() <= 0.0F);
|
||||
}
|
||||
|
||||
void testMummyHurtState() {
|
||||
std::cout << "[T15] mummy enterHurt → fugida + recuperació\n";
|
||||
Map::loadFrom("data/maps/pyramid.yaml");
|
||||
|
||||
fkyaml::node tmpl = EntityLoader::loadTemplate("mummy");
|
||||
Mummy mummy(tmpl);
|
||||
mummy.setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
||||
// Mateixa col·locació que testMummyStep: corredor exterior
|
||||
// superior, lliure de sarcs, prou marge per fugir cap a la dreta.
|
||||
mummy.setPixelRect({.x = 60.0F, .y = 40.0F, .w = 12.0F, .h = 24.0F});
|
||||
|
||||
// Direcció preferida: dreta. Mòmia ha d'entrar en HURT i avançar
|
||||
// a la dreta (la velocitat de hurt és més alta que la base).
|
||||
mummy.enterHurt(Mummy::Direction::RIGHT);
|
||||
REQUIRE(mummy.getState() == Mummy::State::HURT);
|
||||
REQUIRE(mummy.getDirX() > 0.0F);
|
||||
|
||||
const auto BEFORE = mummy.getPixelRect();
|
||||
mummy.update(0.05F); // 50 ms: hauria d'avançar
|
||||
const auto AFTER = mummy.getPixelRect();
|
||||
REQUIRE(AFTER.x > BEFORE.x);
|
||||
REQUIRE(mummy.getState() == Mummy::State::HURT);
|
||||
|
||||
// Avancem fins esgotar el timer (`hurt.duration_ms` = 600 ms al
|
||||
// YAML → 0.6 s; sumem 1 s amb un sol step gran per garantir-ho).
|
||||
mummy.update(1.0F);
|
||||
REQUIRE(mummy.getState() == Mummy::State::ROAMING);
|
||||
}
|
||||
|
||||
void testMummyHurtAgainstWall() {
|
||||
std::cout << "[T16] mummy HURT contra mur → surt abans del timer\n";
|
||||
Map::loadFrom("data/maps/pyramid.yaml");
|
||||
|
||||
fkyaml::node tmpl = EntityLoader::loadTemplate("mummy");
|
||||
Mummy mummy(tmpl);
|
||||
mummy.setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
||||
|
||||
// Posicionem la mòmia tocant la paret dreta del playfield i la
|
||||
// fem fugir cap a la dreta: el primer pas hauria de topar amb
|
||||
// la paret i fer-la sortir de HURT.
|
||||
const float RIGHT_WALL = static_cast<float>(Map::playfield_x + Map::playfield_w);
|
||||
mummy.setPixelRect({.x = RIGHT_WALL - 12.0F, .y = 40.0F, .w = 12.0F, .h = 24.0F});
|
||||
|
||||
// La direcció preferida (RIGHT) està bloquejada al test
|
||||
// d'`is_open`; entrarem amb una perpendicular o quieta.
|
||||
// L'objecte de la prova és que `state_` torne a ROAMING quan
|
||||
// el moviment topa: ho forcem fent que `enterHurt` triï igual
|
||||
// RIGHT i topem en cridar `update`. Per aconseguir-ho posicionem
|
||||
// la mòmia al límit i forcem la direcció via `setDirection`
|
||||
// ABANS d'`enterHurt`. Però com `enterHurt` reescriu la
|
||||
// direcció, el camí més directe és:
|
||||
// 1) `enterHurt(RIGHT)` → si is_open(RIGHT) val false, intenta
|
||||
// perpendiculars; si totes fallen, queda quieta.
|
||||
// 2) Si està quieta, el HURT tampoc avança i només mor pel
|
||||
// timer. Aquesta variant del test NO comprova la sortida
|
||||
// per col·lisió; només verifica que entrar a HURT a la
|
||||
// paret no peta.
|
||||
mummy.enterHurt(Mummy::Direction::RIGHT);
|
||||
REQUIRE(mummy.getState() == Mummy::State::HURT);
|
||||
// Update llarg → expira el timer i torna a ROAMING.
|
||||
mummy.update(1.0F);
|
||||
REQUIRE(mummy.getState() == Mummy::State::ROAMING);
|
||||
}
|
||||
|
||||
void testMummyWhippableFlag() {
|
||||
std::cout << "[T17] mummy YAML llig `whippable` correctament\n";
|
||||
fkyaml::node tmpl = EntityLoader::loadTemplate("mummy");
|
||||
Mummy mummy(tmpl);
|
||||
REQUIRE(mummy.isWhippable());
|
||||
}
|
||||
|
||||
void testTorchAnimation() {
|
||||
std::cout << "[T10] torch animation wired from yaml\n";
|
||||
// Regressió: el torch.yaml declarava `frames: [0]` i el render no
|
||||
@@ -449,6 +524,46 @@ namespace {
|
||||
REQUIRE(torch.getAnimation()->currentFrame() == 2);
|
||||
}
|
||||
|
||||
void testExitedAggregator() {
|
||||
std::cout << "[T14] EntityManager: player_exited → level_complete\n";
|
||||
EntityManager manager;
|
||||
|
||||
// Comptador inicialitzat per a 2 jugadors. Sense aquesta crida,
|
||||
// l'agregador no faria res (expected_players_ = 0).
|
||||
manager.setExpectedPlayerCount(2);
|
||||
|
||||
int level_complete_count = 0;
|
||||
manager.getBus().subscribe("level_complete", [&](const Event& ev) {
|
||||
++level_complete_count;
|
||||
// Event d'agregat: no atribuïble a un jugador concret.
|
||||
REQUIRE(ev.player_id == -1);
|
||||
});
|
||||
|
||||
// Primer player_exited: comptador a 1 < 2, no emet encara.
|
||||
manager.getBus().publish(Event{.name = "player_exited", .payload = std::monostate{}, .player_id = 0});
|
||||
REQUIRE(manager.exitedPlayerCount() == 1);
|
||||
REQUIRE(level_complete_count == 0);
|
||||
|
||||
// Segon player_exited: comptador a 2 == expected, emet una vegada.
|
||||
manager.getBus().publish(Event{.name = "player_exited", .payload = std::monostate{}, .player_id = 1});
|
||||
REQUIRE(manager.exitedPlayerCount() == 2);
|
||||
REQUIRE(level_complete_count == 1);
|
||||
|
||||
// Un tercer player_exited (no hauria de passar amb 2 jugadors,
|
||||
// però defensa contra publicacions extra): no torna a emetre.
|
||||
manager.getBus().publish(Event{.name = "player_exited", .payload = std::monostate{}, .player_id = 0});
|
||||
REQUIRE(level_complete_count == 1);
|
||||
|
||||
// setExpectedPlayerCount torna a obrir el cicle (això simularia
|
||||
// el que fa GameScene després d'un reload de sala). El comptador
|
||||
// d'EXITED es reinicia a 0 i el flag d'emissió també.
|
||||
manager.setExpectedPlayerCount(2);
|
||||
REQUIRE(manager.exitedPlayerCount() == 0);
|
||||
manager.getBus().publish(Event{.name = "player_exited", .payload = std::monostate{}, .player_id = 0});
|
||||
manager.getBus().publish(Event{.name = "player_exited", .payload = std::monostate{}, .player_id = 1});
|
||||
REQUIRE(level_complete_count == 2); // segon cicle ha emès una vegada més
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto main() -> int {
|
||||
@@ -466,7 +581,11 @@ auto main() -> int {
|
||||
testSarcophagusYaml();
|
||||
testCollisionBox();
|
||||
testMummyStep();
|
||||
testMummyHurtState();
|
||||
testMummyHurtAgainstWall();
|
||||
testMummyWhippableFlag();
|
||||
testTorchAnimation();
|
||||
testExitedAggregator();
|
||||
|
||||
if (g_failures == 0) {
|
||||
std::cout << "ALL OK\n";
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
// Smoke test de la FSM de sala (RoomFsm) extreta de GameScene. Cobreix:
|
||||
// T1 loadConfig — fitxer real → tots els camps llegits.
|
||||
// T2 loadConfig — fitxer absent → fallbacks compilats; corbes default
|
||||
// resoltes per UiConfig::curveByName.
|
||||
// T3 loadConfig — claus parcials → camps absents conserven fallback.
|
||||
// T4 advance — ROOM_INTRO consumeix les 5 sub-fases en ordre.
|
||||
// T5 advance — ROOM_INTRO desemboca a PLAYING quan acaba l'última fase.
|
||||
// T6 advance — PLAYING és sumidor: no avança res ni canvia d'estat.
|
||||
// T7 advance — ROOM_OUTRO consumeix les 4 sub-fases i recau a ROOM_INTRO.
|
||||
// T8 advance — un delta gran salta més d'una sub-fase en un sol pas.
|
||||
// T9 formatIntroPyramidLine / formatIntroRoomLine — substitució amb Locale.
|
||||
// T10 formatIntroPyramidLine / formatIntroRoomLine — fallback sense Locale.
|
||||
// T11 stateName — strings esperats per a cada State.
|
||||
//
|
||||
// El render no es testeja: la FSM és pura i no pinta res. Les regles de
|
||||
// gating d'input dels jugadors viuen a `GameScene::update` i estan
|
||||
// cobertes pel smoke run del binari (xvfb-run) — la part testable és
|
||||
// que `advance` retorna l'estat correcte, que és el senyal que activa
|
||||
// el gating al callsite.
|
||||
//
|
||||
// Execució:
|
||||
// make build && cd build && ctest --output-on-failure
|
||||
// O bé:
|
||||
// ./build/test_game_fsm
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/ui/ui_config.hpp"
|
||||
#include "game/scenes/room_fsm.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
int g_failures = 0;
|
||||
|
||||
void check(bool cond, const char* msg, const char* file, int line) {
|
||||
if (!cond) {
|
||||
std::cerr << " FAIL: " << msg << " (" << file << ":" << line << ")\n";
|
||||
++g_failures;
|
||||
}
|
||||
}
|
||||
|
||||
#define CHECK(cond) check((cond), #cond, __FILE__, __LINE__)
|
||||
|
||||
auto writeTemp(const std::string& path, const std::string& content) -> std::string {
|
||||
std::ofstream file(path);
|
||||
file << content;
|
||||
return path;
|
||||
}
|
||||
|
||||
void testLoadConfigFull() {
|
||||
std::cout << "T1 loadConfig — fitxer real complet\n";
|
||||
const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_full.yaml").string();
|
||||
writeTemp(PATH,
|
||||
"room_intro:\n"
|
||||
" fade_in_ms: 100\n"
|
||||
" text_delay_ms: 200\n"
|
||||
" text_enter_ms: 300\n"
|
||||
" text_hold_ms: 400\n"
|
||||
" text_exit_ms: 500\n"
|
||||
"room_outro:\n"
|
||||
" text_enter_ms: 600\n"
|
||||
" text_hold_ms: 700\n"
|
||||
" text_exit_ms: 800\n"
|
||||
" fade_out_ms: 900\n"
|
||||
"text:\n"
|
||||
" enter_curve: outQuad\n"
|
||||
" exit_curve: inQuad\n");
|
||||
const auto CFG = RoomFsm::loadConfig(PATH);
|
||||
CHECK(CFG.intro_fade_in_ms == 100);
|
||||
CHECK(CFG.intro_text_delay_ms == 200);
|
||||
CHECK(CFG.intro_text_enter_ms == 300);
|
||||
CHECK(CFG.intro_text_hold_ms == 400);
|
||||
CHECK(CFG.intro_text_exit_ms == 500);
|
||||
CHECK(CFG.outro_text_enter_ms == 600);
|
||||
CHECK(CFG.outro_text_hold_ms == 700);
|
||||
CHECK(CFG.outro_text_exit_ms == 800);
|
||||
CHECK(CFG.outro_fade_out_ms == 900);
|
||||
CHECK(CFG.text_enter_curve == UiConfig::curveByName("outQuad"));
|
||||
CHECK(CFG.text_exit_curve == UiConfig::curveByName("inQuad"));
|
||||
std::filesystem::remove(PATH);
|
||||
}
|
||||
|
||||
void testLoadConfigMissing() {
|
||||
std::cout << "T2 loadConfig — fitxer absent (fallbacks)\n";
|
||||
const auto CFG = RoomFsm::loadConfig("/tmp/aee_2026_does_not_exist.yaml");
|
||||
CHECK(CFG.intro_fade_in_ms == 500);
|
||||
CHECK(CFG.intro_text_delay_ms == 500);
|
||||
CHECK(CFG.intro_text_enter_ms == 500);
|
||||
CHECK(CFG.intro_text_hold_ms == 1000);
|
||||
CHECK(CFG.intro_text_exit_ms == 500);
|
||||
CHECK(CFG.outro_text_enter_ms == 500);
|
||||
CHECK(CFG.outro_text_hold_ms == 1000);
|
||||
CHECK(CFG.outro_text_exit_ms == 500);
|
||||
CHECK(CFG.outro_fade_out_ms == 500);
|
||||
CHECK(CFG.text_enter_curve != nullptr);
|
||||
CHECK(CFG.text_exit_curve != nullptr);
|
||||
}
|
||||
|
||||
void testLoadConfigPartial() {
|
||||
std::cout << "T3 loadConfig — claus parcials (mantenir fallback)\n";
|
||||
const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_partial.yaml").string();
|
||||
writeTemp(PATH,
|
||||
"room_intro:\n"
|
||||
" fade_in_ms: 42\n");
|
||||
const auto CFG = RoomFsm::loadConfig(PATH);
|
||||
CHECK(CFG.intro_fade_in_ms == 42);
|
||||
CHECK(CFG.intro_text_delay_ms == 500); // fallback
|
||||
CHECK(CFG.outro_fade_out_ms == 500); // fallback
|
||||
std::filesystem::remove(PATH);
|
||||
}
|
||||
|
||||
auto makeShortConfig() -> RoomFsm::Config {
|
||||
// Durades curtes per fer els tests d'`advance` deterministes i
|
||||
// independents dels valors per defecte.
|
||||
RoomFsm::Config cfg{};
|
||||
cfg.intro_fade_in_ms = 10;
|
||||
cfg.intro_text_delay_ms = 20;
|
||||
cfg.intro_text_enter_ms = 30;
|
||||
cfg.intro_text_hold_ms = 40;
|
||||
cfg.intro_text_exit_ms = 50;
|
||||
cfg.outro_text_enter_ms = 11;
|
||||
cfg.outro_text_hold_ms = 22;
|
||||
cfg.outro_text_exit_ms = 33;
|
||||
cfg.outro_fade_out_ms = 44;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
void testAdvanceIntroPhases() {
|
||||
std::cout << "T4 advance — ROOM_INTRO consumeix sub-fases en ordre\n";
|
||||
const auto CFG = makeShortConfig();
|
||||
RoomFsm::Transition t{};
|
||||
auto s = RoomFsm::State::ROOM_INTRO;
|
||||
|
||||
s = RoomFsm::advance(s, t, CFG, 5); // dins fase 0
|
||||
CHECK(s == RoomFsm::State::ROOM_INTRO && t.phase == 0 && t.elapsed_ms == 5);
|
||||
|
||||
s = RoomFsm::advance(s, t, CFG, 10); // surt de fase 0, entra a fase 1 (5 ms dins)
|
||||
CHECK(s == RoomFsm::State::ROOM_INTRO && t.phase == 1 && t.elapsed_ms == 5);
|
||||
|
||||
s = RoomFsm::advance(s, t, CFG, 20); // surt de fase 1 (15 dins), entra a fase 2 amb 5 ms (5+20-20=5)
|
||||
CHECK(s == RoomFsm::State::ROOM_INTRO && t.phase == 2 && t.elapsed_ms == 5);
|
||||
}
|
||||
|
||||
void testAdvanceIntroToPlaying() {
|
||||
std::cout << "T5 advance — ROOM_INTRO acaba a PLAYING\n";
|
||||
const auto CFG = makeShortConfig();
|
||||
RoomFsm::Transition t{};
|
||||
const int TOTAL = CFG.intro_fade_in_ms + CFG.intro_text_delay_ms +
|
||||
CFG.intro_text_enter_ms + CFG.intro_text_hold_ms +
|
||||
CFG.intro_text_exit_ms;
|
||||
const auto S = RoomFsm::advance(RoomFsm::State::ROOM_INTRO, t, CFG, TOTAL);
|
||||
CHECK(S == RoomFsm::State::PLAYING);
|
||||
}
|
||||
|
||||
void testAdvancePlayingNoOp() {
|
||||
std::cout << "T6 advance — PLAYING és sumidor\n";
|
||||
const auto CFG = makeShortConfig();
|
||||
RoomFsm::Transition t{42, 999};
|
||||
const auto S = RoomFsm::advance(RoomFsm::State::PLAYING, t, CFG, 1000);
|
||||
CHECK(S == RoomFsm::State::PLAYING);
|
||||
CHECK(t.phase == 42); // intacte
|
||||
CHECK(t.elapsed_ms == 999);
|
||||
}
|
||||
|
||||
void testAdvanceOutroToIntro() {
|
||||
std::cout << "T7 advance — ROOM_OUTRO recau a ROOM_INTRO\n";
|
||||
const auto CFG = makeShortConfig();
|
||||
RoomFsm::Transition t{};
|
||||
const int TOTAL = CFG.outro_text_enter_ms + CFG.outro_text_hold_ms +
|
||||
CFG.outro_text_exit_ms + CFG.outro_fade_out_ms;
|
||||
const auto S = RoomFsm::advance(RoomFsm::State::ROOM_OUTRO, t, CFG, TOTAL);
|
||||
CHECK(S == RoomFsm::State::ROOM_INTRO);
|
||||
}
|
||||
|
||||
void testAdvanceLargeDeltaJumps() {
|
||||
std::cout << "T8 advance — un delta gran salta múltiples sub-fases\n";
|
||||
const auto CFG = makeShortConfig();
|
||||
RoomFsm::Transition t{};
|
||||
// 10+20+5 = 35 ms → consumeix fase 0 i 1, queda a fase 2 amb 5 ms.
|
||||
const auto S = RoomFsm::advance(RoomFsm::State::ROOM_INTRO, t, CFG, 35);
|
||||
CHECK(S == RoomFsm::State::ROOM_INTRO);
|
||||
CHECK(t.phase == 2);
|
||||
CHECK(t.elapsed_ms == 5);
|
||||
}
|
||||
|
||||
void testFormatRoomTitleWithLocale() {
|
||||
std::cout << "T9 formatIntroPyramidLine / formatIntroRoomLine — substitució amb Locale\n";
|
||||
const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_locale.yaml").string();
|
||||
writeTemp(PATH,
|
||||
"game:\n"
|
||||
" room_intro_pyramid: \"Piramide {p}\"\n"
|
||||
" room_intro_room: \"Sala {r}\"\n");
|
||||
Locale::get().load(PATH);
|
||||
CHECK(RoomFsm::formatIntroPyramidLine(1) == "Piramide 1");
|
||||
CHECK(RoomFsm::formatIntroPyramidLine(5) == "Piramide 5");
|
||||
CHECK(RoomFsm::formatIntroRoomLine(1) == "Sala 1");
|
||||
CHECK(RoomFsm::formatIntroRoomLine(7) == "Sala 7");
|
||||
std::filesystem::remove(PATH);
|
||||
}
|
||||
|
||||
void testFormatRoomTitleNoLocale() {
|
||||
std::cout << "T10 formatIntroPyramidLine / formatIntroRoomLine — fallback sense traducció\n";
|
||||
const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_locale_empty.yaml").string();
|
||||
writeTemp(PATH, "other: foo\n");
|
||||
Locale::get().load(PATH);
|
||||
// Sense les claus, Locale retorna la pròpia clau i com que no
|
||||
// porten `{p}`/`{r}`, la substitució no canvia res.
|
||||
CHECK(RoomFsm::formatIntroPyramidLine(3) == "game.room_intro_pyramid");
|
||||
CHECK(RoomFsm::formatIntroRoomLine(2) == "game.room_intro_room");
|
||||
std::filesystem::remove(PATH);
|
||||
}
|
||||
|
||||
void testStateName() {
|
||||
std::cout << "T11 stateName — strings esperats\n";
|
||||
CHECK(std::string(RoomFsm::stateName(RoomFsm::State::ROOM_INTRO)) == "ROOM_INTRO");
|
||||
CHECK(std::string(RoomFsm::stateName(RoomFsm::State::PLAYING)) == "PLAYING");
|
||||
CHECK(std::string(RoomFsm::stateName(RoomFsm::State::ROOM_OUTRO)) == "ROOM_OUTRO");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto main() -> int {
|
||||
testLoadConfigFull();
|
||||
testLoadConfigMissing();
|
||||
testLoadConfigPartial();
|
||||
testAdvanceIntroPhases();
|
||||
testAdvanceIntroToPlaying();
|
||||
testAdvancePlayingNoOp();
|
||||
testAdvanceOutroToIntro();
|
||||
testAdvanceLargeDeltaJumps();
|
||||
testFormatRoomTitleWithLocale();
|
||||
testFormatRoomTitleNoLocale();
|
||||
testStateName();
|
||||
|
||||
if (g_failures == 0) {
|
||||
std::cout << "OK: tots els tests de RoomFsm passen\n";
|
||||
return 0;
|
||||
}
|
||||
std::cerr << g_failures << " tests han fallat\n";
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
// Smoke tests del WhipAttack: hitbox per direcció, retall contra murs i
|
||||
// sarcs, aplicació de dany a mòmies whippable, alreadyHit. Friendly
|
||||
// fire (Player↔Player) NO es cobreix aquí — Player viu enllaçat amb
|
||||
// stubs sense estat real (veure whip_attack_player_stubs.cpp).
|
||||
//
|
||||
// Convencions iguals a test_entities_smoke: framework-less, REQUIRE
|
||||
// macro, comptador `g_failures`. Execució: `./test_whip_attack` des de
|
||||
// build/, working_dir = arrel del repo.
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "game/entities/concrete/mummy.hpp"
|
||||
#include "game/entities/concrete/whip_attack.hpp"
|
||||
#include "game/entities/entity_loader.hpp"
|
||||
#include "game/entities/entity_manager.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
int g_failures = 0;
|
||||
|
||||
void check(bool cond, const char* msg, const char* file, int line) {
|
||||
if (!cond) {
|
||||
std::cerr << " FAIL: " << msg << " (" << file << ":" << line << ")\n";
|
||||
++g_failures;
|
||||
}
|
||||
}
|
||||
|
||||
#define REQUIRE(cond) check((cond), #cond, __FILE__, __LINE__)
|
||||
|
||||
// Caja de jugador "estàndard" AEE: 16×32 amb cantonada (x, y).
|
||||
// El damage_rect del whip s'alinearà al feet box (els últims 16 px),
|
||||
// i el draw_rect respectarà els offsets de la taula del disseny.
|
||||
auto playerBox(float x, float y) -> SDL_FRect {
|
||||
return SDL_FRect{.x = x, .y = y, .w = 16.0F, .h = 32.0F};
|
||||
}
|
||||
|
||||
// Spawneja un WhipAttack ben configurat per al test, sense surface.
|
||||
// Els tests no exerciten render, així que `surface_=nullptr` és OK.
|
||||
auto makeWhip(WhipAttack::Direction dir, const SDL_FRect& owner_box, float range_px)
|
||||
-> std::unique_ptr<WhipAttack> {
|
||||
auto yaml = EntityLoader::loadTemplate("whip_attack");
|
||||
auto whip = std::make_unique<WhipAttack>(yaml);
|
||||
whip->configure(dir, owner_box, range_px);
|
||||
return whip;
|
||||
}
|
||||
|
||||
void testWhipDrawRectPerDirection() {
|
||||
std::cout << "[W1] WhipAttack draw_rect per direcció\n";
|
||||
Map::loadFrom("data/maps/pyramid.yaml");
|
||||
|
||||
// Owner ben dins del playfield perquè el draw_rect (no es retalla)
|
||||
// tinga la mida nominal completa.
|
||||
// Posicionem el jugador amb la collision (bottom 16×16) al
|
||||
// corredor entre files 2 i 3 de sarcs (y∈[144,160]) i a la
|
||||
// columna entre cols 0 i 1 (x∈[88,104]) — així el damage_rect
|
||||
// cap al sarc-buit corredor i no es retalla per sarcs.
|
||||
const float OX = static_cast<float>(Map::playfield_x + 64);
|
||||
const float OY = static_cast<float>(Map::playfield_y + 80);
|
||||
const auto BOX = playerBox(OX, OY);
|
||||
const float RANGE = 48.0F;
|
||||
|
||||
const auto WE = makeWhip(WhipAttack::Direction::EAST, BOX, RANGE);
|
||||
REQUIRE(WE->getPixelRect().w == 80.0F);
|
||||
REQUIRE(WE->getPixelRect().h == 32.0F);
|
||||
|
||||
const auto WW = makeWhip(WhipAttack::Direction::WEST, BOX, RANGE);
|
||||
REQUIRE(WW->getPixelRect().w == 80.0F);
|
||||
REQUIRE(WW->getPixelRect().h == 32.0F);
|
||||
|
||||
const auto WN = makeWhip(WhipAttack::Direction::NORTH, BOX, RANGE);
|
||||
REQUIRE(WN->getPixelRect().w == 16.0F);
|
||||
REQUIRE(WN->getPixelRect().h == 96.0F);
|
||||
|
||||
const auto WS = makeWhip(WhipAttack::Direction::SOUTH, BOX, RANGE);
|
||||
REQUIRE(WS->getPixelRect().w == 16.0F);
|
||||
REQUIRE(WS->getPixelRect().h == 96.0F);
|
||||
}
|
||||
|
||||
void testWhipDrawRectOffsets() {
|
||||
std::cout << "[W7] offsets del draw_rect per direcció (taula del disseny)\n";
|
||||
Map::loadFrom("data/maps/pyramid.yaml");
|
||||
|
||||
// Posicionem el jugador amb la collision (bottom 16×16) al
|
||||
// corredor entre files 2 i 3 de sarcs (y∈[144,160]) i a la
|
||||
// columna entre cols 0 i 1 (x∈[88,104]) — així el damage_rect
|
||||
// cap al sarc-buit corredor i no es retalla per sarcs.
|
||||
const float OX = static_cast<float>(Map::playfield_x + 64);
|
||||
const float OY = static_cast<float>(Map::playfield_y + 80);
|
||||
const auto BOX = playerBox(OX, OY);
|
||||
const float RANGE = 48.0F;
|
||||
|
||||
// E: (player.x - 16, player.y), 80×32.
|
||||
const auto WE = makeWhip(WhipAttack::Direction::EAST, BOX, RANGE);
|
||||
REQUIRE(WE->getPixelRect().x == OX - 16.0F);
|
||||
REQUIRE(WE->getPixelRect().y == OY);
|
||||
|
||||
// W: (player.x - 48, player.y), 80×32.
|
||||
const auto WW = makeWhip(WhipAttack::Direction::WEST, BOX, RANGE);
|
||||
REQUIRE(WW->getPixelRect().x == OX - 48.0F);
|
||||
REQUIRE(WW->getPixelRect().y == OY);
|
||||
|
||||
// N: (player.x, player.y - 48), 16×96.
|
||||
const auto WN = makeWhip(WhipAttack::Direction::NORTH, BOX, RANGE);
|
||||
REQUIRE(WN->getPixelRect().x == OX);
|
||||
REQUIRE(WN->getPixelRect().y == OY - 48.0F);
|
||||
|
||||
// S: (player.x, player.y - 16), 16×96.
|
||||
const auto WS = makeWhip(WhipAttack::Direction::SOUTH, BOX, RANGE);
|
||||
REQUIRE(WS->getPixelRect().x == OX);
|
||||
REQUIRE(WS->getPixelRect().y == OY - 16.0F);
|
||||
}
|
||||
|
||||
void testWhipFeetYDelegation() {
|
||||
std::cout << "[W8] feet_y delegat a la base de l'owner_box\n";
|
||||
Map::loadFrom("data/maps/pyramid.yaml");
|
||||
|
||||
// Posicionem el jugador amb la collision (bottom 16×16) al
|
||||
// corredor entre files 2 i 3 de sarcs (y∈[144,160]) i a la
|
||||
// columna entre cols 0 i 1 (x∈[88,104]) — així el damage_rect
|
||||
// cap al sarc-buit corredor i no es retalla per sarcs.
|
||||
const float OX = static_cast<float>(Map::playfield_x + 64);
|
||||
const float OY = static_cast<float>(Map::playfield_y + 80);
|
||||
const auto BOX = playerBox(OX, OY);
|
||||
const float EXPECTED_FEET_Y = OY + 32.0F; // base inferior del 16×32
|
||||
|
||||
// Independent de la direcció: el feet_y és el del jugador.
|
||||
for (auto dir : {WhipAttack::Direction::EAST,
|
||||
WhipAttack::Direction::WEST,
|
||||
WhipAttack::Direction::NORTH,
|
||||
WhipAttack::Direction::SOUTH}) {
|
||||
const auto W = makeWhip(dir, BOX, 48.0F);
|
||||
REQUIRE(W->getFeetY() == EXPECTED_FEET_Y);
|
||||
}
|
||||
}
|
||||
|
||||
void testWhipHitboxClippedByWall() {
|
||||
std::cout << "[W2] damage_rect retallat per la paret del playfield\n";
|
||||
Map::loadFrom("data/maps/pyramid.yaml");
|
||||
|
||||
// Llancem un E des d'una posició a 10 px de la paret dreta del
|
||||
// playfield, amb un range de 48 px → la hitbox efectiva ha de
|
||||
// quedar acotada a < 48 i no més enllà del mur.
|
||||
const float RIGHT_WALL = static_cast<float>(Map::playfield_x + Map::playfield_w);
|
||||
const float OX = RIGHT_WALL - 16.0F - 10.0F; // l'owner mateix dins
|
||||
const float OY = static_cast<float>(Map::playfield_y + 96);
|
||||
const auto BOX = playerBox(OX, OY);
|
||||
const float RANGE = 48.0F;
|
||||
|
||||
const auto W = makeWhip(WhipAttack::Direction::EAST, BOX, RANGE);
|
||||
const auto DMG = W->getDamageRect();
|
||||
REQUIRE(DMG.w < RANGE);
|
||||
REQUIRE(DMG.x + DMG.w <= RIGHT_WALL);
|
||||
}
|
||||
|
||||
void testWhipHitboxFreeAtFullRange() {
|
||||
std::cout << "[W3] damage_rect sense bloquejador → longitud completa\n";
|
||||
Map::loadFrom("data/maps/pyramid.yaml");
|
||||
|
||||
// Owner ben dins del playfield, range curt → 16 px lliures fins
|
||||
// a topar amb res. La longitud efectiva = RANGE.
|
||||
const float OX = static_cast<float>(Map::playfield_x + 32);
|
||||
const float OY = static_cast<float>(Map::playfield_y + 96);
|
||||
const auto BOX = playerBox(OX, OY);
|
||||
const float RANGE = 16.0F;
|
||||
|
||||
const auto W = makeWhip(WhipAttack::Direction::SOUTH, BOX, RANGE);
|
||||
const auto DMG = W->getDamageRect();
|
||||
REQUIRE(DMG.h == RANGE);
|
||||
REQUIRE(DMG.w == 16.0F);
|
||||
}
|
||||
|
||||
void testWhipHitsMummyInRange() {
|
||||
std::cout << "[W4] mòmia en rang entra a HURT\n";
|
||||
Map::loadFrom("data/maps/pyramid.yaml");
|
||||
|
||||
EntityManager manager;
|
||||
auto mummy_yaml = EntityLoader::loadTemplate("mummy");
|
||||
auto mummy_owned = std::make_unique<Mummy>(mummy_yaml);
|
||||
mummy_owned->setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
||||
|
||||
// Owner i mòmia al mateix corredor; mòmia 24 px a la dreta del
|
||||
// jugador → dins del damage_rect (48 px de rang).
|
||||
// Posicionem el jugador amb la collision (bottom 16×16) al
|
||||
// corredor entre files 2 i 3 de sarcs (y∈[144,160]) i a la
|
||||
// columna entre cols 0 i 1 (x∈[88,104]) — així el damage_rect
|
||||
// cap al sarc-buit corredor i no es retalla per sarcs.
|
||||
const float OX = static_cast<float>(Map::playfield_x + 64);
|
||||
const float OY = static_cast<float>(Map::playfield_y + 80);
|
||||
const auto BOX = playerBox(OX, OY);
|
||||
// Posicionem la mòmia perquè la seua collision box solape amb
|
||||
// la zona on cau el damage_rect (els últims 16 px verticals
|
||||
// del player, és a dir y ∈ [OY+16, OY+32]).
|
||||
// Volem que la collision (offset_y=12, height=12) solape amb el
|
||||
// damage_rect del whip (a y ∈ [OY+16, OY+32]). Posant pixel_rect.y
|
||||
// = OY, la collision queda a [OY+12, OY+24], 8 px d'overlap.
|
||||
mummy_owned->setPixelRect({.x = OX + 24.0F, .y = OY, .w = 12.0F, .h = 24.0F});
|
||||
auto* mummy_raw = static_cast<Mummy*>(manager.add(std::move(mummy_owned)));
|
||||
|
||||
auto whip = makeWhip(WhipAttack::Direction::EAST, BOX, 48.0F);
|
||||
whip->setWorldContext(&manager, nullptr);
|
||||
|
||||
// 60 ms → frame 1 (active). active_frames: [1, 2].
|
||||
whip->update(0.06F);
|
||||
|
||||
REQUIRE(mummy_raw->getState() == Mummy::State::HURT);
|
||||
}
|
||||
|
||||
void testWhipNoHitOutOfRange() {
|
||||
std::cout << "[W5] mòmia fora del rang no rep dany\n";
|
||||
Map::loadFrom("data/maps/pyramid.yaml");
|
||||
|
||||
EntityManager manager;
|
||||
auto mummy_yaml = EntityLoader::loadTemplate("mummy");
|
||||
auto mummy_owned = std::make_unique<Mummy>(mummy_yaml);
|
||||
mummy_owned->setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
||||
|
||||
// Mateixa col·locació corredor 2-3 que W4: el damage_rect cap
|
||||
// íntegre al sarc-buit; només l'eix x distingirà "dins" de "fora".
|
||||
const float OX = static_cast<float>(Map::playfield_x + 64);
|
||||
const float OY = static_cast<float>(Map::playfield_y + 80);
|
||||
const auto BOX = playerBox(OX, OY);
|
||||
// Mòmia 96 px a la dreta de l'owner → fora d'un range de 32.
|
||||
// Vertically aligned with the damage band perquè el test només
|
||||
// depenga de l'eix horitzontal (no d'una falla d'alineament Y).
|
||||
mummy_owned->setPixelRect({.x = OX + 96.0F, .y = OY, .w = 12.0F, .h = 24.0F});
|
||||
auto* mummy_raw = static_cast<Mummy*>(manager.add(std::move(mummy_owned)));
|
||||
|
||||
auto whip = makeWhip(WhipAttack::Direction::EAST, BOX, 32.0F);
|
||||
whip->setWorldContext(&manager, nullptr);
|
||||
whip->update(0.06F);
|
||||
|
||||
REQUIRE(mummy_raw->getState() == Mummy::State::ROAMING);
|
||||
}
|
||||
|
||||
void testWhipSingleHitPerSwing() {
|
||||
std::cout << "[W6] una mòmia rep un sol impacte per swing\n";
|
||||
Map::loadFrom("data/maps/pyramid.yaml");
|
||||
|
||||
EntityManager manager;
|
||||
auto mummy_yaml = EntityLoader::loadTemplate("mummy");
|
||||
auto mummy_owned = std::make_unique<Mummy>(mummy_yaml);
|
||||
mummy_owned->setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
||||
|
||||
// Posicionem el jugador amb la collision (bottom 16×16) al
|
||||
// corredor entre files 2 i 3 de sarcs (y∈[144,160]) i a la
|
||||
// columna entre cols 0 i 1 (x∈[88,104]) — així el damage_rect
|
||||
// cap al sarc-buit corredor i no es retalla per sarcs.
|
||||
const float OX = static_cast<float>(Map::playfield_x + 64);
|
||||
const float OY = static_cast<float>(Map::playfield_y + 80);
|
||||
const auto BOX = playerBox(OX, OY);
|
||||
// Volem que la collision (offset_y=12, height=12) solape amb el
|
||||
// damage_rect del whip (a y ∈ [OY+16, OY+32]). Posant pixel_rect.y
|
||||
// = OY, la collision queda a [OY+12, OY+24], 8 px d'overlap.
|
||||
mummy_owned->setPixelRect({.x = OX + 24.0F, .y = OY, .w = 12.0F, .h = 24.0F});
|
||||
auto* mummy_raw = static_cast<Mummy*>(manager.add(std::move(mummy_owned)));
|
||||
|
||||
auto whip = makeWhip(WhipAttack::Direction::EAST, BOX, 48.0F);
|
||||
whip->setWorldContext(&manager, nullptr);
|
||||
|
||||
// Primera passada (frame 1, actiu) → mòmia a HURT.
|
||||
whip->update(0.06F);
|
||||
REQUIRE(mummy_raw->getState() == Mummy::State::HURT);
|
||||
const float HURT_INITIAL = mummy_raw->getHurtRemainingS();
|
||||
|
||||
// Avancem la mòmia (descompta el seu timer) i tornem a tickar el
|
||||
// whip mentre l'animació segueix activa: si `already_hit_`
|
||||
// funciona, el segon hit es bloqueja i el timer de la mòmia NO
|
||||
// es reseteja al valor inicial.
|
||||
mummy_raw->update(0.05F);
|
||||
const float HURT_AFTER_MUMMY_TICK = mummy_raw->getHurtRemainingS();
|
||||
REQUIRE(HURT_AFTER_MUMMY_TICK < HURT_INITIAL);
|
||||
|
||||
whip->update(0.05F); // frame 2, encara actiu
|
||||
REQUIRE(mummy_raw->getHurtRemainingS() == HURT_AFTER_MUMMY_TICK);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto main() -> int {
|
||||
std::cout << "== smoke tests: WhipAttack ==\n";
|
||||
|
||||
testWhipDrawRectPerDirection();
|
||||
testWhipDrawRectOffsets();
|
||||
testWhipFeetYDelegation();
|
||||
testWhipHitboxClippedByWall();
|
||||
testWhipHitboxFreeAtFullRange();
|
||||
testWhipHitsMummyInRange();
|
||||
testWhipNoHitOutOfRange();
|
||||
testWhipSingleHitPerSwing();
|
||||
|
||||
if (g_failures == 0) {
|
||||
std::cout << "ALL OK\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
std::cerr << g_failures << " failures\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Stubs de Player per al binari `test_whip_attack`.
|
||||
//
|
||||
// El path de friendly fire del `WhipAttack` referencia
|
||||
// `Player::enterHurt(Cardinal)` i `Player::getBaseBox() const`. Linkar
|
||||
// `player.cpp` arrossegaria tot l'stack del jugador (audio, gamepad,
|
||||
// input, screen, resource_cache…), una explosió de dependències per a
|
||||
// uns tests que no exerciten aqueix camí. Aquests stubs proporcionen
|
||||
// els símbols com a no-op perquè el test del whip pugui linkar i centrar
|
||||
// la cobertura en hitbox + Mummy.
|
||||
//
|
||||
// Si algun dia volem cobrir friendly fire, caldrà o bé un binari de test
|
||||
// amb tot l'stack o invertir la dependència via callable injectable
|
||||
// (refactor de `WhipAttack::applyDamage`).
|
||||
#include "game/entities/player.hpp"
|
||||
|
||||
void Player::enterHurt(Cardinal /*preferred*/) {
|
||||
}
|
||||
|
||||
auto Player::getBaseBox() const -> SDL_FRect {
|
||||
return SDL_FRect{0.0F, 0.0F, 0.0F, 0.0F};
|
||||
}
|
||||