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 |
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
|
# Sources comuns del sistema d'entitats: s'afegeixen tant a l'executable
|
||||||
# principal com al smoke test. Mantenir-los en variable separada evita
|
# principal com al smoke test. Mantenir-los en variable separada evita
|
||||||
# duplicació entre els dos targets.
|
# 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/entity.cpp
|
||||||
source/game/entities/animation_component.cpp
|
source/game/entities/animation_component.cpp
|
||||||
source/game/entities/event_bus.cpp
|
source/game/entities/event_bus.cpp
|
||||||
@@ -50,12 +53,21 @@ set(ENTITY_SOURCES
|
|||||||
source/game/entities/concrete/dust.cpp
|
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
|
set(APP_SOURCES
|
||||||
source/main.cpp
|
source/main.cpp
|
||||||
source/core/system/director.cpp
|
source/core/system/director.cpp
|
||||||
source/core/audio/audio.cpp
|
source/core/audio/audio.cpp
|
||||||
source/core/audio/audio_adapter.cpp
|
source/core/audio/audio_adapter.cpp
|
||||||
|
source/core/audio/audio_effects.cpp
|
||||||
source/core/audio/jail_audio.cpp
|
source/core/audio/jail_audio.cpp
|
||||||
|
source/core/audio/sound_effects_config.cpp
|
||||||
source/core/locale/locale.cpp
|
source/core/locale/locale.cpp
|
||||||
source/core/ui/ui_config.cpp
|
source/core/ui/ui_config.cpp
|
||||||
source/core/input/key_config.cpp
|
source/core/input/key_config.cpp
|
||||||
@@ -101,6 +113,7 @@ set(APP_SOURCES
|
|||||||
source/game/map/tilemap.cpp
|
source/game/map/tilemap.cpp
|
||||||
source/game/map/tilemap_writer.cpp
|
source/game/map/tilemap_writer.cpp
|
||||||
source/game/map/paint_layer.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`).
|
# 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
|
# En Release el fitxer compila a un object buit i l'enllaçador no
|
||||||
# arrossega cap símbol del joc final.
|
# arrossega cap símbol del joc final.
|
||||||
@@ -163,6 +176,15 @@ endif()
|
|||||||
|
|
||||||
add_executable(${PROJECT_NAME} ${APP_SOURCES})
|
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)
|
if(NOT APPLE AND GLSLC_EXE)
|
||||||
add_dependencies(${PROJECT_NAME} shaders)
|
add_dependencies(${PROJECT_NAME} shaders)
|
||||||
endif()
|
endif()
|
||||||
@@ -326,7 +348,7 @@ if(BUILD_TESTS)
|
|||||||
source/core/resources/resource_helper.cpp
|
source/core/resources/resource_helper.cpp
|
||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
${ENTITY_SOURCES}
|
${ENTITY_SOURCES_BASE}
|
||||||
)
|
)
|
||||||
target_include_directories(test_entities_smoke PRIVATE
|
target_include_directories(test_entities_smoke PRIVATE
|
||||||
"${CMAKE_SOURCE_DIR}/source"
|
"${CMAKE_SOURCE_DIR}/source"
|
||||||
@@ -339,6 +361,29 @@ if(BUILD_TESTS)
|
|||||||
COMMAND test_entities_smoke
|
COMMAND test_entities_smoke
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
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:
|
# Smoke del flux d'escenes i del parser de debug.yaml. Stand-alone:
|
||||||
# només enllaça els helpers purs (debug_config, placeholder_input,
|
# només enllaça els helpers purs (debug_config, placeholder_input,
|
||||||
# key_config) perquè la integració completa dels placeholders arrossegaria
|
# key_config) perquè la integració completa dels placeholders arrossegaria
|
||||||
|
|||||||
@@ -1,13 +1,43 @@
|
|||||||
PROJECT := aee_2026
|
PROJECT := aee_2026
|
||||||
BUILDDIR := build
|
BUILDDIR := build
|
||||||
JOBS ?= $(shell nproc 2>/dev/null || echo 4)
|
|
||||||
|
|
||||||
# VERSION (única font de veritat: source/utils/defines.hpp)
|
# Detecció de plataforma. En Windows CMake defaulteja a "NMake Makefiles"
|
||||||
VERSION := v$(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
|
# (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
|
# 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).
|
# 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),)
|
ifeq ($(GIT_HASH),)
|
||||||
GIT_HASH := unknown
|
GIT_HASH := unknown
|
||||||
endif
|
endif
|
||||||
@@ -26,11 +56,11 @@ CMAKE_DEFS := -DGIT_HASH=$(GIT_HASH)
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
all:
|
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)
|
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||||
|
|
||||||
debug:
|
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)
|
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||||
|
|
||||||
run: all
|
run: all
|
||||||
@@ -57,7 +87,7 @@ rebuild: clean all
|
|||||||
# del pack encara que res no hagi canviat dins data/. L'empaquetat també es fa
|
# 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`.
|
# automàticament a cada `make`/`make debug` via el target CMake `resource_pack`.
|
||||||
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
|
@cmake --build $(BUILDDIR) --target pack_resources
|
||||||
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
||||||
|
|
||||||
@@ -74,23 +104,23 @@ controllerdb:
|
|||||||
# CODE QUALITY (delegados a cmake)
|
# CODE QUALITY (delegados a cmake)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
format:
|
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
|
@cmake --build $(BUILDDIR) --target format
|
||||||
|
|
||||||
format-check:
|
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
|
@cmake --build $(BUILDDIR) --target format-check
|
||||||
|
|
||||||
tidy:
|
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
|
@cmake --build $(BUILDDIR) --target tidy
|
||||||
|
|
||||||
tidy-fix:
|
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
|
@cmake --build $(BUILDDIR) --target tidy-fix
|
||||||
|
|
||||||
cppcheck:
|
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
|
@cmake --build $(BUILDDIR) --target cppcheck
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|||||||
@@ -38,3 +38,13 @@ ai:
|
|||||||
stats:
|
stats:
|
||||||
speed: 30
|
speed: 30
|
||||||
damage: 1
|
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
|
- id: editor_picker
|
||||||
code: "T"
|
code: "T"
|
||||||
desc: "Obri/tanca el tilepicker dins de l'editor"
|
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
|
- id: exit
|
||||||
code: "Escape"
|
code: "Escape"
|
||||||
desc: "Eixir (confirmar amb segona pulsació)"
|
desc: "Eixir (confirmar amb segona pulsació)"
|
||||||
@@ -37,11 +37,15 @@ hud:
|
|||||||
room: "Sala"
|
room: "Sala"
|
||||||
|
|
||||||
# Textos de la transició d'entrada/sortida de sala (FSM de GameScene).
|
# Textos de la transició d'entrada/sortida de sala (FSM de GameScene).
|
||||||
# `room_intro` accepta el placeholder `{n}` que es substitueix amb
|
# Tant l'INTRO com l'OUTRO es renderitzen com a dues línies separades
|
||||||
# l'índex de sala actual; veure `GameScene::formatRoomTitle`.
|
# (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:
|
game:
|
||||||
room_intro: "Sala {n}"
|
room_intro_pyramid: "Piramide {p}"
|
||||||
room_complete: "Fase completada"
|
room_intro_room: "Sala {r}"
|
||||||
|
room_complete_line1: "Fase"
|
||||||
|
room_complete_line2: "Completada"
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
titles:
|
titles:
|
||||||
|
|||||||
@@ -36,12 +36,15 @@ hud:
|
|||||||
pyramid: "Pyramid"
|
pyramid: "Pyramid"
|
||||||
room: "Room"
|
room: "Room"
|
||||||
|
|
||||||
# Room transition texts (GameScene FSM intro/outro). The `room_intro`
|
# Room transition texts (GameScene FSM intro/outro). Both INTRO and
|
||||||
# string accepts a `{n}` placeholder that is replaced with the current
|
# OUTRO render as two independent lines with their own animation.
|
||||||
# room index; see `GameScene::formatRoomTitle`.
|
# INTRO uses `{p}` (pyramid) and `{r}` (room) placeholders; OUTRO has
|
||||||
|
# no placeholders — it's the two words of the "stage complete" label.
|
||||||
game:
|
game:
|
||||||
room_intro: "Room {n}"
|
room_intro_pyramid: "Pyramid {p}"
|
||||||
room_complete: "Stage complete"
|
room_intro_room: "Room {r}"
|
||||||
|
room_complete_line1: "Stage"
|
||||||
|
room_complete_line2: "Complete"
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
titles:
|
titles:
|
||||||
|
|||||||
@@ -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]
|
||||||
@@ -68,6 +68,36 @@ animations:
|
|||||||
frames: [8, 9, 10, 11]
|
frames: [8, 9, 10, 11]
|
||||||
frame_ms: 100
|
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
|
# 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
|
# 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`.
|
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||||
@@ -127,3 +157,11 @@ jump:
|
|||||||
hurt:
|
hurt:
|
||||||
speed: 120
|
speed: 120
|
||||||
brake_time_ms: 500
|
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]
|
frames: [8, 9, 10, 11]
|
||||||
frame_ms: 100
|
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
|
# 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
|
# 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`.
|
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||||
@@ -127,3 +157,16 @@ jump:
|
|||||||
hurt:
|
hurt:
|
||||||
speed: 120
|
speed: 120
|
||||||
brake_time_ms: 500
|
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
|
||||||
@@ -7,18 +7,43 @@
|
|||||||
room_intro:
|
room_intro:
|
||||||
fade_in_ms: 500 # negre opac → món visible
|
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_delay_ms: 500 # espera amb el món net abans de sortir el text
|
||||||
text_enter_ms: 500 # text esquerra → centre
|
text_enter_ms: 400 # text esquerra → centre
|
||||||
text_hold_ms: 1000 # text quiet al centre
|
text_hold_ms: 2000 # text quiet al centre
|
||||||
text_exit_ms: 500 # text centre → fora per l'esquerra
|
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:
|
room_outro:
|
||||||
text_enter_ms: 500
|
text_enter_ms: 400
|
||||||
text_hold_ms: 1000
|
text_hold_ms: 2000
|
||||||
text_exit_ms: 500
|
text_exit_ms: 400
|
||||||
fade_out_ms: 500 # món visible → negre opac
|
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:
|
text:
|
||||||
# Noms d'easing tal com els resol UiConfig::curveByName (vegeu
|
# Noms d'easing tal com els resol UiConfig::curveByName (vegeu
|
||||||
# `data/ui.yaml`). El text entra suau i surt accelerant.
|
# `data/ui.yaml`). El text entra suau i surt accelerant.
|
||||||
enter_curve: outCubic
|
enter_curve: outCubic
|
||||||
exit_curve: inCubic
|
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_label: 58
|
||||||
hud_value: 187
|
hud_value: 187
|
||||||
hud_flash: 237
|
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:
|
timings:
|
||||||
notification_ms: 4000
|
notification_ms: 4000
|
||||||
@@ -36,6 +42,12 @@ fonts:
|
|||||||
gif: "data/fonts/ari-w9500-condensed-display.gif"
|
gif: "data/fonts/ari-w9500-condensed-display.gif"
|
||||||
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
|
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
|
||||||
kerning: 1
|
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:
|
menu_title:
|
||||||
name: "ari-w9500-display"
|
name: "ari-w9500-display"
|
||||||
gif: "data/fonts/ari-w9500-display.gif"
|
gif: "data/fonts/ari-w9500-display.gif"
|
||||||
@@ -48,7 +60,7 @@ fonts:
|
|||||||
kerning: 0
|
kerning: 0
|
||||||
|
|
||||||
hud:
|
hud:
|
||||||
bg_enabled: true
|
bg_enabled: false
|
||||||
entry_ms: 800 # durada de l'animació d'entrada dels tres blocs
|
entry_ms: 800 # durada de l'animació d'entrada dels tres blocs
|
||||||
flash_ms: 250 # durada del flash quan un jugador rep un hit
|
flash_ms: 250 # durada del flash quan un jugador rep un hit
|
||||||
shake_ms: 300 # durada del shake que acompanya el flash
|
shake_ms: 300 # durada del shake que acompanya el flash
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "core/audio/audio_adapter.hpp" // Per a AudioResource::getMusic/getSound
|
#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/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
|
#include "utils/defaults.hpp" // Per a Defaults::Audio::FREQUENCY
|
||||||
|
|
||||||
// Invariant compile-time: tots els valors d'Audio::Group han de cabre als slots
|
// Invariant compile-time: tots els valors d'Audio::Group han de cabre als slots
|
||||||
@@ -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
|
// Atura tots els sons
|
||||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
||||||
void Audio::stopAllSounds() {
|
void Audio::stopAllSounds() {
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ class Audio {
|
|||||||
// --- Control de sons ---
|
// --- Control de sons ---
|
||||||
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
|
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 playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (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)
|
void stopAllSounds(); // Aturar tots els sons (muta globals de Ja)
|
||||||
|
|
||||||
// --- Control de volum (API interna: float 0.0..1.0) ---
|
// --- Control de volum (API interna: float 0.0..1.0) ---
|
||||||
|
|||||||
@@ -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 <cassert>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <memory>
|
#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
|
// 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
|
// d'implementació. Les definicions les aporta source/external/stb_vorbis_impl.cpp
|
||||||
@@ -397,13 +401,17 @@ namespace Ja {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Engine::stopChannel(const int channel) {
|
void Engine::stopChannel(const int channel) {
|
||||||
forEachTargetChannel(channel, [](Channel& ch) {
|
forEachTargetChannel(channel, [this](Channel& ch) {
|
||||||
if (ch.state != ChannelState::FREE) {
|
if (ch.state != ChannelState::FREE) {
|
||||||
if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
|
if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
|
||||||
ch.stream = nullptr;
|
ch.stream = nullptr;
|
||||||
ch.state = ChannelState::FREE;
|
ch.state = ChannelState::FREE;
|
||||||
ch.pos = 0;
|
ch.pos = 0;
|
||||||
ch.sound = nullptr;
|
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) ---
|
// --- Factories i destructors (permanents) ---
|
||||||
|
|
||||||
auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
|
auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
|
||||||
|
|||||||
@@ -48,6 +48,26 @@ namespace Ja {
|
|||||||
// --- Constants ---
|
// --- Constants ---
|
||||||
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
|
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
|
||||||
inline constexpr int MAX_GROUPS = 2;
|
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
|
// 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
|
// 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 times{0};
|
||||||
int group{0};
|
int group{0};
|
||||||
ChannelState state{ChannelState::FREE};
|
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 {
|
struct Music {
|
||||||
@@ -147,6 +171,12 @@ namespace Ja {
|
|||||||
// --- So ---
|
// --- So ---
|
||||||
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
|
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
|
||||||
auto playSoundOnChannel(Sound* sound, int channel, 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 pauseChannel(int channel);
|
||||||
void resumeChannel(int channel);
|
void resumeChannel(int channel);
|
||||||
void stopChannel(int channel);
|
void stopChannel(int channel);
|
||||||
@@ -161,6 +191,10 @@ namespace Ja {
|
|||||||
void updateIncomingFade();
|
void updateIncomingFade();
|
||||||
void updateCurrentMusic();
|
void updateCurrentMusic();
|
||||||
void updateSoundChannels();
|
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>
|
template <typename Fn>
|
||||||
void forEachTargetChannel(int channel, Fn&& fn);
|
void forEachTargetChannel(int channel, Fn&& fn);
|
||||||
@@ -174,6 +208,9 @@ namespace Ja {
|
|||||||
OutgoingMusic outgoing_music_;
|
OutgoingMusic outgoing_music_;
|
||||||
FadeState incoming_fade_;
|
FadeState incoming_fade_;
|
||||||
std::function<void()> on_music_ended_;
|
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 _
|
// NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static amb sufix _
|
||||||
static Engine* active_;
|
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 setAccelY(float value) { ay_ = value; }
|
||||||
|
|
||||||
void setFlip(SDL_FlipMode flip) { flip_ = flip; }
|
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; }
|
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
|
// Moviment eix-separat amb col·lisió: aplica `dx` (i si la nova
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
|
#include "core/audio/sound_effects_config.hpp"
|
||||||
#include "core/input/gamepad_manager.hpp"
|
#include "core/input/gamepad_manager.hpp"
|
||||||
#include "core/input/key_config.hpp"
|
#include "core/input/key_config.hpp"
|
||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
#include "core/system/event_buffer.hpp"
|
#include "core/system/event_buffer.hpp"
|
||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
#include "game/debug_config.hpp"
|
#include "game/debug_config.hpp"
|
||||||
|
#include "game/levels/level_manager.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
#include "game/scenes/game_scene.hpp"
|
#include "game/scenes/game_scene.hpp"
|
||||||
#include "game/scenes/intro_scene.hpp"
|
#include "game/scenes/intro_scene.hpp"
|
||||||
@@ -121,11 +123,24 @@ Director::Director() {
|
|||||||
const std::string PLACEHOLDER_FONT_GIF_KEY = UiConfig::get().placeholderFont().name + ".gif";
|
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().loadSurface(PLACEHOLDER_FONT_GIF_KEY, UiConfig::get().placeholderFont().gif);
|
||||||
Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
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í
|
// 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.
|
// 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");
|
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
|
// Audio: singleton manual propietari d'un SDL audio device. Es destrueix
|
||||||
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els
|
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els
|
||||||
// JA_Music_t que continga, així que no queden streams orfes.
|
// JA_Music_t que continga, així que no queden streams orfes.
|
||||||
@@ -182,6 +197,13 @@ Director::Director() {
|
|||||||
DebugConfig::createDefaultIfMissing(DEBUG_YAML);
|
DebugConfig::createDefaultIfMissing(DEBUG_YAML);
|
||||||
const auto START = DebugConfig::resolveStartScene(DEBUG_YAML);
|
const auto START = DebugConfig::resolveStartScene(DEBUG_YAML);
|
||||||
const auto STARTING_PLAYERS = DebugConfig::resolveStartingPlayers(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
|
#else
|
||||||
const auto START = DebugConfig::StartScene::LOGO;
|
const auto START = DebugConfig::StartScene::LOGO;
|
||||||
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
|
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
|
||||||
@@ -189,6 +211,12 @@ Director::Director() {
|
|||||||
std::cout << "Director: start_scene=" << DebugConfig::toString(START)
|
std::cout << "Director: start_scene=" << DebugConfig::toString(START)
|
||||||
<< " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n';
|
<< " 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 make_game = [STARTING_PLAYERS]() -> std::unique_ptr<Scene> {
|
||||||
auto cfg = GameScene::defaultConfig();
|
auto cfg = GameScene::defaultConfig();
|
||||||
cfg.starting_players = STARTING_PLAYERS;
|
cfg.starting_players = STARTING_PLAYERS;
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ UiConfig::UiConfig()
|
|||||||
hud_label_(Defaults::UI::HUD_LABEL),
|
hud_label_(Defaults::UI::HUD_LABEL),
|
||||||
hud_value_(Defaults::UI::HUD_VALUE),
|
hud_value_(Defaults::UI::HUD_VALUE),
|
||||||
hud_flash_(Defaults::UI::HUD_FLASH),
|
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),
|
notification_ms_(Defaults::UI::NOTIFICATION_TIME_MS),
|
||||||
exit_ms_(Defaults::UI::EXIT_TIME_MS),
|
exit_ms_(Defaults::UI::EXIT_TIME_MS),
|
||||||
hud_entry_ms_(Defaults::UI::HUD_ENTRY_MS),
|
hud_entry_ms_(Defaults::UI::HUD_ENTRY_MS),
|
||||||
@@ -96,6 +98,7 @@ UiConfig::UiConfig()
|
|||||||
pause_exit_curve_(curveByName(Defaults::UI::PAUSE_EXIT_CURVE)),
|
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},
|
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},
|
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_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} {
|
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_label", hud_label_);
|
||||||
readColorIndex(c, "hud_value", hud_value_);
|
readColorIndex(c, "hud_value", hud_value_);
|
||||||
readColorIndex(c, "hud_flash", hud_flash_);
|
readColorIndex(c, "hud_flash", hud_flash_);
|
||||||
|
readColorIndex(c, "room_text", room_text_);
|
||||||
|
readColorIndex(c, "room_text_outline", room_text_outline_);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (yaml.contains("timings")) {
|
if (yaml.contains("timings")) {
|
||||||
@@ -159,6 +164,7 @@ void UiConfig::load(const std::string& file_path) {
|
|||||||
const auto& f = yaml["fonts"];
|
const auto& f = yaml["fonts"];
|
||||||
readFont(f, "hud", hud_font_);
|
readFont(f, "hud", hud_font_);
|
||||||
readFont(f, "placeholder", placeholder_font_);
|
readFont(f, "placeholder", placeholder_font_);
|
||||||
|
readFont(f, "room_text", room_text_font_);
|
||||||
readFont(f, "menu_title", menu_title_font_);
|
readFont(f, "menu_title", menu_title_font_);
|
||||||
readFont(f, "menu_items", menu_items_font_);
|
readFont(f, "menu_items", menu_items_font_);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ class UiConfig {
|
|||||||
[[nodiscard]] auto hudLabel() const -> Uint8 { return hud_label_; }
|
[[nodiscard]] auto hudLabel() const -> Uint8 { return hud_label_; }
|
||||||
[[nodiscard]] auto hudValue() const -> Uint8 { return hud_value_; }
|
[[nodiscard]] auto hudValue() const -> Uint8 { return hud_value_; }
|
||||||
[[nodiscard]] auto hudFlash() const -> Uint8 { return hud_flash_; }
|
[[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].
|
// 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
|
// Permet triar corbes des de data/ui.yaml per nom (cadena) i guardar
|
||||||
@@ -77,6 +79,7 @@ class UiConfig {
|
|||||||
// Fonts de UI.
|
// Fonts de UI.
|
||||||
[[nodiscard]] auto hudFont() const -> const Font& { return hud_font_; }
|
[[nodiscard]] auto hudFont() const -> const Font& { return hud_font_; }
|
||||||
[[nodiscard]] auto placeholderFont() const -> const Font& { return placeholder_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 menuTitleFont() const -> const Font& { return menu_title_font_; }
|
||||||
[[nodiscard]] auto menuItemsFont() const -> const Font& { return menu_items_font_; }
|
[[nodiscard]] auto menuItemsFont() const -> const Font& { return menu_items_font_; }
|
||||||
|
|
||||||
@@ -105,6 +108,8 @@ class UiConfig {
|
|||||||
Uint8 hud_label_;
|
Uint8 hud_label_;
|
||||||
Uint8 hud_value_;
|
Uint8 hud_value_;
|
||||||
Uint8 hud_flash_;
|
Uint8 hud_flash_;
|
||||||
|
Uint8 room_text_;
|
||||||
|
Uint8 room_text_outline_;
|
||||||
|
|
||||||
int notification_ms_;
|
int notification_ms_;
|
||||||
int exit_ms_;
|
int exit_ms_;
|
||||||
@@ -118,6 +123,7 @@ class UiConfig {
|
|||||||
|
|
||||||
Font hud_font_;
|
Font hud_font_;
|
||||||
Font placeholder_font_;
|
Font placeholder_font_;
|
||||||
|
Font room_text_font_;
|
||||||
Font menu_title_font_;
|
Font menu_title_font_;
|
||||||
Font menu_items_font_;
|
Font menu_items_font_;
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,34 @@ namespace DebugConfig {
|
|||||||
return parseStartingPlayers(CONTENT);
|
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* {
|
auto toString(StartScene scene) -> const char* {
|
||||||
switch (scene) {
|
switch (scene) {
|
||||||
case StartScene::LOGO:
|
case StartScene::LOGO:
|
||||||
@@ -140,6 +168,9 @@ namespace DebugConfig {
|
|||||||
file << "# debug.yaml — ajust personal de dev (no versionat).\n";
|
file << "# debug.yaml — ajust personal de dev (no versionat).\n";
|
||||||
file << "# start_scene: LOGO | TITLE | INTRO | GAME\n";
|
file << "# start_scene: LOGO | TITLE | INTRO | GAME\n";
|
||||||
file << "# starting_players: P1 | P2 | BOTH\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 << "start_scene: GAME\n";
|
||||||
file << "starting_players: BOTH\n";
|
file << "starting_players: BOTH\n";
|
||||||
std::cout << "debug.yaml: creat a " << file_path << " amb start_scene=GAME 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
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
// Configuració personal de desenvolupament, carregada de `debug.yaml` dins la
|
// 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 parseStartingPlayers(const std::string& yaml_content) -> StartingPlayers;
|
||||||
[[nodiscard]] auto resolveStartingPlayers(const std::string& file_path) -> 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".
|
// Nom canònic de l'enum (útil per a logs): "LOGO" / "TITLE" / "INTRO" / "GAME".
|
||||||
[[nodiscard]] auto toString(StartScene scene) -> const char*;
|
[[nodiscard]] auto toString(StartScene scene) -> const char*;
|
||||||
[[nodiscard]] auto toString(StartingPlayers players) -> const char*;
|
[[nodiscard]] auto toString(StartingPlayers players) -> const char*;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "core/system/event_buffer.hpp"
|
#include "core/system/event_buffer.hpp"
|
||||||
|
#include "game/levels/level_manager.hpp"
|
||||||
#include "game/map/tilemap.hpp"
|
#include "game/map/tilemap.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
#include "utils/defines.hpp"
|
#include "utils/defines.hpp"
|
||||||
@@ -76,11 +77,13 @@ void Editor::exit(Tilemap& tilemap) {
|
|||||||
if (!active_) { return; }
|
if (!active_) { return; }
|
||||||
active_ = false;
|
active_ = false;
|
||||||
picker_open_ = false;
|
picker_open_ = false;
|
||||||
// Resolem la ruta via Resource::Helper perquè en Debug el root de
|
// El writer salva sempre sobre la sala actualment carregada (segons
|
||||||
// dades és AEE_SOURCE_DIR (ruta absoluta al repo), no el CWD. Sense
|
// el LevelManager): així el "9" de l'editor desa el que tens davant,
|
||||||
// això, el writer obriria `data/maps/pyramid.yaml` relatiu al CWD
|
// no una constant fixa. Resolem la ruta via Resource::Helper perquè
|
||||||
// i fallaria sempre excepte si el binari es llança des del repo root.
|
// en Debug el root de dades és AEE_SOURCE_DIR (ruta absoluta al repo),
|
||||||
const auto ABS_PATH = Resource::Helper::resolve(Defaults::Editor::SAVE_PATH);
|
// 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);
|
const bool SAVED = tilemap.save(ABS_PATH);
|
||||||
Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error"));
|
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};
|
||||||
|
};
|
||||||
@@ -104,6 +104,22 @@ Mummy::Mummy(const fkyaml::node& yaml)
|
|||||||
} else if (initial == "down") {
|
} else if (initial == "down") {
|
||||||
changeDirection(0.0F, 1.0F);
|
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) {
|
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)
|
REGISTER_ENTITY("mummy", Mummy)
|
||||||
@@ -21,6 +21,21 @@ class Mummy : public DynamicHazard {
|
|||||||
LEFT,
|
LEFT,
|
||||||
RIGHT };
|
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);
|
explicit Mummy(const fkyaml::node& yaml);
|
||||||
|
|
||||||
// Hook d'IA. Donat un conjunt de direccions admissibles, decideix la
|
// 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.
|
// En ambdós casos el frame de gir no consumeix avenç extra.
|
||||||
void step(float delta_time) override;
|
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.
|
// Contacte amb el jugador: publica `player_hit` amb el dany.
|
||||||
void onPlayerContact(Player* player) override;
|
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:
|
private:
|
||||||
// Caixa de col·lisió candidata si l'entitat es desplaçara (dx, dy).
|
// Caixa de col·lisió candidata si l'entitat es desplaçara (dx, dy).
|
||||||
[[nodiscard]] auto candidateBox(float dx, float dy) const -> SDL_FRect;
|
[[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.
|
// S'usa al pool de creuament per afegir la recta com a candidata.
|
||||||
[[nodiscard]] auto currentDirection() const -> Direction;
|
[[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
|
// Animacions carregades del YAML, una per cada facing. Per a horitzontal
|
||||||
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta.
|
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta.
|
||||||
AnimationComponent anim_walk_down_;
|
AnimationComponent anim_walk_down_;
|
||||||
AnimationComponent anim_walk_up_;
|
AnimationComponent anim_walk_up_;
|
||||||
AnimationComponent anim_walk_lateral_;
|
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
|
// 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
|
// Z-order per ordenar entitats i jugador de menys a més profunditat
|
||||||
// (REND-01, quan s'implemente).
|
// (REND-01, quan s'implemente). Virtual perquè entitats efímeres
|
||||||
[[nodiscard]] auto getFeetY() const -> float;
|
// (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
|
// El loader li assigna una caixa explícita llegida del YAML; sense
|
||||||
// aquesta crida, l'entitat queda sense `CollisionBox` i la caixa de
|
// aquesta crida, l'entitat queda sense `CollisionBox` i la caixa de
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ void EntityManager::setExpectedPlayerCount(int n) {
|
|||||||
level_complete_emitted_ = false;
|
level_complete_emitted_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EntityManager::forceOpenDoor() {
|
||||||
|
if (door_ != nullptr) { door_->open(); }
|
||||||
|
}
|
||||||
|
|
||||||
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
||||||
if (entity == nullptr) { return nullptr; }
|
if (entity == nullptr) { return nullptr; }
|
||||||
entity->setEventBus(&bus_);
|
entity->setEventBus(&bus_);
|
||||||
|
|||||||
@@ -112,6 +112,13 @@ class EntityManager {
|
|||||||
[[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; }
|
[[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; }
|
||||||
[[nodiscard]] auto exitedPlayerCount() const -> std::size_t { return exited_players_; }
|
[[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:
|
private:
|
||||||
EventBus bus_;
|
EventBus bus_;
|
||||||
std::vector<std::unique_ptr<Entity>> entities_;
|
std::vector<std::unique_ptr<Entity>> entities_;
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
|
|||||||
const auto BOX = entity->getCollisionRect();
|
const auto BOX = entity->getCollisionRect();
|
||||||
const int E_HEIGHT = entity->getHeight();
|
const int E_HEIGHT = entity->getHeight();
|
||||||
for (auto* p : players) {
|
for (auto* p : players) {
|
||||||
if (p == nullptr || p->isExited()) { continue; }
|
if (p == nullptr || p->isOutOfPlay()) { continue; }
|
||||||
// Filtre per altura: si el jugador està en l'aire i està per
|
// 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.
|
// damunt de l'alçada del cos de l'entitat, no hi ha contacte.
|
||||||
// És el mecanisme del salt per esquivar momies, roques, etc.
|
// És el mecanisme del salt per esquivar momies, roques, etc.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@@ -12,6 +14,7 @@
|
|||||||
// Forward: els events poden dur un punter a `Entity` (p. ex. quin sarcòfag
|
// Forward: els events poden dur un punter a `Entity` (p. ex. quin sarcòfag
|
||||||
// s'ha obert). No cal incloure el header complet aquí.
|
// s'ha obert). No cal incloure el header complet aquí.
|
||||||
class Entity;
|
class Entity;
|
||||||
|
class Player;
|
||||||
|
|
||||||
// Event pub/sub minimalista. Hi ha un únic `EventBus` per mundus (propietat
|
// Event pub/sub minimalista. Hi ha un únic `EventBus` per mundus (propietat
|
||||||
// de l'`EntityManager`); les entitats que el necessiten en reben un punter
|
// de l'`EntityManager`); les entitats que el necessiten en reben un punter
|
||||||
@@ -49,9 +52,31 @@ struct WorldPos {
|
|||||||
float y{0.0F};
|
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 {
|
struct Event {
|
||||||
std::string name;
|
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;
|
Payload payload;
|
||||||
// Jugador que ha originat (o provocat) l'event. -1 = sense jugador
|
// Jugador que ha originat (o provocat) l'event. -1 = sense jugador
|
||||||
// associat (events del món, com `sarcophagus_opened` des de la pròpia
|
// 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) {
|
void publish(const std::string& name, WorldPos pos) {
|
||||||
publish(Event{.name = name, .payload = pos});
|
publish(Event{.name = name, .payload = pos});
|
||||||
}
|
}
|
||||||
|
void publish(const std::string& name, const WhipSpawnPayload& whip) {
|
||||||
|
publish(Event{.name = name, .payload = whip});
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Subscription {
|
struct Subscription {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ Player::TurnConfig Player::turn_config;
|
|||||||
Player::InertiaConfig Player::inertia_config;
|
Player::InertiaConfig Player::inertia_config;
|
||||||
Player::JumpConfig Player::jump_config;
|
Player::JumpConfig Player::jump_config;
|
||||||
Player::HurtConfig Player::hurt_config;
|
Player::HurtConfig Player::hurt_config;
|
||||||
|
Player::WhipConfig Player::whip_config;
|
||||||
|
|
||||||
void Player::loadFrom(const std::string& yaml_path) {
|
void Player::loadFrom(const std::string& yaml_path) {
|
||||||
auto bytes = Resource::Helper::loadFile(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
|
// Bloc opcional `hurt:` — estat post-impacte. Si manca o el speed
|
||||||
// és 0, l'estat HURT mai s'activa (cas degenerat).
|
// és 0, l'estat HURT mai s'activa (cas degenerat).
|
||||||
readHurtBlock(yaml);
|
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) {
|
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>(); }
|
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) {
|
void Player::applyCardinal(Cardinal c) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case Cardinal::NORTH:
|
case Cardinal::NORTH:
|
||||||
@@ -328,10 +344,10 @@ void Player::update(float delta_time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
||||||
// EXITED: jugador despawnat. No xoca amb res ni res xoca amb ell.
|
// EXITED/DISABLED: jugador fora del joc. No xoca amb res ni res
|
||||||
// També salta jugadors EXITED del check actor↔actor més avall via
|
// xoca amb ell. També es filtren els altres jugadors fora de joc
|
||||||
// el filtre de `isExited` (vegeu el predicat any_of).
|
// al check actor↔actor més avall via el predicat `isOutOfPlay`.
|
||||||
if (state_ == State::EXITED) { return false; }
|
if (isOutOfPlay()) { return false; }
|
||||||
if (map_.collides(box)) { return true; }
|
if (map_.collides(box)) { return true; }
|
||||||
if (all_players_ == nullptr) { return false; }
|
if (all_players_ == nullptr) { return false; }
|
||||||
// Per al contacte actor-actor usem la hit-box petita a banda i banda:
|
// Per al contacte actor-actor usem la hit-box petita a banda i banda:
|
||||||
@@ -357,7 +373,7 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
|||||||
const float MY_Z = jump_z_;
|
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) {
|
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 == nullptr || p == this) { return false; }
|
||||||
if (p->isExited()) { return false; }
|
if (p->isOutOfPlay()) { return false; }
|
||||||
if (MY_HURT || p->isHurt()) { return false; }
|
if (MY_HURT || p->isHurt()) { return false; }
|
||||||
if (MY_AIR != p->isAirborne()) { return false; }
|
if (MY_AIR != p->isAirborne()) { return false; }
|
||||||
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
|
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
|
||||||
@@ -407,10 +423,11 @@ void Player::integrateAxis(float& phys, float input, float delta_time, float mot
|
|||||||
// llegibilitat. La complexitat puja 1 per la guarda EXITED.
|
// llegibilitat. La complexitat puja 1 per la guarda EXITED.
|
||||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
||||||
void Player::tick(float delta_time) {
|
void Player::tick(float delta_time) {
|
||||||
// EXITED: jugador despawnat, no fa res. Cap input, cap física, cap
|
// EXITED/DISABLED: jugador fora del joc, no fa res. Cap input, cap
|
||||||
// bus event. Continua sent al vector de jugadors per simplicitat
|
// física, cap bus event. Continua sent al vector de jugadors per
|
||||||
// (no s'esborra) però queda completament inert fins al respawn.
|
// simplicitat (no s'esborra) però queda completament inert fins
|
||||||
if (state_ == State::EXITED) { return; }
|
// que GameScene el reactiva (respawn / spawnFromAir).
|
||||||
|
if (isOutOfPlay()) { return; }
|
||||||
|
|
||||||
// Reset per frame: el resolver del GameScene el tornarà a posar a
|
// 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
|
// true si aquest frame hi ha push actiu. Així quan el push acaba
|
||||||
@@ -439,6 +456,7 @@ void Player::tick(float delta_time) {
|
|||||||
// durant el stun: així si l'usuari l'ha estat prement abans, no
|
// durant el stun: així si l'usuari l'ha estat prement abans, no
|
||||||
// se'l consumeix com a salt nou en sortir del stun.
|
// se'l consumeix com a salt nou en sortir del stun.
|
||||||
(void)consumeJumpEdge();
|
(void)consumeJumpEdge();
|
||||||
|
(void)consumeAttackEdge();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,11 +466,36 @@ void Player::tick(float delta_time) {
|
|||||||
// pulsació quan torne a terra es detecte com a flanc nou.
|
// pulsació quan torne a terra es detecte com a flanc nou.
|
||||||
if (state_ == State::AIRBORNE) {
|
if (state_ == State::AIRBORNE) {
|
||||||
(void)consumeJumpEdge();
|
(void)consumeJumpEdge();
|
||||||
|
(void)consumeAttackEdge();
|
||||||
setVelX(intended_vx_);
|
setVelX(intended_vx_);
|
||||||
setVelY(intended_vy_);
|
setVelY(intended_vy_);
|
||||||
return;
|
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
|
// En HURT el jugador està projectat sense control: descomptem
|
||||||
// velocitat per inèrcia (decaiment lineal fins a 0). Quan arriba a
|
// 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.
|
// zero, tornem a GROUNDED i el frame següent ja torna a llegir input.
|
||||||
@@ -460,6 +503,7 @@ void Player::tick(float delta_time) {
|
|||||||
// estats), que també gestiona el xoc contra murs.
|
// estats), que també gestiona el xoc contra murs.
|
||||||
if (state_ == State::HURT) {
|
if (state_ == State::HURT) {
|
||||||
(void)consumeJumpEdge();
|
(void)consumeJumpEdge();
|
||||||
|
(void)consumeAttackEdge();
|
||||||
const float DECEL = (hurt_config.brake_time_ms > 0)
|
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)
|
? hurt_config.initial_speed_px_per_s / (static_cast<float>(hurt_config.brake_time_ms) / 1000.0F)
|
||||||
: 0.0F;
|
: 0.0F;
|
||||||
@@ -534,6 +578,18 @@ void Player::tick(float delta_time) {
|
|||||||
// només disparen amb intenció explícita del jugador.
|
// només disparen amb intenció explícita del jugador.
|
||||||
updateFacingAndTurn(delta_time, input_vx_, input_vy_);
|
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
|
// 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
|
// 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
|
// despegue. `being_pushed_` no ho pot bloquejar ací (encara no s'ha
|
||||||
@@ -564,6 +620,81 @@ auto Player::consumeJumpEdge() -> bool {
|
|||||||
return edge;
|
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() {
|
void Player::startJump() {
|
||||||
// No saltem si l'estat no ho permet. `being_pushed_` no és segur de
|
// 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),
|
// consultar ací (es pot haver marcat o no, segons l'ordre del frame),
|
||||||
@@ -696,7 +827,15 @@ auto Player::integrateJump(float delta_time) -> bool {
|
|||||||
state_ = State::LANDING_STUN;
|
state_ = State::LANDING_STUN;
|
||||||
landing_stun_remaining_ms_ = static_cast<float>(jump_config.landing_stun_ms);
|
landing_stun_remaining_ms_ = static_cast<float>(jump_config.landing_stun_ms);
|
||||||
if (!jump_config.land_sound_key.empty() && Audio::get() != nullptr) {
|
if (!jump_config.land_sound_key.empty() && Audio::get() != nullptr) {
|
||||||
|
// 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);
|
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) {
|
if (bus_ != nullptr) {
|
||||||
bus_->publish(Event{
|
bus_->publish(Event{
|
||||||
@@ -719,6 +858,9 @@ void Player::resolveLandingOverlap() {
|
|||||||
if (all_players_ == nullptr) { return; }
|
if (all_players_ == nullptr) { return; }
|
||||||
for (Player* other : *all_players_) {
|
for (Player* other : *all_players_) {
|
||||||
if (other == nullptr || other == this) { continue; }
|
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.
|
// 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:
|
// Coherent amb la resta de col·lisions actor-actor del joc:
|
||||||
// momies, fuetades futures, resolver PvP… tots usen hit-box. Un
|
// momies, fuetades futures, resolver PvP… tots usen hit-box. Un
|
||||||
@@ -753,8 +895,8 @@ void Player::resolveLandingOverlap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Player::applyMovement(float delta_time) {
|
void Player::applyMovement(float delta_time) {
|
||||||
// EXITED: cap moviment ni col·lisió. Idèntic al gating de `tick`.
|
// EXITED/DISABLED: cap moviment ni col·lisió. Idèntic al gating de `tick`.
|
||||||
if (state_ == State::EXITED) { return; }
|
if (isOutOfPlay()) { return; }
|
||||||
|
|
||||||
// Integració del salt: si estem AIRBORNE, avança jump_z_/jump_vz_.
|
// 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
|
// El facing i intended_v* es mantenen del moment del despegue (no es
|
||||||
@@ -776,6 +918,15 @@ void Player::applyMovement(float delta_time) {
|
|||||||
return;
|
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_`
|
// El resolver del GameScene pot haver escalat `intended_vx_/vy_`
|
||||||
// (cas push: velocitat * push_factor) o haver-les anul·lat (cas
|
// (cas push: velocitat * push_factor) o haver-les anul·lat (cas
|
||||||
// head-on). Consumim-les aquí com a velocitat definitiva del frame.
|
// head-on). Consumim-les aquí com a velocitat definitiva del frame.
|
||||||
@@ -875,9 +1026,10 @@ void Player::publishTileEnteredIfChanged() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Player::render() {
|
void Player::render() {
|
||||||
// EXITED: invisible. La crida al render base es salta sencera; el
|
// EXITED/DISABLED: invisible. La crida al render base es salta
|
||||||
// jugador no apareix al z-sort tot i seguir vivint al vector.
|
// sencera; el jugador no apareix al z-sort tot i seguir vivint al
|
||||||
if (state_ == State::EXITED) { return; }
|
// vector.
|
||||||
|
if (isOutOfPlay()) { return; }
|
||||||
|
|
||||||
// Mentre el jugador està enlaire, el sprite es desplaça visualment
|
// Mentre el jugador està enlaire, el sprite es desplaça visualment
|
||||||
// cap amunt `jump_z_` píxels. Modifiquem `pos_.y` (la posició entera
|
// cap amunt `jump_z_` píxels. Modifiquem `pos_.y` (la posició entera
|
||||||
@@ -976,6 +1128,37 @@ auto Player::animationName(bool moving) const -> const char* {
|
|||||||
}
|
}
|
||||||
return "hurt";
|
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_) {
|
switch (facing_) {
|
||||||
case Facing::UP:
|
case Facing::UP:
|
||||||
return moving ? "walk_up" : "stand_up";
|
return moving ? "walk_up" : "stand_up";
|
||||||
@@ -1054,6 +1237,54 @@ void Player::respawn(float x, float y) {
|
|||||||
setPosY(y);
|
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 {
|
auto Player::getBaseBox() const -> SDL_FRect {
|
||||||
return {
|
return {
|
||||||
.x = getPosX() + static_cast<float>(collision_x_offset),
|
.x = getPosX() + static_cast<float>(collision_x_offset),
|
||||||
@@ -1144,6 +1375,7 @@ auto Player::tryPushSnapBlockers(
|
|||||||
const SDL_FRect MY_HIT = getHitBox();
|
const SDL_FRect MY_HIT = getHitBox();
|
||||||
for (Player* p : *all_players_) {
|
for (Player* p : *all_players_) {
|
||||||
if (p == nullptr || p == this) { continue; }
|
if (p == nullptr || p == this) { continue; }
|
||||||
|
if (p->isOutOfPlay()) { continue; }
|
||||||
const SDL_FRect OTHER_HIT = p->getHitBox();
|
const SDL_FRect OTHER_HIT = p->getHitBox();
|
||||||
if (SDL_HasRectIntersectionFloat(&MY_HIT, &OTHER_HIT)) {
|
if (SDL_HasRectIntersectionFloat(&MY_HIT, &OTHER_HIT)) {
|
||||||
blockers.push_back(p);
|
blockers.push_back(p);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class Player : public AnimatedSprite {
|
|||||||
SDL_Scancode left;
|
SDL_Scancode left;
|
||||||
SDL_Scancode right;
|
SDL_Scancode right;
|
||||||
SDL_Scancode jump;
|
SDL_Scancode jump;
|
||||||
|
SDL_Scancode attack;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Estat d'alt nivell del jugador. Ortogonal a `Facing`/`Cardinal`
|
// Estat d'alt nivell del jugador. Ortogonal a `Facing`/`Cardinal`
|
||||||
@@ -44,12 +45,29 @@ class Player : public AnimatedSprite {
|
|||||||
AIRBORNE,
|
AIRBORNE,
|
||||||
LANDING_STUN,
|
LANDING_STUN,
|
||||||
HURT,
|
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:
|
// 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
|
// no es renderitza, no processa input, no es mou, no col·lisiona
|
||||||
// amb res ni rep contacte d'entitats. Idempotent: les crides
|
// amb res ni rep contacte d'entitats. Idempotent: les crides
|
||||||
// posteriors a `exit()` són no-op. El reset el porta `respawn()`
|
// posteriors a `exit()` són no-op. El reset el porta `respawn()`
|
||||||
// (cridada per GameScene en transició OUTRO → INTRO).
|
// (cridada per GameScene en transició OUTRO → INTRO).
|
||||||
EXITED,
|
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`).
|
// Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`).
|
||||||
@@ -184,6 +202,15 @@ class Player : public AnimatedSprite {
|
|||||||
[[nodiscard]] auto isAirborne() const -> bool { return state_ == State::AIRBORNE; }
|
[[nodiscard]] auto isAirborne() const -> bool { return state_ == State::AIRBORNE; }
|
||||||
[[nodiscard]] auto isHurt() const -> bool { return state_ == State::HURT; }
|
[[nodiscard]] auto isHurt() const -> bool { return state_ == State::HURT; }
|
||||||
[[nodiscard]] auto isExited() const -> bool { return state_ == State::EXITED; }
|
[[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
|
// Tangible per a contactes jugador↔jugador: només els estats a terra
|
||||||
// amb cos sòlid. AIRBORNE i HURT són intangibles (es travessen).
|
// 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.
|
// EXITED tampoc — el jugador ha eixit lògicament i ja no és al món.
|
||||||
@@ -216,9 +243,41 @@ class Player : public AnimatedSprite {
|
|||||||
// Reset complet a l'estat inicial: posició a (x, y), velocitats a 0,
|
// Reset complet a l'estat inicial: posició a (x, y), velocitats a 0,
|
||||||
// estat GROUNDED, qualsevol flag de control reinicialitzat. Cridat
|
// estat GROUNDED, qualsevol flag de control reinicialitzat. Cridat
|
||||||
// per GameScene a la transició ROOM_OUTRO → ROOM_INTRO. Idempotent
|
// per GameScene a la transició ROOM_OUTRO → ROOM_INTRO. Idempotent
|
||||||
// (es pot cridar des de qualsevol estat).
|
// (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);
|
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).
|
// 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
|
// 0 quan està a terra. L'usa el render i el sistema de col·lisions
|
||||||
// per altura.
|
// per altura.
|
||||||
@@ -350,6 +409,14 @@ class Player : public AnimatedSprite {
|
|||||||
// gestiona `intended_vx_/vy_` escalats pel resolver.
|
// gestiona `intended_vx_/vy_` escalats pel resolver.
|
||||||
bool being_pushed_{false};
|
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
|
// Escala d'empenta usada per `snapToLane` quan descobreix un blocker
|
||||||
// (mateixa del resolver). 1.0 = sense escalar (comportament si
|
// (mateixa del resolver). 1.0 = sense escalar (comportament si
|
||||||
// GameScene no ha cridat setPushFactor).
|
// GameScene no ha cridat setPushFactor).
|
||||||
@@ -374,12 +441,16 @@ class Player : public AnimatedSprite {
|
|||||||
// Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°.
|
// 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
|
// El parell (Facing, flip) només té 3+1 estats; aquesta enum n'afegeix
|
||||||
// la 4a sense reorganitzar el sistema d'animacions.
|
// 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,
|
enum class Cardinal : std::uint8_t { NONE,
|
||||||
NORTH,
|
NORTH,
|
||||||
SOUTH,
|
SOUTH,
|
||||||
EAST,
|
EAST,
|
||||||
WEST };
|
WEST };
|
||||||
|
|
||||||
|
private:
|
||||||
// Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb
|
// Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb
|
||||||
// `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i
|
// `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i
|
||||||
// el comportament és instantani: soltar input para el jugador en sec.
|
// el comportament és instantani: soltar input para el jugador en sec.
|
||||||
@@ -446,6 +517,37 @@ class Player : public AnimatedSprite {
|
|||||||
// Llig el bloc `hurt:` del YAML i n'omple `hurt_config`.
|
// Llig el bloc `hurt:` del YAML i n'omple `hurt_config`.
|
||||||
static void readHurtBlock(const fkyaml::node& yaml);
|
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
|
// Activa l'estat HURT: el jugador queda projectat amb velocitat
|
||||||
// inicial `hurt_config.initial_speed_px_per_s`. La direcció es tria
|
// inicial `hurt_config.initial_speed_px_per_s`. La direcció es tria
|
||||||
// així:
|
// així:
|
||||||
@@ -456,8 +558,12 @@ class Player : public AnimatedSprite {
|
|||||||
// i tria el que dóna més recorregut lliure.
|
// i tria el que dóna més recorregut lliure.
|
||||||
// Decelera linealment fins a parar i, en eixe instant, torna a
|
// Decelera linealment fins a parar i, en eixe instant, torna a
|
||||||
// GROUNDED. Sense col·lisió amb altres jugadors mentre dura.
|
// 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);
|
void enterHurt(Cardinal preferred = Cardinal::NONE);
|
||||||
|
|
||||||
|
private:
|
||||||
// Tria la direcció definitiva del HURT. Si `preferred` cabe el
|
// Tria la direcció definitiva del HURT. Si `preferred` cabe el
|
||||||
// recorregut natural complet, la retorna; d'altra manera tria la
|
// recorregut natural complet, la retorna; d'altra manera tria la
|
||||||
// direcció amb més recorregut lliure entre els 4 cardinals.
|
// direcció amb més recorregut lliure entre els 4 cardinals.
|
||||||
@@ -492,10 +598,24 @@ class Player : public AnimatedSprite {
|
|||||||
bool jump_held_keyboard_{false};
|
bool jump_held_keyboard_{false};
|
||||||
bool jump_held_gamepad_{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,
|
// Llig el botó de salt aquest frame (teclat + mando assignat) i,
|
||||||
// amb edge-detection, retorna true si s'ha de despegar el salt.
|
// amb edge-detection, retorna true si s'ha de despegar el salt.
|
||||||
[[nodiscard]] auto consumeJumpEdge() -> bool;
|
[[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,
|
// Inicia un salt: copia `intended_v*` com a velocitat fixada del salt,
|
||||||
// posa `jump_vz_ = impulse`, transiciona a AIRBORNE.
|
// posa `jump_vz_ = impulse`, transiciona a AIRBORNE.
|
||||||
void startJump();
|
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
|
||||||
@@ -28,10 +28,13 @@
|
|||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "game/entities/concrete/dust.hpp"
|
#include "game/entities/concrete/dust.hpp"
|
||||||
|
#include "game/entities/concrete/whip_attack.hpp"
|
||||||
#include "game/entities/entity.hpp"
|
#include "game/entities/entity.hpp"
|
||||||
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/entity_loader.hpp"
|
#include "game/entities/entity_loader.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
#include "game/entities/player.hpp"
|
#include "game/entities/player.hpp"
|
||||||
|
#include "game/levels/level_manager.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
#include "game/ui/hud.hpp"
|
#include "game/ui/hud.hpp"
|
||||||
@@ -113,12 +116,12 @@ namespace {
|
|||||||
// surt projectat sense control i els dos es travessen. Coincideix
|
// surt projectat sense control i els dos es travessen. Coincideix
|
||||||
// amb la regla d'`Player::collidesWithWorld`.
|
// amb la regla d'`Player::collidesWithWorld`.
|
||||||
if (a->isHurt() || b->isHurt()) { return; }
|
if (a->isHurt() || b->isHurt()) { return; }
|
||||||
// EXITED: el jugador despawnat no és tangible. Si entrés al
|
// EXITED/DISABLED: jugadors fora del joc, no participen al resolver.
|
||||||
// resolver, la seua presència física frenaria l'altre (per
|
// 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
|
// overlap predictiu) tot i que `applyMovement` és no-op. Sortir
|
||||||
// d'aquí garanteix que el segon jugador pot creuar la porta
|
// d'aquí garanteix que el jugador actiu pot creuar la porta o
|
||||||
// sense interferència del primer ja eixit.
|
// moure's lliurement sense interferència d'un slot inactiu.
|
||||||
if (a->isExited() || b->isExited()) { return; }
|
if (a->isOutOfPlay() || b->isOutOfPlay()) { return; }
|
||||||
|
|
||||||
// Aire ↔ tierra: el saltador es travessa net del caminant. Sense
|
// Aire ↔ tierra: el saltador es travessa net del caminant. Sense
|
||||||
// resolver, sense head-on, sense push. Els dos continuen amb la
|
// resolver, sense head-on, sense push. Els dos continuen amb la
|
||||||
@@ -221,8 +224,11 @@ namespace {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto GameScene::defaultConfig() -> Config {
|
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 {
|
return {
|
||||||
.map_yaml = Defaults::Pyramid::MAP_YAML,
|
.map_yaml = Levels::LevelManager::get().currentPath(),
|
||||||
.player_sprite = Defaults::Pyramid::PLAYER_SPRITE,
|
.player_sprite = Defaults::Pyramid::PLAYER_SPRITE,
|
||||||
.player_yaml = Defaults::Pyramid::PLAYER_YAML,
|
.player_yaml = Defaults::Pyramid::PLAYER_YAML,
|
||||||
.player2_sprite = Defaults::Pyramid::PLAYER_SPRITE_P2,
|
.player2_sprite = Defaults::Pyramid::PLAYER_SPRITE_P2,
|
||||||
@@ -235,6 +241,12 @@ auto GameScene::defaultConfig() -> Config {
|
|||||||
GameScene::GameScene(Config config)
|
GameScene::GameScene(Config config)
|
||||||
: config_(std::move(config)),
|
: config_(std::move(config)),
|
||||||
tilemap_(makeTilemap(config_.map_yaml)) {
|
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è
|
// Convenció: el Resource::Cache indexa per basename del fitxer, perquè
|
||||||
// la mateixa clau l'usa el parseador d'animació (YAML > tileSetFile).
|
// la mateixa clau l'usa el parseador d'animació (YAML > tileSetFile).
|
||||||
// Lambda perquè tenim dos conjunts d'assets (sam/morcus) i l'alta al
|
// Lambda perquè tenim dos conjunts d'assets (sam/morcus) i l'alta al
|
||||||
@@ -250,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_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);
|
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;
|
// Carreguem assets per als DOS jugadors sempre, encara que algun no
|
||||||
std::string anim_p2;
|
// arranque actiu: l'slot existeix igualment al vector com a DISABLED
|
||||||
if (USE_P1) { anim_p1 = load_player_assets(config_.player_sprite, config_.player_yaml); }
|
// i podria reactivar-se amb un futur "join in progress". El cost de
|
||||||
if (USE_P2) { anim_p2 = load_player_assets(config_.player2_sprite, config_.player2_yaml); }
|
// 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
|
// 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
|
// del mateix YAML que l'animació. Ha d'estar feta abans d'instanciar
|
||||||
// Player perquè el spawn consulta `collision_*`. Els membres de Player
|
// Player perquè el spawn consulta `collision_*`. Els membres de Player
|
||||||
// són `static inline`, compartits per tots els jugadors: cridem
|
// són `static inline`, compartits per tots els jugadors; carregar des
|
||||||
// `loadFrom` amb el YAML del primer jugador actiu (P1 o P2). Els dos
|
// del YAML de P1 és suficient (els dos personatges comparteixen
|
||||||
// personatges usen la mateixa geometria (morcus és un repaletat de sam).
|
// geometria — morcus és un repaletat de sam).
|
||||||
Player::loadFrom(USE_P1 ? config_.player_yaml : config_.player2_yaml);
|
Player::loadFrom(config_.player_yaml);
|
||||||
|
|
||||||
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
||||||
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
||||||
@@ -299,6 +314,18 @@ GameScene::GameScene(Config config)
|
|||||||
if (pos != nullptr) { spawnDust(pos->x, pos->y, true); }
|
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
|
// Controls per jugador: P1 amb fletxes, P2 amb WASD. Cada Player
|
||||||
// escolta les seues pròpies tecles dins del seu `update`, no hi ha
|
// escolta les seues pròpies tecles dins del seu `update`, no hi ha
|
||||||
// estat global d'input.
|
// estat global d'input.
|
||||||
@@ -308,6 +335,7 @@ GameScene::GameScene(Config config)
|
|||||||
.left = Defaults::KeysGame::LEFT,
|
.left = Defaults::KeysGame::LEFT,
|
||||||
.right = Defaults::KeysGame::RIGHT,
|
.right = Defaults::KeysGame::RIGHT,
|
||||||
.jump = Defaults::KeysGame::JUMP,
|
.jump = Defaults::KeysGame::JUMP,
|
||||||
|
.attack = Defaults::KeysGame::ATTACK,
|
||||||
};
|
};
|
||||||
const Player::Controls CONTROLS_P2{
|
const Player::Controls CONTROLS_P2{
|
||||||
.up = Defaults::KeysGameP2::UP,
|
.up = Defaults::KeysGameP2::UP,
|
||||||
@@ -315,13 +343,14 @@ GameScene::GameScene(Config config)
|
|||||||
.left = Defaults::KeysGameP2::LEFT,
|
.left = Defaults::KeysGameP2::LEFT,
|
||||||
.right = Defaults::KeysGameP2::RIGHT,
|
.right = Defaults::KeysGameP2::RIGHT,
|
||||||
.jump = Defaults::KeysGameP2::JUMP,
|
.jump = Defaults::KeysGameP2::JUMP,
|
||||||
|
.attack = Defaults::KeysGameP2::ATTACK,
|
||||||
};
|
};
|
||||||
// Spawn lligat a la porta: tots els jugadors apareixen davant del
|
// Spawn lligat a la porta: tots els jugadors apareixen davant del
|
||||||
// passadís de sortida. `computeSpawn` deriva les coordenades de
|
// passadís de sortida. `computeSpawn` deriva les coordenades de
|
||||||
// `Map::door_col` perquè canviar la columna de la porta al YAML
|
// `Map::door_col` perquè canviar la columna de la porta al YAML
|
||||||
// moga automàticament l'spawn — no hi ha cap valor hardcoded.
|
// moga automàticament l'spawn — no hi ha cap valor hardcoded.
|
||||||
const auto SPAWN = computeSpawn();
|
const auto SPAWN = computeSpawn();
|
||||||
const bool BOTH = (USE_P1 && USE_P2);
|
const bool BOTH_ACTIVE = (USE_P1 && USE_P2);
|
||||||
|
|
||||||
// Índex de tile amb què pinta cada jugador — llegit del YAML del mapa
|
// Índex de tile amb què pinta cada jugador — llegit del YAML del mapa
|
||||||
// (camps opcionals `player1_paint_tile` / `player2_paint_tile`). Si no
|
// (camps opcionals `player1_paint_tile` / `player2_paint_tile`). Si no
|
||||||
@@ -331,16 +360,30 @@ GameScene::GameScene(Config config)
|
|||||||
// puntuació (veure `Event::player_id`).
|
// puntuació (veure `Event::player_id`).
|
||||||
constexpr int ID_P1 = 0;
|
constexpr int ID_P1 = 0;
|
||||||
constexpr int ID_P2 = 1;
|
constexpr int ID_P2 = 1;
|
||||||
if (USE_P1) {
|
|
||||||
const int P1_PAINT = readOptionalInt(config_.map_yaml, "player1_paint_tile", -1);
|
const int P1_PAINT = readOptionalInt(config_.map_yaml, "player1_paint_tile", -1);
|
||||||
const float X = BOTH ? SPAWN.p1_x : SPAWN.p1_x + static_cast<float>(Map::tile_size);
|
|
||||||
players_.emplace_back(std::make_unique<Player>(anim_p1, ID_P1, X, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
|
||||||
}
|
|
||||||
if (USE_P2) {
|
|
||||||
const int P2_PAINT = readOptionalInt(config_.map_yaml, "player2_paint_tile", -1);
|
const int P2_PAINT = readOptionalInt(config_.map_yaml, "player2_paint_tile", -1);
|
||||||
const float X = BOTH ? SPAWN.p2_x : SPAWN.p2_x - static_cast<float>(Map::tile_size);
|
|
||||||
players_.emplace_back(std::make_unique<Player>(anim_p2, ID_P2, X, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
|
// 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
|
// PaintLayer: font única del pintat del nivell. Es crea després de
|
||||||
// `loadFromLevel` perquè l'EntityManager ja ha de tenir els sarcòfags
|
// `loadFromLevel` perquè l'EntityManager ja ha de tenir els sarcòfags
|
||||||
@@ -366,10 +409,11 @@ GameScene::GameScene(Config config)
|
|||||||
for (auto& p : players_) { p->setOtherPlayers(&players_view_); }
|
for (auto& p : players_) { p->setOtherPlayers(&players_view_); }
|
||||||
|
|
||||||
// Comptador d'EXITED a EntityManager: publica `level_complete` quan
|
// Comptador d'EXITED a EntityManager: publica `level_complete` quan
|
||||||
// tots els jugadors actius han travessat la porta. Cap setExpectedPlayerCount
|
// tots els jugadors *actius* han travessat la porta. Els slots DISABLED
|
||||||
// ha d'inicialitzar-se abans del primer `player_exited`; ho fem aquí
|
// permanents (single-player) NO compten — d'altra manera la sala mai
|
||||||
// perquè ja sabem el nombre real d'instàncies.
|
// es completaria, perquè un slot DISABLED no pot eixir per la porta.
|
||||||
entity_manager_.setExpectedPlayerCount(static_cast<int>(players_.size()));
|
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:
|
// El snap també empeny lateralment si l'altre jugador ocupa el carril:
|
||||||
// passem el mateix factor perquè vaja al mateix ritme que el resolver.
|
// passem el mateix factor perquè vaja al mateix ritme que el resolver.
|
||||||
for (auto& p : players_) { p->setPushFactor(push_factor_); }
|
for (auto& p : players_) { p->setPushFactor(push_factor_); }
|
||||||
@@ -387,12 +431,20 @@ GameScene::GameScene(Config config)
|
|||||||
// només P1). Mentre els events del bus no porten `player_id`, els hits
|
// només P1). Mentre els events del bus no porten `player_id`, els hits
|
||||||
// segueixen aplicant-se a P1 (veure TODO a Hud::onPlayerHit).
|
// segueixen aplicant-se a P1 (veure TODO a Hud::onPlayerHit).
|
||||||
if (USE_P2) { hud_->activatePlayer(2); }
|
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
|
// La música de fons no s'arrenca ací: la sala comença a ROOM_INTRO,
|
||||||
// (preload al Director), així que playMusic només mou el punter cap a
|
// que és silenciós per disseny. La música la posa en marxa
|
||||||
// jail_audio i obri l'stream. Audio::get() està garantit no-null:
|
// `setState(PLAYING)` quan la cinemàtica acaba (i la torna a aturar
|
||||||
// Director::init crida Audio::init abans d'instanciar GameScene.
|
// `enterRoomIntro` quan entrem a una nova sala). El track ja està
|
||||||
Audio::get()->playMusic(config_.music_track, -1);
|
// 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
|
// Font de l'overlay de pausa: reutilitzem la Surface ja cachejada al
|
||||||
// Director (PLACEHOLDER_NAME + ".gif"). Text només parseja el .fnt a
|
// Director (PLACEHOLDER_NAME + ".gif"). Text només parseja el .fnt a
|
||||||
@@ -402,10 +454,28 @@ GameScene::GameScene(Config config)
|
|||||||
pause_font_ = std::make_shared<Text>(pause_surface, UiConfig::get().placeholderFont().fnt);
|
pause_font_ = std::make_shared<Text>(pause_surface, UiConfig::get().placeholderFont().fnt);
|
||||||
pause_font_->setDefaultKerning(UiConfig::get().placeholderFont().kerning);
|
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
|
// Timings i corbes de la FSM de sala. Llegits del YAML una sola
|
||||||
// vegada; canvis requereixen reiniciar el joc.
|
// vegada; canvis requereixen reiniciar el joc.
|
||||||
transition_config_ = RoomFsm::loadConfig(Defaults::GameScene::CONFIG_YAML);
|
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
|
// 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
|
// les transicions són instantànies (sense fades ni text) i el primer
|
||||||
// tick d'`updateRoomTransition` ja saltarà a PLAYING.
|
// tick d'`updateRoomTransition` ja saltarà a PLAYING.
|
||||||
@@ -438,58 +508,187 @@ auto GameScene::computeSpawn() -> PlayerSpawn {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::resetRoom() {
|
void GameScene::loadRoom(const std::string& yaml_path) {
|
||||||
// Recarregar les entitats des del YAML del mapa: la porta torna a
|
// 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 l'open=false a Map al loadFrom), els sarcs tornen a
|
||||||
// CLOSED amb visited buit, momies i altres actors a la posició inicial.
|
// CLOSED amb visited buit, momies i altres actors a la posició inicial.
|
||||||
Map::loadFrom(config_.map_yaml);
|
config_.map_yaml = yaml_path;
|
||||||
entity_manager_.reload(config_.map_yaml);
|
Map::loadFrom(yaml_path);
|
||||||
entity_manager_.setExpectedPlayerCount(static_cast<int>(players_.size()));
|
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(); }
|
if (paint_layer_) { paint_layer_->clear(); }
|
||||||
|
|
||||||
// Respawn dels jugadors. Recalcular spawn per si door_col canvia
|
// Tornem tots els slots a DISABLED. Els actius els reactivarà el
|
||||||
// (avui el mapa no canvia entre cicles, però la lògica ja queda
|
// trigger d'spawn aeri dins de `updateRoomTransition` quan torni a
|
||||||
// preparada per a l'itinerari futur).
|
// disparar-se el delay del nou cicle d'INTRO. Així cada cicle de
|
||||||
const auto SPAWN = computeSpawn();
|
// sala repeteix la cinemàtica de caiguda de la mateixa manera que
|
||||||
const bool BOTH = (players_.size() >= 2);
|
// el primer.
|
||||||
// Mode single-player: l'únic jugador queda centrat (= SPAWN.p1_x +
|
for (auto& p : players_) { p->disable(); }
|
||||||
// tile_size, perquè SPAWN.p1_x és el centre menys un tile).
|
|
||||||
const float SINGLE_X = SPAWN.p1_x + static_cast<float>(Map::tile_size);
|
// Refresca el marcador (piràmide / sala) amb el LevelManager. El
|
||||||
for (std::size_t i = 0; i < players_.size(); ++i) {
|
// GameScene no manté els valors a part: la lectura sempre és a
|
||||||
float x = SINGLE_X;
|
// través del singleton, així no es desincronitzen mai entre
|
||||||
if (BOTH) { x = (i == 0) ? SPAWN.p1_x : SPAWN.p2_x; }
|
// hotkeys de salt, debug.yaml i creuament natural de porta.
|
||||||
players_[i]->respawn(x, SPAWN.y);
|
if (hud_) {
|
||||||
|
const auto& mgr = Levels::LevelManager::get();
|
||||||
|
hud_->setPyramidRoom(mgr.currentPyramid(), mgr.currentRoom());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "[ROOM] reset: entities reloaded, paint cleared, "
|
std::cout << "[ROOM] load " << yaml_path << ": entities reloaded, paint cleared, "
|
||||||
<< players_.size() << " player(s) respawned\n";
|
<< 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) {
|
void GameScene::setState(RoomFsm::State new_state) {
|
||||||
if (new_state == state_) { return; }
|
if (new_state == state_) { return; }
|
||||||
std::cout << "[ROOM] " << RoomFsm::stateName(state_) << " -> " << RoomFsm::stateName(new_state) << '\n';
|
std::cout << "[ROOM] " << RoomFsm::stateName(state_) << " -> " << RoomFsm::stateName(new_state) << '\n';
|
||||||
const auto PREV = state_;
|
|
||||||
state_ = new_state;
|
state_ = new_state;
|
||||||
transition_ = {};
|
transition_ = {};
|
||||||
// En tornar a ROOM_INTRO des de ROOM_OUTRO (placeholder de
|
// En entrar a un nou cicle d'INTRO, resetegem el timer i el flag de
|
||||||
// "torna a la mateixa sala"), reseteja completament l'estat del
|
// l'spawn aeri perquè es torne a disparar al delay configurat.
|
||||||
// nivell. Si la transició és la inicial (PREV també és ROOM_INTRO,
|
if (new_state == RoomFsm::State::ROOM_INTRO) {
|
||||||
// imposible amb el guard de dalt) o ve d'un altre estat, no fa res.
|
room_intro_elapsed_ms_ = 0;
|
||||||
if (new_state == RoomFsm::State::ROOM_INTRO && PREV == RoomFsm::State::ROOM_OUTRO) {
|
players_spawned_in_intro_.fill(false);
|
||||||
resetRoom();
|
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) {
|
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);
|
const auto NEXT = RoomFsm::advance(state_, transition_, transition_config_, delta_ms);
|
||||||
if (NEXT != state_) { setState(NEXT); }
|
|
||||||
|
// 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() {
|
void GameScene::renderRoomFade() {
|
||||||
// Fade negre sobre el món, pintat un cop els actors ja estan al
|
// Fade negre sobre el món, pintat un cop els actors ja estan al
|
||||||
// canvas i abans del HUD. Dos casos:
|
// canvas i abans del HUD. Tres casos:
|
||||||
// ROOM_INTRO/fase 0: 255 → 0 (entrada de sala, món emergent)
|
// 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 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;
|
int dur = 0;
|
||||||
float t = 0.0F;
|
float t = 0.0F;
|
||||||
bool fade_in = false;
|
bool fade_in = false;
|
||||||
@@ -512,72 +711,162 @@ void GameScene::renderRoomFade() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::renderRoomText() {
|
void GameScene::renderRoomText() {
|
||||||
// Text de la transició de sala. Reusa la font cachejada per a la
|
// Orquestrador del rètol de transició de sala. La feina pesada
|
||||||
// pausa (mateix tipus de lletra, mateix índex de color), i fa el
|
// (animació de X, animació d'altura del panel i pintat) la fan
|
||||||
// mateix patró esquerra → centre → esquerra que el text de pausa
|
// quatre helpers privats; aquesta funció només decideix quin
|
||||||
// però amb les durades pròpies de la transició.
|
// text dibuixar i amb quines fases segons l'estat.
|
||||||
//
|
//
|
||||||
// Mapatge per estat (només els estats amb text actiu hi entren):
|
// Mapatge per estat (només els estats amb text actiu hi entren):
|
||||||
// ROOM_INTRO: phases 2..4 → enter / hold / exit; text = sala N
|
// ROOM_INTRO: phases 2..4 → enter / hold / exit; dues línies
|
||||||
// ROOM_OUTRO: phases 0..2 → enter / hold / exit; text = "fase completada"
|
// (piràmide damunt, sala davall) que travessen la
|
||||||
if (!pause_font_) { return; }
|
// pantalla en direccions oposades.
|
||||||
|
// ROOM_OUTRO: phases 0..2 → enter / hold / exit; dues línies
|
||||||
std::string text;
|
// oposades ("fase" / "completada"). Phase 3 és el
|
||||||
int phase_enter = 0;
|
// fade-out i 4 la pausa en negre — cap text en cap
|
||||||
int phase_exit = 0;
|
// de les dues.
|
||||||
int dur_enter = 0;
|
if (!room_text_font_) { return; }
|
||||||
int dur_exit = 0;
|
|
||||||
if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase >= 2 && transition_.phase <= 4) {
|
|
||||||
text = RoomFsm::formatRoomTitle(room_index_);
|
|
||||||
phase_enter = 2;
|
|
||||||
phase_exit = 4;
|
|
||||||
dur_enter = transition_config_.intro_text_enter_ms;
|
|
||||||
dur_exit = transition_config_.intro_text_exit_ms;
|
|
||||||
} else if (state_ == RoomFsm::State::ROOM_OUTRO && transition_.phase >= 0 && transition_.phase <= 2) {
|
|
||||||
text = Locale::get().text("game.room_complete");
|
|
||||||
phase_enter = 0;
|
|
||||||
phase_exit = 2;
|
|
||||||
dur_enter = transition_config_.outro_text_enter_ms;
|
|
||||||
dur_exit = transition_config_.outro_text_exit_ms;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr int KERNING = 0;
|
constexpr int KERNING = 0;
|
||||||
const int W = pause_font_->length(text, KERNING);
|
// Centre vertical de la zona de joc (canvas sense la HUD). Reemplaça
|
||||||
// El text entra des de l'esquerra (off-screen amb el seu cantó dret
|
// l'antic GameCanvas::CENTER_Y per descomptar la franja del marcador
|
||||||
// a x=0) i, després del hold, surt cap a la dreta (off-screen amb
|
// i tenir el rètol equilibrat sobre el playfield.
|
||||||
// el seu cantó esquerre a x=WIDTH). Així el text "travessa" la
|
const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2;
|
||||||
// pantalla en una sola direcció en compte de tornar enrere.
|
// Colors del rètol via UiConfig (`data/ui.yaml > colors.room_text` i
|
||||||
const int OFF_LEFT = -W;
|
// `room_text_outline`). Independents dels colors de notificació.
|
||||||
const int OFF_RIGHT = GameCanvas::WIDTH;
|
const Uint8 FILL = UiConfig::get().roomText();
|
||||||
int x = GameCanvas::CENTER_X;
|
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) {
|
if (transition_.phase == phase_enter) {
|
||||||
const float T = (dur_enter > 0)
|
const float T = (dur_enter > 0)
|
||||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_enter), 0.0F, 1.0F)
|
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_enter), 0.0F, 1.0F)
|
||||||
: 1.0F;
|
: 1.0F;
|
||||||
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
||||||
x = Easing::lerpInt(OFF_LEFT, GameCanvas::CENTER_X, CURVE(T));
|
return Easing::lerpInt(OFF_ENTER, GameCanvas::CENTER_X, CURVE(T));
|
||||||
} else if (transition_.phase == phase_exit) {
|
}
|
||||||
|
if (transition_.phase == phase_exit) {
|
||||||
const float T = (dur_exit > 0)
|
const float T = (dur_exit > 0)
|
||||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_exit), 0.0F, 1.0F)
|
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_exit), 0.0F, 1.0F)
|
||||||
: 1.0F;
|
: 1.0F;
|
||||||
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
||||||
x = Easing::lerpInt(GameCanvas::CENTER_X, OFF_RIGHT, CURVE(T));
|
return Easing::lerpInt(GameCanvas::CENTER_X, OFF_EXIT, CURVE(T));
|
||||||
|
}
|
||||||
|
return GameCanvas::CENTER_X;
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr Uint8 OUTLINE_WHITE = 255;
|
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;
|
constexpr Uint8 STROKE_DIST = 1;
|
||||||
const int Y = GameCanvas::CENTER_Y - (pause_font_->getLineHeight() / 2);
|
room_text_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE4_FLAG,
|
||||||
pause_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE4_FLAG,
|
|
||||||
x,
|
x,
|
||||||
Y,
|
y,
|
||||||
text,
|
text,
|
||||||
KERNING,
|
KERNING,
|
||||||
UiConfig::get().notificationText(),
|
fill_color,
|
||||||
STROKE_DIST,
|
STROKE_DIST,
|
||||||
OUTLINE_WHITE);
|
outline_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::spawnDust(float feet_x_center, float feet_y, bool is_landing) {
|
void GameScene::spawnDust(float feet_x_center, float feet_y, bool is_landing) {
|
||||||
@@ -604,6 +893,26 @@ void GameScene::spawnDust(float feet_x_center, float feet_y, bool is_landing) {
|
|||||||
entity_manager_.add(std::move(dust));
|
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() {
|
void GameScene::handlePauseInput() {
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
// Mutex amb l'editor: si està actiu, ignorem la tecla de pausa.
|
// Mutex amb l'editor: si està actiu, ignorem la tecla de pausa.
|
||||||
@@ -690,6 +999,28 @@ namespace {
|
|||||||
return ev.type == SDL_EVENT_KEY_DOWN && ev.key.scancode == SC;
|
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
|
} // namespace
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -709,6 +1040,28 @@ void GameScene::update(int delta_ms) {
|
|||||||
if (pause_.state == PauseState::IDLE && detectEditorToggle()) {
|
if (pause_.state == PauseState::IDLE && detectEditorToggle()) {
|
||||||
editor_.toggle(tilemap_);
|
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
|
#endif
|
||||||
|
|
||||||
GlobalInputs::update(delta_ms);
|
GlobalInputs::update(delta_ms);
|
||||||
@@ -725,15 +1078,9 @@ void GameScene::update(int delta_ms) {
|
|||||||
constexpr bool EDITOR_FROZEN = false;
|
constexpr bool EDITOR_FROZEN = false;
|
||||||
#endif
|
#endif
|
||||||
const int SCALED_DELTA_MS = static_cast<int>(static_cast<float>(delta_ms) * pause_.scale);
|
const int SCALED_DELTA_MS = static_cast<int>(static_cast<float>(delta_ms) * pause_.scale);
|
||||||
const bool GAMEPLAY_ACTIVE = (state_ == RoomFsm::State::PLAYING);
|
|
||||||
if (!EDITOR_FROZEN && SCALED_DELTA_MS > 0) {
|
if (!EDITOR_FROZEN && SCALED_DELTA_MS > 0) {
|
||||||
const float DT = static_cast<float>(SCALED_DELTA_MS) / 1000.0F;
|
const float DT = static_cast<float>(SCALED_DELTA_MS) / 1000.0F;
|
||||||
|
|
||||||
// Durant ROOM_INTRO/ROOM_OUTRO els jugadors no s'actualitzen ni
|
|
||||||
// poden col·lisionar amb entitats. Les entitats sí, perquè el
|
|
||||||
// disseny demana que el món continui animant-se durant les
|
|
||||||
// transicions de sala.
|
|
||||||
if (GAMEPLAY_ACTIVE) {
|
|
||||||
// Pipeline de moviment en tres passes per poder intercalar la
|
// Pipeline de moviment en tres passes per poder intercalar la
|
||||||
// resolució de contactes jugador↔jugador entre input i posició:
|
// resolució de contactes jugador↔jugador entre input i posició:
|
||||||
// 1) tick: cada jugador calcula la velocitat intencionada
|
// 1) tick: cada jugador calcula la velocitat intencionada
|
||||||
@@ -744,10 +1091,14 @@ void GameScene::update(int delta_ms) {
|
|||||||
// pusher faça `tryMove`, el pushed ja estiga fora de la
|
// pusher faça `tryMove`, el pushed ja estiga fora de la
|
||||||
// seua caixa. La callback també tracta l'altre jugador com
|
// seua caixa. La callback també tracta l'altre jugador com
|
||||||
// a sòlid: xarxa de seguretat davant de qualsevol inexactitud.
|
// 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); }
|
for (auto& player : players_) { player->tick(DT); }
|
||||||
resolvePlayerVsPlayer(players_view_, DT, push_factor_);
|
resolvePlayerVsPlayer(players_view_, DT, push_factor_);
|
||||||
for (auto* player : orderedForMovement(players_view_)) { player->applyMovement(DT); }
|
for (auto* player : orderedForMovement(players_view_)) { player->applyMovement(DT); }
|
||||||
}
|
|
||||||
|
|
||||||
// El tick d'entitats DESPRÉS del tick dels jugadors: si el jugador ha
|
// El tick d'entitats DESPRÉS del tick dels jugadors: si el jugador ha
|
||||||
// emès `on_tile_entered` aquest frame, l'EntityManager ja l'ha encaminat
|
// emès `on_tile_entered` aquest frame, l'EntityManager ja l'ha encaminat
|
||||||
@@ -755,13 +1106,12 @@ void GameScene::update(int delta_ms) {
|
|||||||
// aplicar la transició OPENING → OPEN en el mateix frame.
|
// aplicar la transició OPENING → OPEN en el mateix frame.
|
||||||
entity_manager_.update(DT);
|
entity_manager_.update(DT);
|
||||||
|
|
||||||
// Detecció de contacte player↔entitat després que totes les entitats
|
// Detecció de contacte player↔entitat. `checkPlayerCollisions` ja
|
||||||
// hagin mogut aquest frame. Reutilitzem la vista plana que ja
|
// filtra jugadors fora del joc (`isOutOfPlay`), així que durant
|
||||||
// manté el GameScene per al resolver player↔player.
|
// l'INTRO previ a l'spawn no es disparen contactes amb les
|
||||||
if (GAMEPLAY_ACTIVE) {
|
// entitats que ja s'animen al fons.
|
||||||
entity_manager_.checkPlayerCollisions(players_view_);
|
entity_manager_.checkPlayerCollisions(players_view_);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
editor_.update(delta_ms, tilemap_);
|
editor_.update(delta_ms, tilemap_);
|
||||||
@@ -799,19 +1149,14 @@ void GameScene::render() {
|
|||||||
// explícita: l'editor edita el tilemap, no la capa de pintat).
|
// explícita: l'editor edita el tilemap, no la capa de pintat).
|
||||||
if (!SKIP_ACTORS && paint_layer_) { paint_layer_->render(); }
|
if (!SKIP_ACTORS && paint_layer_) { paint_layer_->render(); }
|
||||||
if (!SKIP_ACTORS) {
|
if (!SKIP_ACTORS) {
|
||||||
// Durant ROOM_INTRO/ROOM_OUTRO els jugadors no es veuen ni es mouen.
|
|
||||||
// Les entitats sí que es renderitzen perquè el disseny demana que
|
|
||||||
// el món continui animant-se durant el fade i el text.
|
|
||||||
const bool RENDER_PLAYERS = (state_ == RoomFsm::State::PLAYING);
|
|
||||||
|
|
||||||
if (RENDER_PLAYERS) {
|
|
||||||
// Passada de sombres dels jugadors saltant. Va per davant del
|
// Passada de sombres dels jugadors saltant. Va per davant del
|
||||||
// pintat (al damunt) però per darrere de TOTS els actors —
|
// pintat (al damunt) però per darrere de TOTS els actors —
|
||||||
// entitats i jugadors, inclòs el saltador mateix— així la sombra
|
// entitats i jugadors, inclòs el saltador mateix— així la sombra
|
||||||
// queda al sòl sense tapar res. `renderShadow` és no-op si el
|
// 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(); }
|
for (const auto& p : players_) { p->renderShadow(); }
|
||||||
}
|
|
||||||
// Z-order interleaving entitats ↔ players per `getFeetY()` ascendent:
|
// Z-order interleaving entitats ↔ players per `getFeetY()` ascendent:
|
||||||
// els actors amb els peus més amunt queden dibuixats primer (al fons),
|
// els actors amb els peus més amunt queden dibuixats primer (al fons),
|
||||||
// els més avall al davant. Així un sarc situat per darrere d'un player
|
// els més avall al davant. Així un sarc situat per darrere d'un player
|
||||||
@@ -822,16 +1167,18 @@ void GameScene::render() {
|
|||||||
Player* player; // nullable; si és nullptr, s'usa `entity`.
|
Player* player; // nullable; si és nullptr, s'usa `entity`.
|
||||||
};
|
};
|
||||||
std::vector<Renderable> order;
|
std::vector<Renderable> order;
|
||||||
order.reserve(entity_manager_.entityCount() + (RENDER_PLAYERS ? players_.size() : 0));
|
order.reserve(entity_manager_.entityCount() + players_.size());
|
||||||
const auto ENTITIES = entity_manager_.getEntities();
|
const auto ENTITIES = entity_manager_.getEntities();
|
||||||
std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) {
|
std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) {
|
||||||
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
||||||
});
|
});
|
||||||
if (RENDER_PLAYERS) {
|
// 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) {
|
std::ranges::transform(players_, std::back_inserter(order), [](const auto& p) {
|
||||||
return Renderable{.feet_y = p->getFeetY(), .entity = nullptr, .player = p.get()};
|
return Renderable{.feet_y = p->getFeetY(), .entity = nullptr, .player = p.get()};
|
||||||
});
|
});
|
||||||
}
|
|
||||||
std::ranges::stable_sort(order, [](const Renderable& a, const Renderable& b) {
|
std::ranges::stable_sort(order, [](const Renderable& a, const Renderable& b) {
|
||||||
return a.feet_y < b.feet_y;
|
return a.feet_y < b.feet_y;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -74,6 +75,12 @@ class GameScene : public Scene {
|
|||||||
// `is_landing` selecciona la variant de l'animació al `Dust`.
|
// `is_landing` selecciona la variant de l'animació al `Dust`.
|
||||||
void spawnDust(float feet_x_center, float feet_y, bool is_landing);
|
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_;
|
Config config_;
|
||||||
Tilemap tilemap_;
|
Tilemap tilemap_;
|
||||||
|
|
||||||
@@ -84,8 +91,43 @@ class GameScene : public Scene {
|
|||||||
|
|
||||||
// Vector des del principi: AEE suportarà multijugador local; cada Player
|
// Vector des del principi: AEE suportarà multijugador local; cada Player
|
||||||
// té els seus propis Controls i entra al vector amb una crida extra.
|
// 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_;
|
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
|
// 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
|
// 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
|
// cop al ctor (els punters són estables perquè `players_` no es
|
||||||
@@ -126,6 +168,13 @@ class GameScene : public Scene {
|
|||||||
// abans — ja cachejada al Director.
|
// abans — ja cachejada al Director.
|
||||||
std::shared_ptr<Text> pause_font_;
|
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 {
|
enum class PauseState : std::uint8_t {
|
||||||
IDLE, // Joc corrent; cap transició en marxa. scale == 1.
|
IDLE, // Joc corrent; cap transició en marxa. scale == 1.
|
||||||
ENTERING, // Frenant cap a la pausa. scale decreix a 0.
|
ENTERING, // Frenant cap a la pausa. scale decreix a 0.
|
||||||
@@ -156,18 +205,24 @@ class GameScene : public Scene {
|
|||||||
// reseteja a {0, 0}.
|
// reseteja a {0, 0}.
|
||||||
RoomFsm::Transition transition_{};
|
RoomFsm::Transition transition_{};
|
||||||
|
|
||||||
// Índex de sala que es passa com a paràmetre {n} al text d'intro.
|
|
||||||
// Avui és sempre 1 (no hi ha itinerari de sales encara). Quan
|
|
||||||
// arribi l'itinerari, és el camp que es modificarà al transicionar
|
|
||||||
// de ROOM_OUTRO → ROOM_INTRO per saltar a la pròxima.
|
|
||||||
int room_index_{1};
|
|
||||||
|
|
||||||
// Píxels del fade negre i del text de la transició de sala.
|
// 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 —
|
// Cridats des de `render()` entre el bloc d'actors i el HUD —
|
||||||
// així marcador, notificacions i service menu queden per damunt.
|
// així marcador, notificacions i service menu queden per damunt.
|
||||||
void renderRoomFade();
|
void renderRoomFade();
|
||||||
void renderRoomText();
|
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
|
// Posició inicial dels jugadors derivada de `Map::door_col`. Tots
|
||||||
// els jugadors comparteixen Y (centre del corredor exterior superior);
|
// els jugadors comparteixen Y (centre del corredor exterior superior);
|
||||||
// X depèn del nombre actiu: amb un jugador queda centrat sota la
|
// X depèn del nombre actiu: amb un jugador queda centrat sota la
|
||||||
@@ -181,11 +236,27 @@ class GameScene : public Scene {
|
|||||||
};
|
};
|
||||||
[[nodiscard]] static auto computeSpawn() -> PlayerSpawn;
|
[[nodiscard]] static auto computeSpawn() -> PlayerSpawn;
|
||||||
|
|
||||||
// Reset complet de la sala: reload de l'EntityManager (sarcs/porta/
|
// Recarrega la sala apuntada per `yaml_path`: refresca Map (geometria,
|
||||||
// momies des de zero), neteja del PaintLayer i respawn dels jugadors
|
// porta tancada), entitats (sarcs/porta/momies des de zero), neteja
|
||||||
// a la posició calculada per `computeSpawn`. Cridat des de
|
// el PaintLayer i deixa els jugadors a DISABLED esperant la cinemàtica
|
||||||
// `setState(ROOM_INTRO)` quan la transició anterior era OUTRO.
|
// d'spawn aeri del nou cicle d'INTRO. També actualitza
|
||||||
void resetRoom();
|
// `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
|
#ifdef _DEBUG
|
||||||
// Editor modal del tilemap. Mútuament excloent amb la pausa: si la
|
// Editor modal del tilemap. Mútuament excloent amb la pausa: si la
|
||||||
|
|||||||
@@ -42,6 +42,17 @@ namespace RoomFsm {
|
|||||||
cfg.intro_text_enter_ms = readChildInt(n, "text_enter_ms", cfg.intro_text_enter_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_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.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")) {
|
if (yaml.contains("room_outro")) {
|
||||||
const auto& n = yaml["room_outro"];
|
const auto& n = yaml["room_outro"];
|
||||||
@@ -49,6 +60,8 @@ namespace RoomFsm {
|
|||||||
cfg.outro_text_hold_ms = readChildInt(n, "text_hold_ms", cfg.outro_text_hold_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_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_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")) {
|
if (yaml.contains("text")) {
|
||||||
const auto& n = yaml["text"];
|
const auto& n = yaml["text"];
|
||||||
@@ -59,6 +72,19 @@ namespace RoomFsm {
|
|||||||
cfg.text_exit_curve = UiConfig::curveByName(n["exit_curve"].get_value<std::string>());
|
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
|
std::cout << "RoomFsm: timings carregats des de " << yaml_path
|
||||||
<< " (intro fade " << cfg.intro_fade_in_ms
|
<< " (intro fade " << cfg.intro_fade_in_ms
|
||||||
@@ -88,33 +114,50 @@ namespace RoomFsm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ROOM_OUTRO
|
// ROOM_OUTRO
|
||||||
const std::array<int, 4> DURS = {
|
const std::array<int, 5> DURS = {
|
||||||
cfg.outro_text_enter_ms,
|
cfg.outro_text_enter_ms,
|
||||||
cfg.outro_text_hold_ms,
|
cfg.outro_text_hold_ms,
|
||||||
cfg.outro_text_exit_ms,
|
cfg.outro_text_exit_ms,
|
||||||
cfg.outro_fade_out_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]) {
|
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
||||||
t.elapsed_ms -= DURS[t.phase];
|
t.elapsed_ms -= DURS[t.phase];
|
||||||
++t.phase;
|
++t.phase;
|
||||||
}
|
}
|
||||||
// Placeholder fins a l'itinerari de sales: l'OUTRO recau a INTRO.
|
// Final de l'OUTRO → ROOM_INTRO de la sala SEGÜENT. La FSM no
|
||||||
// TODO(itinerari): aquest mateix retorn sortirà cap a una nova
|
// sap quina és — només avisa del canvi d'estat. La GameScene,
|
||||||
// sala (incrementant `room_index_` i recreant tilemap/entitats al
|
// dins `setState(ROOM_INTRO)` amb PREV=OUTRO, demana al
|
||||||
// GameScene) abans de tornar a ROOM_INTRO.
|
// LevelManager el següent nivell i recarrega tilemap/entitats.
|
||||||
if (t.phase >= static_cast<int>(DURS.size())) { return State::ROOM_INTRO; }
|
if (t.phase >= static_cast<int>(DURS.size())) { return State::ROOM_INTRO; }
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto formatRoomTitle(int n) -> std::string {
|
namespace {
|
||||||
auto tpl = Locale::get().text("game.room_intro");
|
// Substitueix la primera ocurrència de `placeholder` dins de `tpl`
|
||||||
constexpr std::string_view PLACEHOLDER = "{n}";
|
// pel valor d'`n` convertit a string. Utilitzat per les dues
|
||||||
auto pos = tpl.find(PLACEHOLDER);
|
// 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) {
|
if (pos != std::string::npos) {
|
||||||
tpl.replace(pos, PLACEHOLDER.size(), std::to_string(n));
|
tpl.replace(pos, placeholder.size(), std::to_string(n));
|
||||||
}
|
}
|
||||||
return tpl;
|
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* {
|
auto stateName(State s) -> const char* {
|
||||||
switch (s) {
|
switch (s) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace RoomFsm {
|
|||||||
// Sub-fase i temps acumulat dins de l'estat actual. Es reseteja a
|
// 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:
|
// {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_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
|
// ROOM_OUTRO: 0=text_enter, 1=text_hold, 2=text_exit, 3=fade_out, 4=black_hold
|
||||||
// PLAYING: no s'usa.
|
// PLAYING: no s'usa.
|
||||||
struct Transition {
|
struct Transition {
|
||||||
int phase{0};
|
int phase{0};
|
||||||
@@ -41,9 +41,69 @@ namespace RoomFsm {
|
|||||||
int outro_text_hold_ms{1000};
|
int outro_text_hold_ms{1000};
|
||||||
int outro_text_exit_ms{500};
|
int outro_text_exit_ms{500};
|
||||||
int outro_fade_out_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_enter_curve{nullptr};
|
||||||
UiConfig::CurveFn text_exit_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
|
// Llegeix la configuració des del YAML. Si el fitxer manca o falten
|
||||||
@@ -59,10 +119,15 @@ namespace RoomFsm {
|
|||||||
// (això ho fa `GameScene::setState`). Funció pura sobre l'argument.
|
// (això ho fa `GameScene::setState`). Funció pura sobre l'argument.
|
||||||
[[nodiscard]] auto advance(State current, Transition& t, const Config& cfg, int delta_ms) -> State;
|
[[nodiscard]] auto advance(State current, Transition& t, const Config& cfg, int delta_ms) -> State;
|
||||||
|
|
||||||
// Substitueix `{n}` de la clau "game.room_intro" del Locale per
|
// Substitueix `{p}` de la clau "game.room_intro_pyramid" del Locale
|
||||||
// l'enter passat. Si la clau no està traduïda, retorna la pròpia
|
// per l'enter passat. Pensat per a la primera línia del text d'INTRO.
|
||||||
// clau (fallback de Locale::text).
|
// Si la clau no està traduïda, retorna la pròpia clau (fallback de
|
||||||
[[nodiscard]] auto formatRoomTitle(int n) -> std::string;
|
// 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.
|
// Nom textual per a logs i tests. No es fa servir per a lògica.
|
||||||
[[nodiscard]] auto stateName(State s) -> const char*;
|
[[nodiscard]] auto stateName(State s) -> const char*;
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ namespace Defaults {
|
|||||||
constexpr SDL_Scancode LEFT = SDL_SCANCODE_LEFT;
|
constexpr SDL_Scancode LEFT = SDL_SCANCODE_LEFT;
|
||||||
constexpr SDL_Scancode RIGHT = SDL_SCANCODE_RIGHT;
|
constexpr SDL_Scancode RIGHT = SDL_SCANCODE_RIGHT;
|
||||||
constexpr SDL_Scancode JUMP = SDL_SCANCODE_M;
|
constexpr SDL_Scancode JUMP = SDL_SCANCODE_M;
|
||||||
|
constexpr SDL_Scancode ATTACK = SDL_SCANCODE_N;
|
||||||
constexpr SDL_Scancode EXIT = SDL_SCANCODE_ESCAPE;
|
constexpr SDL_Scancode EXIT = SDL_SCANCODE_ESCAPE;
|
||||||
} // namespace KeysGame
|
} // namespace KeysGame
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ namespace Defaults {
|
|||||||
constexpr SDL_Scancode LEFT = SDL_SCANCODE_A;
|
constexpr SDL_Scancode LEFT = SDL_SCANCODE_A;
|
||||||
constexpr SDL_Scancode RIGHT = SDL_SCANCODE_D;
|
constexpr SDL_Scancode RIGHT = SDL_SCANCODE_D;
|
||||||
constexpr SDL_Scancode JUMP = SDL_SCANCODE_V;
|
constexpr SDL_Scancode JUMP = SDL_SCANCODE_V;
|
||||||
|
constexpr SDL_Scancode ATTACK = SDL_SCANCODE_C;
|
||||||
} // namespace KeysGameP2
|
} // namespace KeysGameP2
|
||||||
|
|
||||||
namespace Player {
|
namespace Player {
|
||||||
@@ -130,6 +132,15 @@ namespace Defaults {
|
|||||||
constexpr const char* PLACEHOLDER_FNT = "data/fonts/ari-w9500-condensed-display.fnt";
|
constexpr const char* PLACEHOLDER_FNT = "data/fonts/ari-w9500-condensed-display.fnt";
|
||||||
constexpr int PLACEHOLDER_KERNING = 1;
|
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
|
// Menú de servei (F9): títol en display normal, items en la
|
||||||
// variant condensed-display (més compacta, perquè caben més
|
// variant condensed-display (més compacta, perquè caben més
|
||||||
// files sense encongir la caixa). Kerning 0 a totes dues perquè
|
// 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_VALUE = 187; // Valors (score, vides, tresors).
|
||||||
constexpr Uint8 HUD_FLASH = 237; // Color temporal del valor en flash.
|
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
|
// Timings d'animació del HUD. L'entrada són els tres blocs lliscant
|
||||||
// a la seua posició; flash i shake acompanyen un `player_hit`.
|
// a la seua posició; flash i shake acompanyen un `player_hit`.
|
||||||
constexpr int HUD_ENTRY_MS = 400;
|
constexpr int HUD_ENTRY_MS = 400;
|
||||||
@@ -206,8 +224,6 @@ namespace Defaults {
|
|||||||
namespace Editor {
|
namespace Editor {
|
||||||
// Color del rectangle 1 px que envolta el canvas durant l'edició.
|
// Color del rectangle 1 px que envolta el canvas durant l'edició.
|
||||||
constexpr std::uint32_t BORDER_ARGB = 0xFF2244FFU;
|
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
|
// Píxels entre el canvas i el rectangle del TilePicker per totes
|
||||||
// les vores. Prou perquè el border blau de l'editor no es
|
// les vores. Prou perquè el border blau de l'editor no es
|
||||||
// superpose amb la graella del picker.
|
// superpose amb la graella del picker.
|
||||||
@@ -231,7 +247,12 @@ namespace Defaults {
|
|||||||
// en contingui cap literal; afegir una piràmide nova serà un altre
|
// en contingui cap literal; afegir una piràmide nova serà un altre
|
||||||
// conjunt de constants + un Config distint passat al ctor.
|
// conjunt de constants + un Config distint passat al ctor.
|
||||||
namespace Pyramid {
|
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_SPRITE = "data/player/sam.gif";
|
||||||
constexpr const char* PLAYER_YAML = "data/player/sam.yaml";
|
constexpr const char* PLAYER_YAML = "data/player/sam.yaml";
|
||||||
// Personatge del segon jugador. Clonat de sam amb repaletat (roig→blau);
|
// Personatge del segon jugador. Clonat de sam amb repaletat (roig→blau);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace Texts {
|
namespace Texts {
|
||||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
||||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||||
constexpr const char* VERSION = "0.24.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 Texts
|
||||||
|
|
||||||
namespace GameCanvas {
|
namespace GameCanvas {
|
||||||
|
|||||||
@@ -431,6 +431,77 @@ namespace {
|
|||||||
REQUIRE(mummy.getDirX() <= 0.0F);
|
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() {
|
void testTorchAnimation() {
|
||||||
std::cout << "[T10] torch animation wired from yaml\n";
|
std::cout << "[T10] torch animation wired from yaml\n";
|
||||||
// Regressió: el torch.yaml declarava `frames: [0]` i el render no
|
// Regressió: el torch.yaml declarava `frames: [0]` i el render no
|
||||||
@@ -510,6 +581,9 @@ auto main() -> int {
|
|||||||
testSarcophagusYaml();
|
testSarcophagusYaml();
|
||||||
testCollisionBox();
|
testCollisionBox();
|
||||||
testMummyStep();
|
testMummyStep();
|
||||||
|
testMummyHurtState();
|
||||||
|
testMummyHurtAgainstWall();
|
||||||
|
testMummyWhippableFlag();
|
||||||
testTorchAnimation();
|
testTorchAnimation();
|
||||||
testExitedAggregator();
|
testExitedAggregator();
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
// T6 advance — PLAYING és sumidor: no avança res ni canvia d'estat.
|
// 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.
|
// 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.
|
// T8 advance — un delta gran salta més d'una sub-fase en un sol pas.
|
||||||
// T9 formatRoomTitle — substitueix `{n}` quan el Locale té la clau.
|
// T9 formatIntroPyramidLine / formatIntroRoomLine — substitució amb Locale.
|
||||||
// T10 formatRoomTitle — sense Locale, retorna la pròpia clau (fallback).
|
// T10 formatIntroPyramidLine / formatIntroRoomLine — fallback sense Locale.
|
||||||
// T11 stateName — strings esperats per a cada State.
|
// T11 stateName — strings esperats per a cada State.
|
||||||
//
|
//
|
||||||
// El render no es testeja: la FSM és pura i no pinta res. Les regles de
|
// El render no es testeja: la FSM és pura i no pinta res. Les regles de
|
||||||
@@ -189,23 +189,29 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void testFormatRoomTitleWithLocale() {
|
void testFormatRoomTitleWithLocale() {
|
||||||
std::cout << "T9 formatRoomTitle — substitució amb Locale\n";
|
std::cout << "T9 formatIntroPyramidLine / formatIntroRoomLine — substitució amb Locale\n";
|
||||||
const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_locale.yaml").string();
|
const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_locale.yaml").string();
|
||||||
writeTemp(PATH, "game:\n room_intro: \"Sala {n}\"\n");
|
writeTemp(PATH,
|
||||||
|
"game:\n"
|
||||||
|
" room_intro_pyramid: \"Piramide {p}\"\n"
|
||||||
|
" room_intro_room: \"Sala {r}\"\n");
|
||||||
Locale::get().load(PATH);
|
Locale::get().load(PATH);
|
||||||
CHECK(RoomFsm::formatRoomTitle(1) == "Sala 1");
|
CHECK(RoomFsm::formatIntroPyramidLine(1) == "Piramide 1");
|
||||||
CHECK(RoomFsm::formatRoomTitle(7) == "Sala 7");
|
CHECK(RoomFsm::formatIntroPyramidLine(5) == "Piramide 5");
|
||||||
|
CHECK(RoomFsm::formatIntroRoomLine(1) == "Sala 1");
|
||||||
|
CHECK(RoomFsm::formatIntroRoomLine(7) == "Sala 7");
|
||||||
std::filesystem::remove(PATH);
|
std::filesystem::remove(PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
void testFormatRoomTitleNoLocale() {
|
void testFormatRoomTitleNoLocale() {
|
||||||
std::cout << "T10 formatRoomTitle — fallback sense traducció\n";
|
std::cout << "T10 formatIntroPyramidLine / formatIntroRoomLine — fallback sense traducció\n";
|
||||||
const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_locale_empty.yaml").string();
|
const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_locale_empty.yaml").string();
|
||||||
writeTemp(PATH, "other: foo\n");
|
writeTemp(PATH, "other: foo\n");
|
||||||
Locale::get().load(PATH);
|
Locale::get().load(PATH);
|
||||||
// Sense la clau "game.room_intro", Locale retorna la pròpia clau,
|
// Sense les claus, Locale retorna la pròpia clau i com que no
|
||||||
// i com que no porta `{n}`, la substitució no canvia res.
|
// porten `{p}`/`{r}`, la substitució no canvia res.
|
||||||
CHECK(RoomFsm::formatRoomTitle(3) == "game.room_intro");
|
CHECK(RoomFsm::formatIntroPyramidLine(3) == "game.room_intro_pyramid");
|
||||||
|
CHECK(RoomFsm::formatIntroRoomLine(2) == "game.room_intro_room");
|
||||||
std::filesystem::remove(PATH);
|
std::filesystem::remove(PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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};
|
||||||
|
}
|
||||||