Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f6aa182f7 | ||
|
|
b65eeb7aaa | ||
|
|
37fdcb4751 | ||
|
|
4739354875 | ||
|
|
2879cbdb17 | ||
|
|
38070b33aa | ||
|
|
9f0a566ffb | ||
|
|
6397f9d4eb | ||
|
|
72384c3113 | ||
|
|
9dc7ff0da3 | ||
|
|
1d43a77800 | ||
|
|
39eda56c9d | ||
|
|
a351f920ba | ||
|
|
72642dc10e | ||
|
|
404db74889 | ||
|
|
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
|
||||
# principal com al smoke test. Mantenir-los en variable separada evita
|
||||
# duplicació entre els dos targets.
|
||||
set(ENTITY_SOURCES
|
||||
# Entitats sense dependència directa amb Player.cpp: les enllacen tant
|
||||
# l'executable principal com el test_entities_smoke (que evita arrossegar
|
||||
# l'stack del jugador i les seues dependències pesades).
|
||||
set(ENTITY_SOURCES_BASE
|
||||
source/game/entities/entity.cpp
|
||||
source/game/entities/animation_component.cpp
|
||||
source/game/entities/event_bus.cpp
|
||||
@@ -50,18 +53,29 @@ set(ENTITY_SOURCES
|
||||
source/game/entities/concrete/dust.cpp
|
||||
)
|
||||
|
||||
# Entitats que criden directament símbols de Player (com WhipAttack via
|
||||
# `enterHurt`/`getBaseBox`): només van a l'executable principal.
|
||||
set(ENTITY_SOURCES
|
||||
${ENTITY_SOURCES_BASE}
|
||||
source/game/entities/concrete/whip_attack.cpp
|
||||
)
|
||||
|
||||
set(APP_SOURCES
|
||||
source/main.cpp
|
||||
source/core/system/director.cpp
|
||||
source/core/audio/audio.cpp
|
||||
source/core/audio/audio_adapter.cpp
|
||||
source/core/audio/audio_effects.cpp
|
||||
source/core/audio/jail_audio.cpp
|
||||
source/core/audio/sound_effects_config.cpp
|
||||
source/core/locale/locale.cpp
|
||||
source/core/ui/ui_config.cpp
|
||||
source/core/input/key_config.cpp
|
||||
source/core/input/global_inputs.cpp
|
||||
source/core/input/input_state.cpp
|
||||
source/core/input/gamepad_manager.cpp
|
||||
source/core/input/gamepad_bindings.cpp
|
||||
source/core/input/define_buttons.cpp
|
||||
source/core/input/mouse.cpp
|
||||
source/core/rendering/screen.cpp
|
||||
source/core/rendering/canvas.cpp
|
||||
@@ -76,6 +90,7 @@ set(APP_SOURCES
|
||||
source/core/rendering/sprite/sprite.cpp
|
||||
source/core/rendering/sprite/moving_sprite.cpp
|
||||
source/core/rendering/sprite/animated_sprite.cpp
|
||||
source/core/rendering/sprite/animated_sprite_parser.cpp
|
||||
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
||||
source/core/rendering/sdl3gpu/viewport.cpp
|
||||
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
|
||||
@@ -101,6 +116,7 @@ set(APP_SOURCES
|
||||
source/game/map/tilemap.cpp
|
||||
source/game/map/tilemap_writer.cpp
|
||||
source/game/map/paint_layer.cpp
|
||||
source/game/levels/level_manager.cpp
|
||||
# Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`).
|
||||
# En Release el fitxer compila a un object buit i l'enllaçador no
|
||||
# arrossega cap símbol del joc final.
|
||||
@@ -163,6 +179,15 @@ endif()
|
||||
|
||||
add_executable(${PROJECT_NAME} ${APP_SOURCES})
|
||||
|
||||
# Silencia warnings de codi de tercers (mateixa filosofia que el
|
||||
# .clang-tidy de source/external/). Només afecta aquests TUs concrets;
|
||||
# la resta del codi continua compilant amb -Wall -Wextra -Wpedantic.
|
||||
set_source_files_properties(
|
||||
source/external/stb_image_write_impl.cpp
|
||||
source/external/stb_vorbis_impl.cpp
|
||||
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations"
|
||||
)
|
||||
|
||||
if(NOT APPLE AND GLSLC_EXE)
|
||||
add_dependencies(${PROJECT_NAME} shaders)
|
||||
endif()
|
||||
@@ -326,7 +351,7 @@ if(BUILD_TESTS)
|
||||
source/core/resources/resource_helper.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
source/game/map/map.cpp
|
||||
${ENTITY_SOURCES}
|
||||
${ENTITY_SOURCES_BASE}
|
||||
)
|
||||
target_include_directories(test_entities_smoke PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/source"
|
||||
@@ -339,6 +364,29 @@ if(BUILD_TESTS)
|
||||
COMMAND test_entities_smoke
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
|
||||
# Smoke del WhipAttack. Inclou tot l'stack d'entitats + whip_attack.cpp.
|
||||
# `whip_attack_player_stubs.cpp` proveeix símbols mínims de Player per
|
||||
# satisfer el linker sense arrossegar audio/gamepad/screen — els tests
|
||||
# del whip s'enfoquen a hitbox + Mummy, no a friendly fire.
|
||||
add_executable(test_whip_attack
|
||||
tests/test_whip_attack.cpp
|
||||
tests/whip_attack_player_stubs.cpp
|
||||
source/core/resources/resource_helper.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
source/game/map/map.cpp
|
||||
${ENTITY_SOURCES_BASE}
|
||||
source/game/entities/concrete/whip_attack.cpp
|
||||
)
|
||||
target_include_directories(test_whip_attack PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/source"
|
||||
"${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
target_link_libraries(test_whip_attack PRIVATE SDL3::SDL3)
|
||||
target_compile_options(test_whip_attack PRIVATE -Wall -Wextra -Wpedantic)
|
||||
add_test(NAME whip_attack
|
||||
COMMAND test_whip_attack
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
|
||||
# Smoke del flux d'escenes i del parser de debug.yaml. Stand-alone:
|
||||
# només enllaça els helpers purs (debug_config, placeholder_input,
|
||||
# key_config) perquè la integració completa dels placeholders arrossegaria
|
||||
@@ -399,4 +447,21 @@ if(BUILD_TESTS)
|
||||
add_test(NAME game_fsm
|
||||
COMMAND test_game_fsm
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
|
||||
# Smoke del parser d'animacions amb suport de flip H/V. Stand-alone:
|
||||
# només enllaça `animated_sprite_parser.cpp`, que és la part del motor
|
||||
# que NO depèn de Resource::Cache ni de Surface — el parser és pure
|
||||
# YAML-in / dades-out, així que no cal arrossegar tot l'stack.
|
||||
add_executable(test_animated_sprite_flip
|
||||
tests/test_animated_sprite_flip.cpp
|
||||
source/core/rendering/sprite/animated_sprite_parser.cpp
|
||||
)
|
||||
target_include_directories(test_animated_sprite_flip PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/source"
|
||||
)
|
||||
target_link_libraries(test_animated_sprite_flip PRIVATE SDL3::SDL3)
|
||||
target_compile_options(test_animated_sprite_flip PRIVATE -Wall -Wextra -Wpedantic)
|
||||
add_test(NAME animated_sprite_flip
|
||||
COMMAND test_animated_sprite_flip
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
endif()
|
||||
@@ -1,13 +1,43 @@
|
||||
PROJECT := aee_2026
|
||||
BUILDDIR := build
|
||||
JOBS ?= $(shell nproc 2>/dev/null || echo 4)
|
||||
|
||||
# VERSION (única font de veritat: source/utils/defines.hpp)
|
||||
VERSION := v$(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
|
||||
# Detecció de plataforma. En Windows CMake defaulteja a "NMake Makefiles"
|
||||
# (que requereix Visual Studio); forcem MinGW Makefiles per usar el g++ de
|
||||
# MinGW. nproc tampoc existeix en cmd.exe → fem servir NUMBER_OF_PROCESSORS.
|
||||
ifeq ($(OS),Windows_NT)
|
||||
# Si Make corre dins MSYS2/Git Bash/MinGW, $(shell ...) usa sh.exe i
|
||||
# "NUL" NO és dispositiu — un redirect "2>NUL" crearia un fitxer literal
|
||||
# anomenat NUL al cwd (i en Windows es complica esborrar-lo). Detectem
|
||||
# MSYSTEM per usar /dev/null en aquests entorns; només cmd.exe pur necessita NUL.
|
||||
ifneq ($(MSYSTEM),)
|
||||
NULDEV := /dev/null
|
||||
else
|
||||
NULDEV := NUL
|
||||
endif
|
||||
JOBS ?= $(NUMBER_OF_PROCESSORS)
|
||||
# Si Ninja està al PATH, l'usem (més ràpid i paral·lelitza millor).
|
||||
# Si no, caiem a MinGW Makefiles. Sense aquesta selecció CMake
|
||||
# defaultejaria a "NMake Makefiles", que requereix Visual Studio.
|
||||
HAS_NINJA := $(shell ninja --version 2>$(NULDEV))
|
||||
ifneq ($(HAS_NINJA),)
|
||||
CMAKE_GEN := -G "Ninja"
|
||||
else
|
||||
CMAKE_GEN := -G "MinGW Makefiles"
|
||||
endif
|
||||
else
|
||||
NULDEV := /dev/null
|
||||
JOBS ?= $(shell nproc 2>/dev/null || echo 4)
|
||||
CMAKE_GEN :=
|
||||
endif
|
||||
|
||||
# VERSION (única font de veritat: source/utils/defines.hpp).
|
||||
# Lazy (=): només es calcula quan s'invoca un target que la usa
|
||||
# (show-version/help). Evita executar grep/sed des de cmd.exe en `make debug`.
|
||||
VERSION = v$(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
|
||||
|
||||
# GIT_HASH calculat al host i passat a CMake (evita problemes si CMake corre en
|
||||
# entorns sense accés a git, p.ex. Docker/emscripten).
|
||||
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null)
|
||||
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>$(NULDEV))
|
||||
ifeq ($(GIT_HASH),)
|
||||
GIT_HASH := unknown
|
||||
endif
|
||||
@@ -26,11 +56,11 @@ CMAKE_DEFS := -DGIT_HASH=$(GIT_HASH)
|
||||
# ==============================================================================
|
||||
|
||||
all:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||
|
||||
debug:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||
|
||||
run: all
|
||||
@@ -57,7 +87,7 @@ rebuild: clean all
|
||||
# del pack encara que res no hagi canviat dins data/. L'empaquetat també es fa
|
||||
# automàticament a cada `make`/`make debug` via el target CMake `resource_pack`.
|
||||
pack:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target pack_resources
|
||||
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
||||
|
||||
@@ -74,23 +104,23 @@ controllerdb:
|
||||
# CODE QUALITY (delegados a cmake)
|
||||
# ==============================================================================
|
||||
format:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target format
|
||||
|
||||
format-check:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target format-check
|
||||
|
||||
tidy:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target tidy
|
||||
|
||||
tidy-fix:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target tidy-fix
|
||||
|
||||
cppcheck:
|
||||
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target cppcheck
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
@@ -23,18 +23,37 @@ flags:
|
||||
collides: true
|
||||
interactable: false
|
||||
# Strip de 12 frames en graella 4×3 (mateix esquema que el jugador):
|
||||
# 0..3 → walk_down (cara avall)
|
||||
# 4..7 → walk_up (cara amunt)
|
||||
# 8..11 → walk (lateral; flip horitzontal segons el sentit)
|
||||
# 0..3 → walk_south (cara avall)
|
||||
# 4..7 → walk_north (cara amunt)
|
||||
# 8..11 → walk_east (lateral cap a l'est)
|
||||
# La variant `walk_west` reaprofita els frames de l'est amb `flip_h: true`:
|
||||
# el motor d'animació aplica el flip al sprite, així el codi de la mòmia
|
||||
# no decideix flip per direcció (només selecciona quina animació mostrar).
|
||||
# La momia no té variants `stand_*` perquè mai no para.
|
||||
animations:
|
||||
walk_down: { frames: [0, 1, 2, 3], fps: 8, loop: true }
|
||||
walk_up: { frames: [4, 5, 6, 7], fps: 8, loop: true }
|
||||
walk: { frames: [8, 9, 10, 11], fps: 8, loop: true }
|
||||
walk_south: { frames: [0, 1, 2, 3], fps: 8, loop: true }
|
||||
walk_north: { frames: [4, 5, 6, 7], fps: 8, loop: true }
|
||||
walk_east: { frames: [8, 9, 10, 11], fps: 8, loop: true }
|
||||
walk_west: { frames: [8, 9, 10, 11], fps: 8, loop: true, flip_h: true }
|
||||
ai:
|
||||
type: corridor_wanderer
|
||||
direction_choice: random
|
||||
initial_direction: right
|
||||
initial_direction: east
|
||||
stats:
|
||||
speed: 30
|
||||
damage: 1
|
||||
# Quan true, el contacte amb el jugador no aplica `hurt` ni knockback:
|
||||
# mata d'un sol toc i dispara el cicle DYING → respawn. El path `hurt`
|
||||
# del jugador segueix funcionant per a futurs hazards no-letals (avui
|
||||
# no n'hi ha cap que el dispari).
|
||||
kills_outright: true
|
||||
|
||||
# Estat HURT: post-fuetada, fugint en línia recta. Si la direcció
|
||||
# preferida (la del whip) està bloquejada, prova perpendiculars; si totes
|
||||
# ho estan, queda quieta tot el `duration_ms` i després torna a ROAMING.
|
||||
# `speed` opcional: si no apareix, s'usa `stats.speed`. El `whippable`
|
||||
# habilita la reacció al látigo (false ⇒ el whip la ignora).
|
||||
hurt:
|
||||
duration_ms: 600
|
||||
speed: 60
|
||||
whippable: true
|
||||
@@ -0,0 +1,38 @@
|
||||
name: whip_attack
|
||||
|
||||
# Quatre sprites distints, un per cardinal. El sistema d'animació actual
|
||||
# només permet UNA mida de frame per surface, i els sprites del látigo
|
||||
# tenen aspect ratios distints segons direcció. Per això són 4 fitxers
|
||||
# en lloc d'una sola sheet compartida amb flips.
|
||||
#
|
||||
# Mides (= `draw_rect` del WhipAttack, que NO es retalla per
|
||||
# bloquejadors):
|
||||
# E/W → 80×32 px per frame: 16 px de wind-up darrere del jugador
|
||||
# + 16 px de solapament + 48 px d'atac
|
||||
# frontal. Cobreix tota l'altura del jugador.
|
||||
# N/S → 16×96 px per frame: 48 px d'atac + 32 px solapant amb el jugador
|
||||
# + 16 px de wind-up "darrere".
|
||||
#
|
||||
# La caja de DANY és independent i més petita (48×16 / 16×48), pegada
|
||||
# al borde del jugador i retallada per murs/sarcs. La calcula
|
||||
# `WhipAttack::configure` a partir del `owner_box` del payload.
|
||||
sprites:
|
||||
east: { path: whip_east.gif, size: [80, 32] }
|
||||
west: { path: whip_west.gif, size: [80, 32] }
|
||||
north: { path: whip_north.gif, size: [16, 96] }
|
||||
south: { path: whip_south.gif, size: [16, 96] }
|
||||
|
||||
# Animació compartida entre les 4 direccions: la seqüència de frames i les
|
||||
# durades són idèntiques; només canvia quina surface/mida es clipa.
|
||||
# - `frames` : índex de tile dins l'spritesheet (graella deduïda).
|
||||
# - `frame_ms` : durada uniforme per frame; 50 ms × 4 ≈ 200 ms swing.
|
||||
# - `loop: false` : el látigo viu una animació i mor (`markDead`).
|
||||
# - `active_frames` : valors dins de `frames` durant els quals la
|
||||
# hitbox aplica dany. Wind-up (0) i recovery (3) no
|
||||
# danyen — només els frames "actius" 1 i 2.
|
||||
animations:
|
||||
swing:
|
||||
frames: [0, 1, 2, 3]
|
||||
frame_ms: 50
|
||||
loop: false
|
||||
active_frames: [1, 2]
|
||||
|
After Width: | Height: | Size: 313 B |
|
After Width: | Height: | Size: 378 B |
|
After Width: | Height: | Size: 369 B |
|
After Width: | Height: | Size: 313 B |
@@ -0,0 +1,132 @@
|
||||
# Font: CaesarDressing-Regular — generado desde CaesarDressing-Regular.ttf size 24
|
||||
# Generado con font_gen.py
|
||||
|
||||
box_width 18
|
||||
box_height 28
|
||||
columns 15
|
||||
|
||||
# codepoint_decimal ancho_visual
|
||||
32 8 # U+0020
|
||||
33 5 # !
|
||||
34 6 # "
|
||||
35 14 # #
|
||||
36 7 # $
|
||||
37 17 # %
|
||||
38 9 # &
|
||||
39 3 # '
|
||||
40 10 # (
|
||||
41 8 # )
|
||||
42 10 # *
|
||||
43 11 # +
|
||||
44 5 # ,
|
||||
45 10 # -
|
||||
46 3 # .
|
||||
47 9 # /
|
||||
48 12 # 0
|
||||
49 6 # 1
|
||||
50 10 # 2
|
||||
51 9 # 3
|
||||
52 12 # 4
|
||||
53 11 # 5
|
||||
54 13 # 6
|
||||
55 11 # 7
|
||||
56 13 # 8
|
||||
57 13 # 9
|
||||
58 5 # :
|
||||
59 4 # ;
|
||||
60 11 # <
|
||||
61 11 # =
|
||||
62 11 # >
|
||||
63 11 # ?
|
||||
64 14 # @
|
||||
65 14 # A
|
||||
66 12 # B
|
||||
67 11 # C
|
||||
68 12 # D
|
||||
69 10 # E
|
||||
70 10 # F
|
||||
71 15 # G
|
||||
72 12 # H
|
||||
73 9 # I
|
||||
74 9 # J
|
||||
75 13 # K
|
||||
76 10 # L
|
||||
77 18 # M
|
||||
78 12 # N
|
||||
79 13 # O
|
||||
80 12 # P
|
||||
81 12 # Q
|
||||
82 13 # R
|
||||
83 9 # S
|
||||
84 12 # T
|
||||
85 11 # U
|
||||
86 12 # V
|
||||
87 17 # W
|
||||
88 11 # X
|
||||
89 12 # Y
|
||||
90 10 # Z
|
||||
91 8 # [
|
||||
92 8 # \
|
||||
93 7 # ]
|
||||
94 11 # ^
|
||||
95 13 # _
|
||||
96 7 # `
|
||||
97 13 # a
|
||||
98 12 # b
|
||||
99 11 # c
|
||||
100 12 # d
|
||||
101 10 # e
|
||||
102 10 # f
|
||||
103 15 # g
|
||||
104 12 # h
|
||||
105 5 # i
|
||||
106 9 # j
|
||||
107 13 # k
|
||||
108 10 # l
|
||||
109 18 # m
|
||||
110 12 # n
|
||||
111 13 # o
|
||||
112 12 # p
|
||||
113 12 # q
|
||||
114 12 # r
|
||||
115 10 # s
|
||||
116 12 # t
|
||||
117 11 # u
|
||||
118 12 # v
|
||||
119 17 # w
|
||||
120 11 # x
|
||||
121 12 # y
|
||||
122 10 # z
|
||||
123 9 # {
|
||||
124 5 # |
|
||||
125 9 # }
|
||||
126 14 # ~
|
||||
192 14 # À
|
||||
193 14 # Á
|
||||
200 10 # È
|
||||
201 10 # É
|
||||
204 9 # Ì
|
||||
205 9 # Í
|
||||
210 13 # Ò
|
||||
211 13 # Ó
|
||||
217 11 # Ù
|
||||
218 11 # Ú
|
||||
209 12 # Ñ
|
||||
199 11 # Ç
|
||||
224 13 # à
|
||||
225 13 # á
|
||||
232 10 # è
|
||||
233 11 # é
|
||||
236 7 # ì
|
||||
237 8 # í
|
||||
242 13 # ò
|
||||
243 13 # ó
|
||||
249 11 # ù
|
||||
250 11 # ú
|
||||
241 12 # ñ
|
||||
231 11 # ç
|
||||
161 5 # ¡
|
||||
191 10 # ¿
|
||||
171 11 # «
|
||||
187 11 # »
|
||||
183 3 # ·
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -47,6 +47,15 @@ keys:
|
||||
- id: editor_picker
|
||||
code: "T"
|
||||
desc: "Obri/tanca el tilepicker dins de l'editor"
|
||||
- id: prev_room
|
||||
code: "["
|
||||
desc: "Salta a la sala anterior del levels.yaml (només Debug)"
|
||||
- id: next_room
|
||||
code: "]"
|
||||
desc: "Salta a la sala següent del levels.yaml (només Debug)"
|
||||
- id: open_door
|
||||
code: "Keypad 1"
|
||||
desc: "Força l'obertura de la porta de la sala (només Debug)"
|
||||
- id: exit
|
||||
code: "Escape"
|
||||
desc: "Eixir (confirmar amb segona pulsació)"
|
||||
@@ -37,11 +37,15 @@ hud:
|
||||
room: "Sala"
|
||||
|
||||
# Textos de la transició d'entrada/sortida de sala (FSM de GameScene).
|
||||
# `room_intro` accepta el placeholder `{n}` que es substitueix amb
|
||||
# l'índex de sala actual; veure `GameScene::formatRoomTitle`.
|
||||
# Tant l'INTRO com l'OUTRO es renderitzen com a dues línies separades
|
||||
# (cadascuna amb la seua animació). L'INTRO accepta els placeholders
|
||||
# `{p}` (piràmide) i `{r}` (sala); l'OUTRO no en té cap — són les
|
||||
# dues paraules del rètol "fase completada".
|
||||
game:
|
||||
room_intro: "Sala {n}"
|
||||
room_complete: "Fase completada"
|
||||
room_intro_pyramid: "Piramide {p}"
|
||||
room_intro_room: "Sala {r}"
|
||||
room_complete_line1: "Fase"
|
||||
room_complete_line2: "Completada"
|
||||
|
||||
menu:
|
||||
titles:
|
||||
@@ -67,8 +71,13 @@ menu:
|
||||
move_down: "Avall"
|
||||
move_left: "Esquerra"
|
||||
move_right: "Dreta"
|
||||
jump: "Saltar"
|
||||
attack: "Atacar"
|
||||
service: "Opcions"
|
||||
gamepad_p1: "Comandament P1"
|
||||
gamepad_p2: "Comandament P2"
|
||||
define_buttons_p1: "Definir botons P1"
|
||||
define_buttons_p2: "Definir botons P2"
|
||||
zoom: "Zoom"
|
||||
screen: "Pantalla completa"
|
||||
vsync: "VSync"
|
||||
@@ -100,3 +109,7 @@ menu:
|
||||
render_info_bottom: "Baix"
|
||||
gamepad_none: "Cap"
|
||||
gamepad_disconnected: "Desconnectat"
|
||||
press_key: "Polsa una tecla"
|
||||
press_button: "Polsa un botó"
|
||||
define_buttons_complete: "Configuració completa"
|
||||
define_buttons_no_pad: "Cap mando assignat"
|
||||
@@ -36,12 +36,15 @@ hud:
|
||||
pyramid: "Pyramid"
|
||||
room: "Room"
|
||||
|
||||
# Room transition texts (GameScene FSM intro/outro). The `room_intro`
|
||||
# string accepts a `{n}` placeholder that is replaced with the current
|
||||
# room index; see `GameScene::formatRoomTitle`.
|
||||
# Room transition texts (GameScene FSM intro/outro). Both INTRO and
|
||||
# OUTRO render as two independent lines with their own animation.
|
||||
# INTRO uses `{p}` (pyramid) and `{r}` (room) placeholders; OUTRO has
|
||||
# no placeholders — it's the two words of the "stage complete" label.
|
||||
game:
|
||||
room_intro: "Room {n}"
|
||||
room_complete: "Stage complete"
|
||||
room_intro_pyramid: "Pyramid {p}"
|
||||
room_intro_room: "Room {r}"
|
||||
room_complete_line1: "Stage"
|
||||
room_complete_line2: "Complete"
|
||||
|
||||
menu:
|
||||
titles:
|
||||
@@ -54,6 +57,7 @@ menu:
|
||||
system: "System"
|
||||
labels:
|
||||
player_1: "Player 1"
|
||||
player_2: "Player 2"
|
||||
gamepad_unavailable: "Not available"
|
||||
items:
|
||||
video_submenu: "Video"
|
||||
@@ -66,6 +70,13 @@ menu:
|
||||
move_down: "Down"
|
||||
move_left: "Left"
|
||||
move_right: "Right"
|
||||
jump: "Jump"
|
||||
attack: "Attack"
|
||||
service: "Options"
|
||||
gamepad_p1: "Gamepad P1"
|
||||
gamepad_p2: "Gamepad P2"
|
||||
define_buttons_p1: "Define buttons P1"
|
||||
define_buttons_p2: "Define buttons P2"
|
||||
zoom: "Zoom"
|
||||
screen: "Fullscreen"
|
||||
vsync: "VSync"
|
||||
@@ -95,3 +106,9 @@ menu:
|
||||
render_info_off: "Off"
|
||||
render_info_top: "Top"
|
||||
render_info_bottom: "Bottom"
|
||||
gamepad_none: "None"
|
||||
gamepad_disconnected: "Disconnected"
|
||||
press_key: "Press a key"
|
||||
press_button: "Press a button"
|
||||
define_buttons_complete: "Configuration complete"
|
||||
define_buttons_no_pad: "No gamepad assigned"
|
||||
@@ -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 pyramid_1.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 1
|
||||
room: 1
|
||||
tileset: data/tilesets/pyramid_1.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_1.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 1
|
||||
room: 2
|
||||
tileset: data/tilesets/pyramid_1.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_1.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 1
|
||||
room: 3
|
||||
tileset: data/tilesets/pyramid_1.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_1.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 1
|
||||
room: 4
|
||||
tileset: data/tilesets/pyramid_1.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_1.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 1
|
||||
room: 5
|
||||
tileset: data/tilesets/pyramid_1.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_1.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 1
|
||||
room: 6
|
||||
tileset: data/tilesets/pyramid_1.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_2.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 2
|
||||
room: 1
|
||||
tileset: data/tilesets/pyramid_2.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_2.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 2
|
||||
room: 2
|
||||
tileset: data/tilesets/pyramid_2.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_2.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 2
|
||||
room: 3
|
||||
tileset: data/tilesets/pyramid_2.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_2.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 2
|
||||
room: 4
|
||||
tileset: data/tilesets/pyramid_2.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_2.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 2
|
||||
room: 5
|
||||
tileset: data/tilesets/pyramid_2.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_2.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 2
|
||||
room: 6
|
||||
tileset: data/tilesets/pyramid_2.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_3.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 3
|
||||
room: 1
|
||||
tileset: data/tilesets/pyramid_3.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_3.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 3
|
||||
room: 2
|
||||
tileset: data/tilesets/pyramid_3.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_3.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 3
|
||||
room: 3
|
||||
tileset: data/tilesets/pyramid_3.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_3.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 3
|
||||
room: 4
|
||||
tileset: data/tilesets/pyramid_3.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_3.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 3
|
||||
room: 5
|
||||
tileset: data/tilesets/pyramid_3.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_3.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 3
|
||||
room: 6
|
||||
tileset: data/tilesets/pyramid_3.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_4.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 4
|
||||
room: 1
|
||||
tileset: data/tilesets/pyramid_4.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_4.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 4
|
||||
room: 2
|
||||
tileset: data/tilesets/pyramid_4.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_4.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 4
|
||||
room: 3
|
||||
tileset: data/tilesets/pyramid_4.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_4.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 4
|
||||
room: 4
|
||||
tileset: data/tilesets/pyramid_4.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_4.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 4
|
||||
room: 5
|
||||
tileset: data/tilesets/pyramid_4.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_4.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 4
|
||||
room: 6
|
||||
tileset: data/tilesets/pyramid_4.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_5.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 5
|
||||
room: 1
|
||||
tileset: data/tilesets/pyramid_5.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_5.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 5
|
||||
room: 2
|
||||
tileset: data/tilesets/pyramid_5.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_5.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 5
|
||||
room: 3
|
||||
tileset: data/tilesets/pyramid_5.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_5.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 5
|
||||
room: 4
|
||||
tileset: data/tilesets/pyramid_5.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_5.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 5
|
||||
room: 5
|
||||
tileset: data/tilesets/pyramid_5.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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 pyramid_5.gif.
|
||||
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
|
||||
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
|
||||
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
|
||||
#
|
||||
# Format del tilemap:
|
||||
# `data` és una llista de files; cada fila és una llista d'índexs del
|
||||
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
|
||||
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 5
|
||||
room: 6
|
||||
tileset: data/tilesets/pyramid_5.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||
# per desactivar el pintat per a aquest jugador.
|
||||
player1_paint_tile: 0
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
|
||||
# s'anul·len (sense push, sense moviment).
|
||||
push_factor: 0.30
|
||||
|
||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
||||
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
|
||||
# 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:
|
||||
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
|
||||
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
|
||||
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
|
||||
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
|
||||
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
|
||||
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
|
||||
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
|
||||
|
||||
# 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]
|
||||
@@ -19,7 +19,7 @@ origin: [-8, 0]
|
||||
# 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
|
||||
player2_paint_tile: 1
|
||||
|
||||
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
|
||||
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -1,113 +1,97 @@
|
||||
# player animation
|
||||
sprite:
|
||||
path: morcus.gif
|
||||
size: [16, 32]
|
||||
|
||||
# Geometria de col·lisió del jugador (independent de l'animació):
|
||||
# size → costat del quadrat de col·lisió (16×16)
|
||||
# base_size → costat del quadrat visual inferior del sprite (els "peus");
|
||||
# s'usa per centrar la caixa de col·lisió sobre ell.
|
||||
# Geometria de col·lisió. `size` és la caixa contra murs/sarcs;
|
||||
# `hit_size`, la caixa contra altres actors (més petita); `height`,
|
||||
# l'alçada que un salt ha de superar per passar per damunt.
|
||||
collision:
|
||||
size: 16
|
||||
base_size: 16
|
||||
# Caixa de contacte actor-actor (jugador↔jugador, jugador↔momia/trampa).
|
||||
# Més petita que `size` perquè els contactes d'acció siguin precisos;
|
||||
# `size` es reserva per a la col·lisió amb murs/sarcòfags i per enfilar
|
||||
# els passadissos. Centrada en el mateix punt que la caixa base.
|
||||
hit_size: 8
|
||||
# Alçada del cos en píxels. Funció DOBLE segons amb què col·lisione:
|
||||
# · Contra entitats (mummy, rolling_rock, sarc): si `jump_z > height`
|
||||
# d'aquella entitat, no hi ha col·lisió. Igual que als YAMLs
|
||||
# d'entitats.
|
||||
# · Contra altres jugadors: la col·lisió és binària per estat aeri.
|
||||
# Si un està en l'aire i l'altre no → cap col·lisió. Si tots dos
|
||||
# estan en l'aire, llavors `body_height` decideix si els rangs
|
||||
# verticals solapen i s'aplica el rebot mid-aire. `0` = sense cos
|
||||
# (ni rebot quan tots dos volen).
|
||||
height: 12
|
||||
|
||||
# Animacions en mapa (clau = nom). Camps per animació:
|
||||
# frames → llista d'índexs dins de l'spritesheet.
|
||||
# frame_ms → durada de cada frame en ms. Escalar = uniforme; llista =
|
||||
# per-frame (mateixa mida que `frames`).
|
||||
# loop → true per defecte. `loop: false` = un sol tret (es queda al
|
||||
# darrer frame i `animationIsCompleted()` passa a true).
|
||||
# loop_from → rebota a aquest índex en passar del final (implica loop).
|
||||
animations:
|
||||
stand_down: { frames: [0] }
|
||||
stand_up: { frames: [4] }
|
||||
stand: { frames: [8] }
|
||||
jump_down: { frames: [12] }
|
||||
jump_up: { frames: [13] }
|
||||
jump: { frames: [14] }
|
||||
stun_down: { frames: [16] }
|
||||
stun_up: { frames: [17] }
|
||||
stun: { frames: [18] }
|
||||
# Les variants `*_west` reaprofiten els frames de l'est i declaren
|
||||
# `flip_h: true` perquè el motor d'animació apliqui el flip al sprite —
|
||||
# el codi del joc no toca mai `flip_` directament.
|
||||
stand_south: { frames: [0] }
|
||||
stand_north: { frames: [4] }
|
||||
stand_east: { frames: [8] }
|
||||
stand_west: { frames: [8], flip_h: true }
|
||||
|
||||
walk_down:
|
||||
frames: [0, 1, 2, 3]
|
||||
frame_ms: 100
|
||||
jump_south: { frames: [12] }
|
||||
jump_north: { frames: [13] }
|
||||
jump_east: { frames: [14] }
|
||||
jump_west: { frames: [14], flip_h: true }
|
||||
|
||||
walk_up:
|
||||
frames: [4, 5, 6, 7]
|
||||
frame_ms: 100
|
||||
stun_south: { frames: [16] }
|
||||
stun_north: { frames: [17] }
|
||||
stun_east: { frames: [18] }
|
||||
stun_west: { frames: [18], flip_h: true }
|
||||
|
||||
walk:
|
||||
frames: [8, 9, 10, 11]
|
||||
frame_ms: 100
|
||||
walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
||||
walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
||||
walk_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
||||
walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
||||
|
||||
hurt_down:
|
||||
frames: [0, 1, 2, 3]
|
||||
frame_ms: 100
|
||||
hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
||||
hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
||||
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
||||
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
||||
|
||||
hurt_up:
|
||||
frames: [4, 5, 6, 7]
|
||||
frame_ms: 100
|
||||
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 }
|
||||
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 }
|
||||
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 }
|
||||
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true }
|
||||
|
||||
hurt:
|
||||
frames: [8, 9, 10, 11]
|
||||
frame_ms: 100
|
||||
recover_south: { frames: [23] }
|
||||
recover_north: { frames: [27] }
|
||||
recover_east: { frames: [31] }
|
||||
recover_west: { frames: [31], flip_h: true }
|
||||
|
||||
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
||||
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
||||
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||
# El `threshold_ms` filtra els tocs curts: si el temps acumulat de
|
||||
# moviment continu no ha arribat al llindar, la parada és instantània
|
||||
# (permet "passets" d'un tile sense rellisquera). L'acumulador es
|
||||
# reseteja només quan no hi ha ni input ni velocitat residual, així
|
||||
# els canvis de direcció enmig del coast no rearmen el llindar.
|
||||
# `brake_time_ms: 0` o absència del bloc = inèrcia desactivada.
|
||||
# Animació de mort: 3 voltes horàries començant pel sud (south →
|
||||
# west → north → east, repetit 3×). Reaprofita els frames stand_*; el
|
||||
# west és l'east amb `flip_h: true` declarat per-frame (mapping form,
|
||||
# no hereta del nivell d'anim). `frame_ms` per-frame creix linealment
|
||||
# per donar la sensació de desaccelerar fins a aturar-se. `loop: false`
|
||||
# → es queda al darrer frame i marca completed.
|
||||
death_spinning:
|
||||
frames:
|
||||
- 0
|
||||
- { index: 8, flip_h: true }
|
||||
- 4
|
||||
- 8
|
||||
- 0
|
||||
- { index: 8, flip_h: true }
|
||||
- 4
|
||||
- 8
|
||||
- 0
|
||||
- { index: 8, flip_h: true }
|
||||
- 4
|
||||
- 8
|
||||
frame_ms: [60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280]
|
||||
loop: false
|
||||
|
||||
# Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra
|
||||
# tocs curts (passets d'un tile sense rellisquera).
|
||||
inertia:
|
||||
brake_time_ms: 150
|
||||
threshold_ms: 300
|
||||
|
||||
# Gir de 180°: mentre dura, el facing queda fixat a la direcció
|
||||
# intermèdia (efecte purament visual; el moviment continua sense
|
||||
# alterar-se). Els girs de 90° no passen per cap intermedi.
|
||||
# Gir de 180°: durant `duration_ms` el facing es fixa a la direcció
|
||||
# intermèdia indicada (purament visual).
|
||||
turn:
|
||||
duration_ms: 80
|
||||
# Direcció intermèdia per a cada gir de 180° (sentit horari).
|
||||
transitions:
|
||||
west_to_east: down # passa per baix (stand_down)
|
||||
east_to_west: up # passa per dalt (stand_up)
|
||||
north_to_south: east # passa per la dreta (stand, sense flip)
|
||||
south_to_north: west # passa per l'esquerra (stand, amb flip)
|
||||
west_to_east: south
|
||||
east_to_west: north
|
||||
north_to_south: east
|
||||
south_to_north: west
|
||||
|
||||
# Salt vertical (px verticals "visuals", no afecta la posició al món).
|
||||
# impulse → velocitat vertical inicial (px/s) en deixar el sòl.
|
||||
# gravity → acceleració descendent constant (px/s²).
|
||||
# landing_stun_ms → temps en què el jugador queda paralitzat en aterrar
|
||||
# (no es pot moure ni tornar a saltar).
|
||||
# shadow_y_offset → desplaçament vertical de la sombra (px, positiu =
|
||||
# puja). Compensa la franja transparent que els
|
||||
# sprites del jugador tenen sota els peus reals
|
||||
# perquè camine "pel mig del passadís" (3 px aquí).
|
||||
# sound → fitxer .wav reproduït al despegue (relatiu a
|
||||
# data/sounds/). Buit o absent = sense so.
|
||||
# land_sound → fitxer .wav reproduït en aterrar.
|
||||
# L'alçada del cos viu al bloc `collision:` (`height`), igual que la resta
|
||||
# d'entitats: és una propietat de col·lisió, no de la física del salt.
|
||||
# Amb impulse=240 i gravity=600: pic ≈ 48 px, temps total en l'aire ≈ 0.8 s.
|
||||
# Salt vertical (px de z, no toca la posició al món).
|
||||
# `shadow_y_offset`: puja la sombra per compensar la franja
|
||||
# transparent sota els peus del sprite.
|
||||
jump:
|
||||
impulse: 200
|
||||
gravity: 600
|
||||
@@ -116,14 +100,27 @@ jump:
|
||||
sound: jump.wav
|
||||
land_sound: land.wav
|
||||
|
||||
# Estat HURT: el jugador queda projectat sense control per un impacte
|
||||
# (algú que cau al damunt; en el futur també fuetada de l'altre jugador).
|
||||
# `enterHurt` tria automàticament la direcció amb més recorregut lliure
|
||||
# entre els 4 cardinals, posa la velocitat inicial a `speed` i la fa
|
||||
# decaure linealment fins a 0 en `brake_time_ms`. Si pel camí xoca amb
|
||||
# un mur, para en sec. En tornar a velocitat 0, recupera l'estat
|
||||
# GROUNDED i el control. Mentre dura, no col·lisiona amb altres jugadors.
|
||||
# Amb speed=120 i brake_time=500ms ⇒ recorregut ≈ 30 px (≈2 tiles).
|
||||
# Projecció post-impacte: surt cap a la direcció amb més recorregut
|
||||
# lliure i frena fins a 0 en `brake_time_ms`.
|
||||
hurt:
|
||||
speed: 120
|
||||
brake_time_ms: 500
|
||||
sound: hurt.wav
|
||||
|
||||
# Atac amb làtigo. `range_tiles` és la longitud abans de retallar
|
||||
# contra murs/sarcs; `cooldown_ms` allarga ATTACKING amb una
|
||||
# recuperació en què la hitbox ja no fa dany.
|
||||
whip:
|
||||
range_tiles: 3
|
||||
swing_ms: 200
|
||||
cooldown_ms: 200
|
||||
damage: 1
|
||||
sound: whip.wav
|
||||
|
||||
# Mort + respawn. `respawn_delay_ms` és la pausa entre la fi de
|
||||
# `death_spinning` i la reaparició al spawn. `lives` és el comptador
|
||||
# inicial d'intents (estil Mario): cada respawn el decrementa en 1, pot
|
||||
# anar a negatius (no hi ha game over).
|
||||
death:
|
||||
respawn_delay_ms: 500
|
||||
lives: 3
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -1,113 +1,97 @@
|
||||
# player animation
|
||||
sprite:
|
||||
path: sam.gif
|
||||
size: [16, 32]
|
||||
|
||||
# Geometria de col·lisió del jugador (independent de l'animació):
|
||||
# size → costat del quadrat de col·lisió (16×16)
|
||||
# base_size → costat del quadrat visual inferior del sprite (els "peus");
|
||||
# s'usa per centrar la caixa de col·lisió sobre ell.
|
||||
# Geometria de col·lisió. `size` és la caixa contra murs/sarcs;
|
||||
# `hit_size`, la caixa contra altres actors (més petita); `height`,
|
||||
# l'alçada que un salt ha de superar per passar per damunt.
|
||||
collision:
|
||||
size: 16
|
||||
base_size: 16
|
||||
# Caixa de contacte actor-actor (jugador↔jugador, jugador↔momia/trampa).
|
||||
# Més petita que `size` perquè els contactes d'acció siguin precisos;
|
||||
# `size` es reserva per a la col·lisió amb murs/sarcòfags i per enfilar
|
||||
# els passadissos. Centrada en el mateix punt que la caixa base.
|
||||
hit_size: 8
|
||||
# Alçada del cos en píxels. Funció DOBLE segons amb què col·lisione:
|
||||
# · Contra entitats (mummy, rolling_rock, sarc): si `jump_z > height`
|
||||
# d'aquella entitat, no hi ha col·lisió. Igual que als YAMLs
|
||||
# d'entitats.
|
||||
# · Contra altres jugadors: la col·lisió és binària per estat aeri.
|
||||
# Si un està en l'aire i l'altre no → cap col·lisió. Si tots dos
|
||||
# estan en l'aire, llavors `body_height` decideix si els rangs
|
||||
# verticals solapen i s'aplica el rebot mid-aire. `0` = sense cos
|
||||
# (ni rebot quan tots dos volen).
|
||||
height: 12
|
||||
|
||||
# Animacions en mapa (clau = nom). Camps per animació:
|
||||
# frames → llista d'índexs dins de l'spritesheet.
|
||||
# frame_ms → durada de cada frame en ms. Escalar = uniforme; llista =
|
||||
# per-frame (mateixa mida que `frames`).
|
||||
# loop → true per defecte. `loop: false` = un sol tret (es queda al
|
||||
# darrer frame i `animationIsCompleted()` passa a true).
|
||||
# loop_from → rebota a aquest índex en passar del final (implica loop).
|
||||
animations:
|
||||
stand_down: { frames: [1] }
|
||||
stand_up: { frames: [5] }
|
||||
stand: { frames: [9] }
|
||||
jump_down: { frames: [12] }
|
||||
jump_up: { frames: [13] }
|
||||
jump: { frames: [14] }
|
||||
stun_down: { frames: [16] }
|
||||
stun_up: { frames: [17] }
|
||||
stun: { frames: [18] }
|
||||
# Les variants `*_west` reaprofiten els frames de l'est i declaren
|
||||
# `flip_h: true` perquè el motor d'animació apliqui el flip al sprite —
|
||||
# el codi del joc no toca mai `flip_` directament.
|
||||
stand_south: { frames: [0] }
|
||||
stand_north: { frames: [4] }
|
||||
stand_east: { frames: [8] }
|
||||
stand_west: { frames: [8], flip_h: true }
|
||||
|
||||
walk_down:
|
||||
frames: [0, 1, 2, 3]
|
||||
frame_ms: 100
|
||||
jump_south: { frames: [12] }
|
||||
jump_north: { frames: [13] }
|
||||
jump_east: { frames: [14] }
|
||||
jump_west: { frames: [14], flip_h: true }
|
||||
|
||||
walk_up:
|
||||
frames: [4, 5, 6, 7]
|
||||
frame_ms: 100
|
||||
stun_south: { frames: [16] }
|
||||
stun_north: { frames: [17] }
|
||||
stun_east: { frames: [18] }
|
||||
stun_west: { frames: [18], flip_h: true }
|
||||
|
||||
walk:
|
||||
frames: [8, 9, 10, 11]
|
||||
frame_ms: 100
|
||||
walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
||||
walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
||||
walk_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
||||
walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
||||
|
||||
hurt_down:
|
||||
frames: [0, 1, 2, 3]
|
||||
frame_ms: 100
|
||||
hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
||||
hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
||||
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
||||
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
||||
|
||||
hurt_up:
|
||||
frames: [4, 5, 6, 7]
|
||||
frame_ms: 100
|
||||
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 }
|
||||
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 }
|
||||
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 }
|
||||
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true }
|
||||
|
||||
hurt:
|
||||
frames: [8, 9, 10, 11]
|
||||
frame_ms: 100
|
||||
recover_south: { frames: [23] }
|
||||
recover_north: { frames: [27] }
|
||||
recover_east: { frames: [31] }
|
||||
recover_west: { frames: [31], flip_h: true }
|
||||
|
||||
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
||||
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
||||
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||
# El `threshold_ms` filtra els tocs curts: si el temps acumulat de
|
||||
# moviment continu no ha arribat al llindar, la parada és instantània
|
||||
# (permet "passets" d'un tile sense rellisquera). L'acumulador es
|
||||
# reseteja només quan no hi ha ni input ni velocitat residual, així
|
||||
# els canvis de direcció enmig del coast no rearmen el llindar.
|
||||
# `brake_time_ms: 0` o absència del bloc = inèrcia desactivada.
|
||||
# Animació de mort: 3 voltes horàries començant pel sud (south →
|
||||
# west → north → east, repetit 3×). Reaprofita els frames stand_*; el
|
||||
# west és l'east amb `flip_h: true` declarat per-frame (mapping form,
|
||||
# no hereta del nivell d'anim). `frame_ms` per-frame creix linealment
|
||||
# per donar la sensació de desaccelerar fins a aturar-se. `loop: false`
|
||||
# → es queda al darrer frame i marca completed.
|
||||
death_spinning:
|
||||
frames:
|
||||
- 0
|
||||
- { index: 8, flip_h: true }
|
||||
- 4
|
||||
- 8
|
||||
- 0
|
||||
- { index: 8, flip_h: true }
|
||||
- 4
|
||||
- 8
|
||||
- 0
|
||||
- { index: 8, flip_h: true }
|
||||
- 4
|
||||
- 8
|
||||
frame_ms: [60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280]
|
||||
loop: false
|
||||
|
||||
# Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra
|
||||
# tocs curts (passets d'un tile sense rellisquera).
|
||||
inertia:
|
||||
brake_time_ms: 150
|
||||
threshold_ms: 300
|
||||
|
||||
# Gir de 180°: mentre dura, el facing queda fixat a la direcció
|
||||
# intermèdia (efecte purament visual; el moviment continua sense
|
||||
# alterar-se). Els girs de 90° no passen per cap intermedi.
|
||||
# Gir de 180°: durant `duration_ms` el facing es fixa a la direcció
|
||||
# intermèdia indicada (purament visual).
|
||||
turn:
|
||||
duration_ms: 80
|
||||
# Direcció intermèdia per a cada gir de 180° (sentit horari).
|
||||
transitions:
|
||||
west_to_east: down # passa per baix (stand_down)
|
||||
east_to_west: up # passa per dalt (stand_up)
|
||||
north_to_south: east # passa per la dreta (stand, sense flip)
|
||||
south_to_north: west # passa per l'esquerra (stand, amb flip)
|
||||
west_to_east: south
|
||||
east_to_west: north
|
||||
north_to_south: east
|
||||
south_to_north: west
|
||||
|
||||
# Salt vertical (px verticals "visuals", no afecta la posició al món).
|
||||
# impulse → velocitat vertical inicial (px/s) en deixar el sòl.
|
||||
# gravity → acceleració descendent constant (px/s²).
|
||||
# landing_stun_ms → temps en què el jugador queda paralitzat en aterrar
|
||||
# (no es pot moure ni tornar a saltar).
|
||||
# shadow_y_offset → desplaçament vertical de la sombra (px, positiu =
|
||||
# puja). Compensa la franja transparent que els
|
||||
# sprites del jugador tenen sota els peus reals
|
||||
# perquè camine "pel mig del passadís" (3 px aquí).
|
||||
# sound → fitxer .wav reproduït al despegue (relatiu a
|
||||
# data/sounds/). Buit o absent = sense so.
|
||||
# land_sound → fitxer .wav reproduït en aterrar.
|
||||
# L'alçada del cos viu al bloc `collision:` (`height`), igual que la resta
|
||||
# d'entitats: és una propietat de col·lisió, no de la física del salt.
|
||||
# Amb impulse=240 i gravity=600: pic ≈ 48 px, temps total en l'aire ≈ 0.8 s.
|
||||
# Salt vertical (px de z, no toca la posició al món).
|
||||
# `shadow_y_offset`: puja la sombra per compensar la franja
|
||||
# transparent sota els peus del sprite.
|
||||
jump:
|
||||
impulse: 200
|
||||
gravity: 600
|
||||
@@ -116,14 +100,27 @@ jump:
|
||||
sound: jump.wav
|
||||
land_sound: land.wav
|
||||
|
||||
# Estat HURT: el jugador queda projectat sense control per un impacte
|
||||
# (algú que cau al damunt; en el futur també fuetada de l'altre jugador).
|
||||
# `enterHurt` tria automàticament la direcció amb més recorregut lliure
|
||||
# entre els 4 cardinals, posa la velocitat inicial a `speed` i la fa
|
||||
# decaure linealment fins a 0 en `brake_time_ms`. Si pel camí xoca amb
|
||||
# un mur, para en sec. En tornar a velocitat 0, recupera l'estat
|
||||
# GROUNDED i el control. Mentre dura, no col·lisiona amb altres jugadors.
|
||||
# Amb speed=120 i brake_time=500ms ⇒ recorregut ≈ 30 px (≈2 tiles).
|
||||
# Projecció post-impacte: surt cap a la direcció amb més recorregut
|
||||
# lliure i frena fins a 0 en `brake_time_ms`.
|
||||
hurt:
|
||||
speed: 120
|
||||
brake_time_ms: 500
|
||||
sound: hurt.wav
|
||||
|
||||
# Atac amb làtigo. `range_tiles` és la longitud abans de retallar
|
||||
# contra murs/sarcs; `cooldown_ms` allarga ATTACKING amb una
|
||||
# recuperació en què la hitbox ja no fa dany.
|
||||
whip:
|
||||
range_tiles: 3
|
||||
swing_ms: 200
|
||||
cooldown_ms: 200
|
||||
damage: 1
|
||||
sound: whip.wav
|
||||
|
||||
# Mort + respawn. `respawn_delay_ms` és la pausa entre la fi de
|
||||
# `death_spinning` i la reaparició al spawn. `lives` és el comptador
|
||||
# inicial d'intents (estil Mario): cada respawn el decrementa en 1, pot
|
||||
# anar a negatius (no hi ha game over).
|
||||
death:
|
||||
respawn_delay_ms: 500
|
||||
lives: 3
|
||||
@@ -5,20 +5,44 @@
|
||||
# fallbacks compilats; veure `readSceneConfig` a game_scene.cpp.
|
||||
|
||||
room_intro:
|
||||
fade_in_ms: 500 # negre opac → món visible
|
||||
text_delay_ms: 500 # espera amb el món net abans de sortir el text
|
||||
text_enter_ms: 500 # text esquerra → centre
|
||||
text_hold_ms: 1000 # text quiet al centre
|
||||
text_exit_ms: 500 # text centre → fora per l'esquerra
|
||||
fade_in_ms: 500 # negre opac → món visible
|
||||
text_delay_ms: 500 # espera amb el món net abans de sortir el text
|
||||
text_enter_ms: 400 # text esquerra → centre
|
||||
text_hold_ms: 2000 # text quiet al centre
|
||||
text_exit_ms: 400 # text centre → fora per l'esquerra
|
||||
line_gap_px: -4 # px extra entre les dues línies (piràmide / sala)
|
||||
# Delays d'spawn aeri **mesurats des de la fi de la música d'INTRO**
|
||||
# (callback `Audio::setOnMusicEnded`). 0 = cau al moment exacte que
|
||||
# `room_intro.ogg` acaba; valors > 0 escalonen els jugadors. En mode
|
||||
# single-player s'usa només `player_spawn_delay_ms`.
|
||||
player_spawn_delay_ms: 0
|
||||
player2_spawn_delay_ms: 300
|
||||
# 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
|
||||
|
||||
room_outro:
|
||||
text_enter_ms: 500
|
||||
text_hold_ms: 1000
|
||||
text_exit_ms: 500
|
||||
text_enter_ms: 400
|
||||
text_hold_ms: 2000
|
||||
text_exit_ms: 400
|
||||
fade_out_ms: 500 # món visible → negre opac
|
||||
line_gap_px: -4 # px extra entre les dues línies (fase / completada)
|
||||
black_hold_ms: 500 # pausa en negre abans d'entrar a la pròxima sala
|
||||
|
||||
text:
|
||||
# Noms d'easing tal com els resol UiConfig::curveByName (vegeu
|
||||
# `data/ui.yaml`). El text entra suau i surt accelerant.
|
||||
enter_curve: outCubic
|
||||
exit_curve: inCubic
|
||||
|
||||
# Panel translúcid pintat sota el rètol de transició (INTRO i OUTRO).
|
||||
# Va centrat verticalment al centre del playfield i ocupa tota
|
||||
# l'amplada del canvas. Creix d'altura amb la mateixa enter_curve del
|
||||
# text i decreix amb l'exit_curve. `color` són tres canals RGB (0..255);
|
||||
# `alpha` és la transparència del blend (0 = invisible, 255 = opac).
|
||||
text_panel:
|
||||
color: [36, 182, 170]
|
||||
alpha: 128
|
||||
height_px: 32
|
||||
@@ -0,0 +1,37 @@
|
||||
# Presets d'efectes per a sons puntuals. Les crides
|
||||
# Audio::playSoundWithEcho(name, preset, group) i
|
||||
# Audio::playSoundWithReverb(name, preset, group) referencien aquests
|
||||
# presets pel seu nom (camp `preset`). Si el nom no existeix, el so es
|
||||
# reprodueix sec amb un avís a stderr.
|
||||
#
|
||||
# Camps:
|
||||
# echo:
|
||||
# delay_ms → temps fins al primer rebot, en mil·lisegons.
|
||||
# feedback → reinjecció (0..0.95). >0.7 sona "metàl·lic".
|
||||
# wet → mescla del senyal humit (0..1). 1 = només eco.
|
||||
# reverb:
|
||||
# room_size → mida percebuda de la sala (0..1).
|
||||
# damping → atenuació de freqüències altes en cada rebot (0..1).
|
||||
# wet → mescla humida (0..1).
|
||||
#
|
||||
# Els processadors limiten la cua a 800 ms (echo) i 1500 ms (reverb).
|
||||
|
||||
echo:
|
||||
cave:
|
||||
delay_ms: 220
|
||||
feedback: 0.45
|
||||
wet: 0.35
|
||||
short:
|
||||
delay_ms: 80
|
||||
feedback: 0.30
|
||||
wet: 0.25
|
||||
|
||||
reverb:
|
||||
stone_hall:
|
||||
room_size: 0.78
|
||||
damping: 0.50
|
||||
wet: 0.40
|
||||
small_room:
|
||||
room_size: 0.40
|
||||
damping: 0.70
|
||||
wet: 0.30
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -20,6 +20,12 @@ colors:
|
||||
hud_label: 58
|
||||
hud_value: 187
|
||||
hud_flash: 237
|
||||
# Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). Independents
|
||||
# de notification_* perquè la fase i les notificacions són dos canals
|
||||
# visuals distints: ací pots canviar el color del rètol sense tocar
|
||||
# els missatges del HUD.
|
||||
room_text: 160
|
||||
room_text_outline: 253
|
||||
|
||||
timings:
|
||||
notification_ms: 4000
|
||||
@@ -36,6 +42,12 @@ fonts:
|
||||
gif: "data/fonts/ari-w9500-condensed-display.gif"
|
||||
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
|
||||
kerning: 1
|
||||
# Font del rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO).
|
||||
room_text:
|
||||
name: "CaesarDressing-Regular"
|
||||
gif: "data/fonts/CaesarDressing-Regular.gif"
|
||||
fnt: "data/fonts/CaesarDressing-Regular.fnt"
|
||||
kerning: 1
|
||||
menu_title:
|
||||
name: "ari-w9500-display"
|
||||
gif: "data/fonts/ari-w9500-display.gif"
|
||||
@@ -48,7 +60,7 @@ fonts:
|
||||
kerning: 0
|
||||
|
||||
hud:
|
||||
bg_enabled: true
|
||||
bg_enabled: false
|
||||
entry_ms: 800 # durada de l'animació d'entrada dels tres blocs
|
||||
flash_ms: 250 # durada del flash quan un jugador rep un hit
|
||||
shake_ms: 300 # durada del shake que acompanya el flash
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
#include <algorithm> // Per a clamp
|
||||
#include <cstdio> // Per a std::fprintf
|
||||
|
||||
#include "core/audio/audio_adapter.hpp" // Per a AudioResource::getMusic/getSound
|
||||
#include "core/audio/jail_audio.hpp" // Per a Ja::* (motor jailgames)
|
||||
#include "utils/defaults.hpp" // Per a Defaults::Audio::FREQUENCY
|
||||
#include "core/audio/audio_adapter.hpp" // Per a AudioResource::getMusic/getSound
|
||||
#include "core/audio/jail_audio.hpp" // Per a Ja::* (motor jailgames)
|
||||
#include "core/audio/sound_effects_config.hpp" // Per a SoundEffectsConfig
|
||||
#include "utils/defaults.hpp" // Per a Defaults::Audio::FREQUENCY
|
||||
|
||||
// Invariant compile-time: tots els valors d'Audio::Group han de cabre als slots
|
||||
// de volum per grup que manté l'engine. Si s'afegeix una nova entrada a Group
|
||||
@@ -128,6 +129,47 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||
}
|
||||
}
|
||||
|
||||
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
|
||||
// existeix o l'engine retorna -1 (cap de canals d'efecte plé), cau a playSound
|
||||
// sec — l'usuari sent el so encara que la cua no s'apliqui.
|
||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
|
||||
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
|
||||
if (!sound_enabled_) { return; }
|
||||
auto* sound = AudioResource::getSound(name);
|
||||
if (sound == nullptr) { return; }
|
||||
|
||||
const auto* params = SoundEffectsConfig::get().findEcho(preset_name);
|
||||
if (params == nullptr) {
|
||||
std::fprintf(stderr, "Audio: preset d'eco '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine_->playSoundWithEcho(sound, *params, static_cast<int>(group)) < 0) {
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
|
||||
// fallback que playSoundWithEcho.
|
||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
|
||||
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
|
||||
if (!sound_enabled_) { return; }
|
||||
auto* sound = AudioResource::getSound(name);
|
||||
if (sound == nullptr) { return; }
|
||||
|
||||
const auto* params = SoundEffectsConfig::get().findReverb(preset_name);
|
||||
if (params == nullptr) {
|
||||
std::fprintf(stderr, "Audio: preset de reverb '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine_->playSoundWithReverb(sound, *params, static_cast<int>(group)) < 0) {
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Atura tots els sons
|
||||
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
|
||||
void Audio::stopAllSounds() {
|
||||
|
||||
@@ -80,7 +80,13 @@ class Audio {
|
||||
// --- Control de sons ---
|
||||
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
|
||||
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
|
||||
void stopAllSounds(); // Aturar tots els sons (muta globals de Ja)
|
||||
// Reprodueix un so processat per un efecte definit a data/sounds.yaml
|
||||
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix
|
||||
// o el motor està al cap de canals amb efecte, fa fallback a playSound
|
||||
// sec — l'usuari sent el so igualment, sense la cua.
|
||||
void playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
|
||||
void playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
|
||||
void stopAllSounds(); // Aturar tots els sons (muta globals de Ja)
|
||||
|
||||
// --- Control de volum (API interna: float 0.0..1.0) ---
|
||||
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
#include "core/audio/audio_effects.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include "core/audio/jail_audio.hpp"
|
||||
|
||||
namespace AudioEffects {
|
||||
|
||||
namespace {
|
||||
|
||||
// --- Caps de cua ---
|
||||
constexpr float ECHO_TAIL_MS = 800.0F;
|
||||
constexpr float REVERB_TAIL_MS = 1500.0F;
|
||||
|
||||
// --- Constants Freeverb ---
|
||||
// Delays de comb i allpass tunats per a 44.1 kHz; els reescalem per
|
||||
// freqüència real de la font.
|
||||
constexpr int COMB_REFERENCE_RATE = 44100;
|
||||
constexpr std::array<int, 8> COMB_DELAYS_L = {1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617};
|
||||
constexpr std::array<int, 4> ALLPASS_DELAYS_L = {556, 441, 341, 225};
|
||||
constexpr int STEREO_SPREAD = 23;
|
||||
|
||||
// Mapeig de Schroeder/Dattorro/Freeverb estàndard.
|
||||
constexpr float FIXED_GAIN = 0.015F;
|
||||
constexpr float SCALE_ROOM = 0.28F;
|
||||
constexpr float OFFSET_ROOM = 0.7F;
|
||||
constexpr float SCALE_DAMP = 0.4F;
|
||||
|
||||
// --- Decodificació a float -1..1 ---
|
||||
// Suporta U8/S16, mono/estèreo. Mono es duplica a L i R (la cadena
|
||||
// d'efectes treballa sempre amb dos canals per simplicitat).
|
||||
auto decodeToStereoFloat(const Ja::Sound& src, std::vector<float>& left, std::vector<float>& right) -> bool {
|
||||
const auto& spec = src.spec;
|
||||
const Uint8* buf = src.buffer.get();
|
||||
if (buf == nullptr || src.length == 0) { return false; }
|
||||
|
||||
int bytes_per_sample = 0;
|
||||
if (spec.format == SDL_AUDIO_S16) {
|
||||
bytes_per_sample = 2;
|
||||
} else if (spec.format == SDL_AUDIO_U8) {
|
||||
bytes_per_sample = 1;
|
||||
} else {
|
||||
std::cerr << "AudioEffects: format de so no suportat (només U8 o S16)\n";
|
||||
return false;
|
||||
}
|
||||
if (spec.channels < 1 || spec.channels > 2) {
|
||||
std::cerr << "AudioEffects: el so ha de ser mono o estèreo\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t TOTAL_FRAMES = src.length / static_cast<std::size_t>(bytes_per_sample * spec.channels);
|
||||
left.resize(TOTAL_FRAMES);
|
||||
right.resize(TOTAL_FRAMES);
|
||||
|
||||
for (std::size_t i = 0; i < TOTAL_FRAMES; ++i) {
|
||||
float sample_l = 0.0F;
|
||||
float sample_r = 0.0F;
|
||||
if (spec.format == SDL_AUDIO_S16) {
|
||||
const auto* p = reinterpret_cast<const std::int16_t*>(buf + (i * spec.channels * 2));
|
||||
sample_l = static_cast<float>(p[0]) / 32768.0F;
|
||||
sample_r = (spec.channels == 2) ? static_cast<float>(p[1]) / 32768.0F : sample_l;
|
||||
} else { // U8
|
||||
const Uint8* p = buf + (i * spec.channels);
|
||||
sample_l = (static_cast<float>(p[0]) - 128.0F) / 128.0F;
|
||||
sample_r = (spec.channels == 2) ? (static_cast<float>(p[1]) - 128.0F) / 128.0F : sample_l;
|
||||
}
|
||||
left[i] = sample_l;
|
||||
right[i] = sample_r;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Empaqueta dos canals float (-1..1) a S16 entrellaçat.
|
||||
void encodeStereoS16(const std::vector<float>& left, const std::vector<float>& right, std::vector<std::uint8_t>& out) {
|
||||
const std::size_t LEN = left.size();
|
||||
out.resize(LEN * 2 * sizeof(std::int16_t));
|
||||
auto* dst = reinterpret_cast<std::int16_t*>(out.data());
|
||||
for (std::size_t i = 0; i < LEN; ++i) {
|
||||
const float L = std::clamp(left[i], -1.0F, 1.0F);
|
||||
const float R = std::clamp(right[i], -1.0F, 1.0F);
|
||||
dst[(i * 2) + 0] = static_cast<std::int16_t>(std::lround(L * 32767.0F));
|
||||
dst[(i * 2) + 1] = static_cast<std::int16_t>(std::lround(R * 32767.0F));
|
||||
}
|
||||
}
|
||||
|
||||
// Reescala un delay de la taula de Freeverb per a la freqüència real.
|
||||
auto scaledDelay(int reference_delay, int rate) -> int {
|
||||
const long SCALED = std::lround(static_cast<double>(reference_delay) * static_cast<double>(rate) / static_cast<double>(COMB_REFERENCE_RATE));
|
||||
return std::max(1, static_cast<int>(SCALED));
|
||||
}
|
||||
|
||||
// --- Filtres bàsics ---
|
||||
struct Comb {
|
||||
std::vector<float> buf;
|
||||
std::size_t idx{0};
|
||||
float feedback{0.0F};
|
||||
float damp1{0.0F};
|
||||
float damp2{1.0F};
|
||||
float store{0.0F};
|
||||
|
||||
void init(int delay, float fb, float damping) {
|
||||
buf.assign(static_cast<std::size_t>(delay), 0.0F);
|
||||
idx = 0;
|
||||
feedback = fb;
|
||||
damp1 = damping;
|
||||
damp2 = 1.0F - damping;
|
||||
store = 0.0F;
|
||||
}
|
||||
auto tick(float in) -> float {
|
||||
const float OUT = buf[idx];
|
||||
store = (OUT * damp2) + (store * damp1);
|
||||
buf[idx] = in + (store * feedback);
|
||||
idx = (idx + 1) % buf.size();
|
||||
return OUT;
|
||||
}
|
||||
};
|
||||
|
||||
struct Allpass {
|
||||
std::vector<float> buf;
|
||||
std::size_t idx{0};
|
||||
|
||||
void init(int delay) {
|
||||
buf.assign(static_cast<std::size_t>(delay), 0.0F);
|
||||
idx = 0;
|
||||
}
|
||||
auto tick(float in) -> float {
|
||||
const float BUFOUT = buf[idx];
|
||||
const float OUT = -in + BUFOUT;
|
||||
buf[idx] = in + (BUFOUT * 0.5F);
|
||||
idx = (idx + 1) % buf.size();
|
||||
return OUT;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
auto applyEcho(const Ja::Sound& src, const Ja::EchoParams& params) -> std::optional<ProcessedSound> {
|
||||
std::vector<float> left;
|
||||
std::vector<float> right;
|
||||
if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; }
|
||||
|
||||
const int RATE = src.spec.freq;
|
||||
const int DELAY_SAMPLES = std::max(1, static_cast<int>(std::lround(params.delay_ms * 0.001F * static_cast<float>(RATE))));
|
||||
const auto TAIL_SAMPLES = static_cast<std::size_t>(std::lround(ECHO_TAIL_MS * 0.001F * static_cast<float>(RATE)));
|
||||
|
||||
const float FEEDBACK = std::clamp(params.feedback, 0.0F, 0.95F);
|
||||
const float WET = std::clamp(params.wet, 0.0F, 1.0F);
|
||||
const float DRY = 1.0F - WET;
|
||||
|
||||
const std::size_t INPUT_LEN = left.size();
|
||||
const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES;
|
||||
|
||||
std::vector<float> ring_l(static_cast<std::size_t>(DELAY_SAMPLES), 0.0F);
|
||||
std::vector<float> ring_r(static_cast<std::size_t>(DELAY_SAMPLES), 0.0F);
|
||||
std::size_t cursor = 0;
|
||||
|
||||
std::vector<float> out_l(TOTAL_LEN);
|
||||
std::vector<float> out_r(TOTAL_LEN);
|
||||
|
||||
for (std::size_t i = 0; i < TOTAL_LEN; ++i) {
|
||||
const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F;
|
||||
const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F;
|
||||
|
||||
const float DELAYED_L = ring_l[cursor];
|
||||
const float DELAYED_R = ring_r[cursor];
|
||||
|
||||
out_l[i] = (DRY * IN_L) + (WET * DELAYED_L);
|
||||
out_r[i] = (DRY * IN_R) + (WET * DELAYED_R);
|
||||
|
||||
ring_l[cursor] = IN_L + (DELAYED_L * FEEDBACK);
|
||||
ring_r[cursor] = IN_R + (DELAYED_R * FEEDBACK);
|
||||
cursor = (cursor + 1) % static_cast<std::size_t>(DELAY_SAMPLES);
|
||||
}
|
||||
|
||||
ProcessedSound result;
|
||||
result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE};
|
||||
encodeStereoS16(out_l, out_r, result.bytes);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto applyReverb(const Ja::Sound& src, const Ja::ReverbParams& params) -> std::optional<ProcessedSound> {
|
||||
std::vector<float> left;
|
||||
std::vector<float> right;
|
||||
if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; }
|
||||
|
||||
const int RATE = src.spec.freq;
|
||||
const auto TAIL_SAMPLES = static_cast<std::size_t>(std::lround(REVERB_TAIL_MS * 0.001F * static_cast<float>(RATE)));
|
||||
|
||||
const float ROOM_SIZE = std::clamp(params.room_size, 0.0F, 1.0F);
|
||||
const float DAMPING = std::clamp(params.damping, 0.0F, 1.0F);
|
||||
const float WET = std::clamp(params.wet, 0.0F, 1.0F);
|
||||
const float DRY = 1.0F - WET;
|
||||
|
||||
const float FEEDBACK = (ROOM_SIZE * SCALE_ROOM) + OFFSET_ROOM; // 0.7..0.98
|
||||
const float DAMP1 = DAMPING * SCALE_DAMP; // 0..0.4
|
||||
|
||||
// Inicialitza els 8 comb filters per cada canal i els 4 allpass.
|
||||
std::array<Comb, 8> comb_l;
|
||||
std::array<Comb, 8> comb_r;
|
||||
for (std::size_t i = 0; i < COMB_DELAYS_L.size(); ++i) {
|
||||
comb_l[i].init(scaledDelay(COMB_DELAYS_L[i], RATE), FEEDBACK, DAMP1);
|
||||
comb_r[i].init(scaledDelay(COMB_DELAYS_L[i] + STEREO_SPREAD, RATE), FEEDBACK, DAMP1);
|
||||
}
|
||||
std::array<Allpass, 4> allpass_l;
|
||||
std::array<Allpass, 4> allpass_r;
|
||||
for (std::size_t i = 0; i < ALLPASS_DELAYS_L.size(); ++i) {
|
||||
allpass_l[i].init(scaledDelay(ALLPASS_DELAYS_L[i], RATE));
|
||||
allpass_r[i].init(scaledDelay(ALLPASS_DELAYS_L[i] + STEREO_SPREAD, RATE));
|
||||
}
|
||||
|
||||
const std::size_t INPUT_LEN = left.size();
|
||||
const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES;
|
||||
std::vector<float> out_l(TOTAL_LEN);
|
||||
std::vector<float> out_r(TOTAL_LEN);
|
||||
|
||||
for (std::size_t i = 0; i < TOTAL_LEN; ++i) {
|
||||
const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F;
|
||||
const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F;
|
||||
const float MONO_INPUT = (IN_L + IN_R) * FIXED_GAIN;
|
||||
|
||||
// 8 comb filters en paral·lel, sumats.
|
||||
float wet_l = 0.0F;
|
||||
float wet_r = 0.0F;
|
||||
for (std::size_t k = 0; k < comb_l.size(); ++k) {
|
||||
wet_l += comb_l[k].tick(MONO_INPUT);
|
||||
wet_r += comb_r[k].tick(MONO_INPUT);
|
||||
}
|
||||
// 4 allpass en sèrie.
|
||||
for (std::size_t k = 0; k < allpass_l.size(); ++k) {
|
||||
wet_l = allpass_l[k].tick(wet_l);
|
||||
wet_r = allpass_r[k].tick(wet_r);
|
||||
}
|
||||
|
||||
out_l[i] = (DRY * IN_L) + (WET * wet_l);
|
||||
out_r[i] = (DRY * IN_R) + (WET * wet_r);
|
||||
}
|
||||
|
||||
ProcessedSound result;
|
||||
result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE};
|
||||
encodeStereoS16(out_l, out_r, result.bytes);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace AudioEffects
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
// Forward-declaració per no incloure jail_audio.hpp (cicle d'inclusió: aquest
|
||||
// header viu sota els params declarats a jail_audio.hpp, i alhora jail_audio
|
||||
// usa applyEcho/applyReverb).
|
||||
namespace Ja {
|
||||
struct Sound;
|
||||
struct EchoParams;
|
||||
struct ReverbParams;
|
||||
} // namespace Ja
|
||||
|
||||
// Processadors d'efectes per a sons puntuals. Reben un Ja::Sound (qualsevol
|
||||
// format suportat pel decodificador WAV: U8/S16, mono o estèreo) i tornen un
|
||||
// buffer PCM en S16 + el seu spec, llest per empenyer a un SDL_AudioStream.
|
||||
//
|
||||
// El buffer de sortida inclou la cua (decay) generada per l'efecte: per al
|
||||
// reverb, fins a 1500 ms; per a l'eco, fins a 800 ms. Aquests caps eviten
|
||||
// allargar indefinidament la reproducció quan els paràmetres reinjecten molt.
|
||||
//
|
||||
// Si el format del so d'origen no es pot processar, retornen std::nullopt
|
||||
// (el caller ha de fer fallback a reproducció seca).
|
||||
namespace AudioEffects {
|
||||
|
||||
struct ProcessedSound {
|
||||
std::vector<std::uint8_t> bytes; // PCM S16 entrellaçat (LRLRLR... si stereo)
|
||||
SDL_AudioSpec spec; // Format/canals/freqüència del buffer
|
||||
};
|
||||
|
||||
[[nodiscard]] auto applyEcho(const Ja::Sound& src, const Ja::EchoParams& params) -> std::optional<ProcessedSound>;
|
||||
[[nodiscard]] auto applyReverb(const Ja::Sound& src, const Ja::ReverbParams& params) -> std::optional<ProcessedSound>;
|
||||
|
||||
} // namespace AudioEffects
|
||||
@@ -4,6 +4,10 @@
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "core/audio/audio_effects.hpp"
|
||||
|
||||
// Només declaracions de stb_vorbis: STB_VORBIS_HEADER_ONLY omet el bloc
|
||||
// d'implementació. Les definicions les aporta source/external/stb_vorbis_impl.cpp
|
||||
@@ -397,13 +401,17 @@ namespace Ja {
|
||||
}
|
||||
|
||||
void Engine::stopChannel(const int channel) {
|
||||
forEachTargetChannel(channel, [](Channel& ch) {
|
||||
forEachTargetChannel(channel, [this](Channel& ch) {
|
||||
if (ch.state != ChannelState::FREE) {
|
||||
if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
|
||||
ch.stream = nullptr;
|
||||
ch.state = ChannelState::FREE;
|
||||
ch.pos = 0;
|
||||
ch.sound = nullptr;
|
||||
if (ch.has_effect) {
|
||||
ch.has_effect = false;
|
||||
if (effect_channels_active_ > 0) { --effect_channels_active_; }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -438,6 +446,55 @@ namespace Ja {
|
||||
}
|
||||
}
|
||||
|
||||
auto Engine::playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, const int group) -> int {
|
||||
// El cap de canals amb efecte es valida abans de reservar slot —
|
||||
// així evitem crear i destruir un stream només per descartar el play.
|
||||
if (effect_channels_active_ >= MAX_EFFECT_CHANNELS) { return -1; }
|
||||
|
||||
int channel = 0;
|
||||
while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { ++channel; }
|
||||
if (channel == MAX_SIMULTANEOUS_CHANNELS) { channel = 0; }
|
||||
|
||||
stopChannel(channel);
|
||||
|
||||
// El stream es crea contra l'spec del buffer processat (S16, ...)
|
||||
// perquè SDL faci el resampling cap a audio_spec_ del device.
|
||||
channels_[channel].stream = SDL_CreateAudioStream(&spec, &audio_spec_);
|
||||
if (channels_[channel].stream == nullptr) {
|
||||
std::fprintf(stderr, "Ja::Engine::playProcessedOnFreeChannel: Failed to create audio stream!\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
channels_[channel].sound = nullptr; // El buffer no és propietat de cap Ja::Sound.
|
||||
channels_[channel].times = 0;
|
||||
channels_[channel].pos = 0;
|
||||
const int CLAMPED_GROUP = (group >= 0 && group < MAX_GROUPS) ? group : 0;
|
||||
channels_[channel].group = CLAMPED_GROUP;
|
||||
channels_[channel].state = ChannelState::PLAYING;
|
||||
channels_[channel].has_effect = true;
|
||||
++effect_channels_active_;
|
||||
|
||||
SDL_PutAudioStreamData(channels_[channel].stream, bytes.data(), static_cast<int>(bytes.size()));
|
||||
SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[CLAMPED_GROUP]);
|
||||
SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream);
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
auto Engine::playSoundWithEcho(const Sound* sound, const EchoParams& params, const int group) -> int {
|
||||
if (sound == nullptr) { return -1; }
|
||||
auto processed = AudioEffects::applyEcho(*sound, params);
|
||||
if (!processed) { return -1; }
|
||||
return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
|
||||
}
|
||||
|
||||
auto Engine::playSoundWithReverb(const Sound* sound, const ReverbParams& params, const int group) -> int {
|
||||
if (sound == nullptr) { return -1; }
|
||||
auto processed = AudioEffects::applyReverb(*sound, params);
|
||||
if (!processed) { return -1; }
|
||||
return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
|
||||
}
|
||||
|
||||
// --- Factories i destructors (permanents) ---
|
||||
|
||||
auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
|
||||
|
||||
@@ -48,6 +48,26 @@ namespace Ja {
|
||||
// --- Constants ---
|
||||
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
|
||||
inline constexpr int MAX_GROUPS = 2;
|
||||
// Cap superior de canals que poden estar simultàniament reproduint un so
|
||||
// amb efecte (eco/reverb). Si està al límit, les noves crides amb efecte
|
||||
// cauen al camí sec — l'usuari sent el so igualment, sense la cua.
|
||||
inline constexpr int MAX_EFFECT_CHANNELS = 4;
|
||||
|
||||
// --- Paràmetres d'efectes ---
|
||||
// Els camps els fixa el caller (Audio) llegint sounds.yaml; el motor només
|
||||
// els passa a AudioEffects::applyEcho/applyReverb. Els defaults són
|
||||
// sensats però els presets els sobreescriuen.
|
||||
struct EchoParams {
|
||||
float delay_ms{220.0F}; // Temps fins al primer rebot.
|
||||
float feedback{0.45F}; // Reinjecció (0..0.95).
|
||||
float wet{0.35F}; // Mescla humida (0..1).
|
||||
};
|
||||
|
||||
struct ReverbParams {
|
||||
float room_size{0.7F}; // Mida percebuda (0..1).
|
||||
float damping{0.5F}; // Atenuació d'aguts per rebot (0..1).
|
||||
float wet{0.4F}; // Mescla humida (0..1).
|
||||
};
|
||||
|
||||
// Spec de fallback del dispositiu. S'aplica abans que l'Engine s'iniciï i
|
||||
// com a valor inicial de Sound/Music. L'spec real d'ús l'imposa el ctor
|
||||
@@ -72,6 +92,10 @@ namespace Ja {
|
||||
int times{0};
|
||||
int group{0};
|
||||
ChannelState state{ChannelState::FREE};
|
||||
// Marca si aquest canal va arrencar amb so processat per un efecte.
|
||||
// El motor compta canals actius amb efecte per fer complir
|
||||
// MAX_EFFECT_CHANNELS i alliberar el comptador en parar.
|
||||
bool has_effect{false};
|
||||
};
|
||||
|
||||
struct Music {
|
||||
@@ -147,6 +171,12 @@ namespace Ja {
|
||||
// --- So ---
|
||||
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
|
||||
auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
|
||||
// Reproducció amb so processat per un efecte. Retorna el canal
|
||||
// assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS).
|
||||
// El sound original només s'usa per consultar el spec/buffer; el
|
||||
// canal manipula el buffer ja processat (no reapunta a `sound`).
|
||||
auto playSoundWithEcho(const Sound* sound, const EchoParams& params, int group = 0) -> int;
|
||||
auto playSoundWithReverb(const Sound* sound, const ReverbParams& params, int group = 0) -> int;
|
||||
void pauseChannel(int channel);
|
||||
void resumeChannel(int channel);
|
||||
void stopChannel(int channel);
|
||||
@@ -161,6 +191,10 @@ namespace Ja {
|
||||
void updateIncomingFade();
|
||||
void updateCurrentMusic();
|
||||
void updateSoundChannels();
|
||||
// Empenta un buffer ja processat (S16) a un canal lliure i el deixa
|
||||
// sonar sense bucle. Camí comú dels dos overloads playSoundWith*.
|
||||
// Retorna el canal o -1 si no queden slots.
|
||||
auto playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, int group) -> int;
|
||||
|
||||
template <typename Fn>
|
||||
void forEachTargetChannel(int channel, Fn&& fn);
|
||||
@@ -174,6 +208,9 @@ namespace Ja {
|
||||
OutgoingMusic outgoing_music_;
|
||||
FadeState incoming_fade_;
|
||||
std::function<void()> on_music_ended_;
|
||||
// Comptador derivat de Channel::has_effect — evita haver-lo de
|
||||
// recalcular cada vegada que algú demana un play amb efecte.
|
||||
int effect_channels_active_{0};
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static amb sufix _
|
||||
static Engine* active_;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "core/audio/sound_effects_config.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Lector de camp amb fallback: deixa el destí intacte si la clau no
|
||||
// existeix (els defaults dels Ja::*Params s'inicialitzen al ctor del
|
||||
// struct, així que el comportament és "preset parcial = preset complet
|
||||
// amb defaults per als camps que falten").
|
||||
template <typename T>
|
||||
void readField(const fkyaml::node& node, const char* key, T& dst) {
|
||||
if (node.contains(key)) { dst = node[key].get_value<T>(); }
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto SoundEffectsConfig::get() -> SoundEffectsConfig& {
|
||||
static SoundEffectsConfig instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
void SoundEffectsConfig::load(const std::string& file_path) {
|
||||
auto bytes = Resource::Helper::loadFile(file_path);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "SoundEffectsConfig: no s'ha pogut obrir " << file_path << " — cap preset d'efecte disponible\n";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||
const auto* end = begin + bytes.size();
|
||||
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||
|
||||
if (yaml.contains("echo") && yaml["echo"].is_mapping()) {
|
||||
for (auto it = yaml["echo"].begin(); it != yaml["echo"].end(); ++it) {
|
||||
const auto NAME = it.key().get_value<std::string>();
|
||||
const auto& node = it.value();
|
||||
Ja::EchoParams params{};
|
||||
readField(node, "delay_ms", params.delay_ms);
|
||||
readField(node, "feedback", params.feedback);
|
||||
readField(node, "wet", params.wet);
|
||||
echoes_[NAME] = params;
|
||||
}
|
||||
}
|
||||
|
||||
if (yaml.contains("reverb") && yaml["reverb"].is_mapping()) {
|
||||
for (auto it = yaml["reverb"].begin(); it != yaml["reverb"].end(); ++it) {
|
||||
const auto NAME = it.key().get_value<std::string>();
|
||||
const auto& node = it.value();
|
||||
Ja::ReverbParams params{};
|
||||
readField(node, "room_size", params.room_size);
|
||||
readField(node, "damping", params.damping);
|
||||
readField(node, "wet", params.wet);
|
||||
reverbs_[NAME] = params;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "SoundEffectsConfig: " << echoes_.size() << " preset(s) d'eco i " << reverbs_.size() << " de reverb des de " << file_path << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "SoundEffectsConfig: error parsejant " << file_path << ": " << e.what() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
auto SoundEffectsConfig::findEcho(const std::string& name) const -> const Ja::EchoParams* {
|
||||
const auto IT = echoes_.find(name);
|
||||
return (IT == echoes_.end()) ? nullptr : &IT->second;
|
||||
}
|
||||
|
||||
auto SoundEffectsConfig::findReverb(const std::string& name) const -> const Ja::ReverbParams* {
|
||||
const auto IT = reverbs_.find(name);
|
||||
return (IT == reverbs_.end()) ? nullptr : &IT->second;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "core/audio/jail_audio.hpp" // Per a Ja::EchoParams / Ja::ReverbParams
|
||||
|
||||
// Catàleg de presets d'efectes carregat des de data/sounds.yaml. La capa
|
||||
// Audio (playSoundWithEcho/playSoundWithReverb) hi accedeix per nom: si el
|
||||
// preset no existeix, el so es reprodueix sec amb un avís a stderr.
|
||||
//
|
||||
// Patró Meyers idèntic a UiConfig/Locale: un sol load() a l'arrencada, sense
|
||||
// hot-reload. Si el fitxer no existeix, el catàleg queda buit (cap preset
|
||||
// disponible) i tots els playSoundWith* es comporten com playSound dry.
|
||||
class SoundEffectsConfig {
|
||||
public:
|
||||
static auto get() -> SoundEffectsConfig&;
|
||||
|
||||
SoundEffectsConfig(const SoundEffectsConfig&) = delete;
|
||||
SoundEffectsConfig(SoundEffectsConfig&&) = delete;
|
||||
auto operator=(const SoundEffectsConfig&) -> SoundEffectsConfig& = delete;
|
||||
auto operator=(SoundEffectsConfig&&) -> SoundEffectsConfig& = delete;
|
||||
|
||||
void load(const std::string& file_path);
|
||||
|
||||
// Retorna nullptr si el preset no existeix.
|
||||
[[nodiscard]] auto findEcho(const std::string& name) const -> const Ja::EchoParams*;
|
||||
[[nodiscard]] auto findReverb(const std::string& name) const -> const Ja::ReverbParams*;
|
||||
|
||||
private:
|
||||
SoundEffectsConfig() = default;
|
||||
~SoundEffectsConfig() = default;
|
||||
|
||||
std::unordered_map<std::string, Ja::EchoParams> echoes_;
|
||||
std::unordered_map<std::string, Ja::ReverbParams> reverbs_;
|
||||
};
|
||||
@@ -0,0 +1,238 @@
|
||||
#include "core/input/define_buttons.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/rendering/text.hpp"
|
||||
#include "core/ui/ui_config.hpp"
|
||||
#include "utils/defines.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Threshold per als triggers L2/R2 — equivalent a CC (16384 ≈ 50%).
|
||||
constexpr Sint16 TRIGGER_THRESHOLD = 16384;
|
||||
|
||||
// Geometria de l'overlay (canvas lògic 320×240).
|
||||
constexpr int BOX_WIDTH = 240;
|
||||
constexpr int PAD_Y = 8;
|
||||
constexpr int LINE_GAP = 4;
|
||||
constexpr Uint8 BG_ALPHA = 230;
|
||||
|
||||
} // namespace
|
||||
|
||||
void DefineButtons::init(std::shared_ptr<Text> title_font,
|
||||
std::shared_ptr<Text> items_font,
|
||||
const Palette& ui_palette) {
|
||||
title_font_ = std::move(title_font);
|
||||
items_font_ = std::move(items_font);
|
||||
ui_palette_ = ui_palette;
|
||||
}
|
||||
|
||||
auto DefineButtons::enable(SDL_Gamepad* pad, std::string name, std::string path) -> bool {
|
||||
if (pad == nullptr) { return false; }
|
||||
pad_ = pad;
|
||||
pad_name_ = std::move(name);
|
||||
pad_path_ = std::move(path);
|
||||
enabled_ = true;
|
||||
finished_ = false;
|
||||
closing_ = false;
|
||||
message_shown_ = false;
|
||||
message_timer_ms_ = 0;
|
||||
l2_was_pressed_ = false;
|
||||
r2_was_pressed_ = false;
|
||||
resetSequence();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DefineButtons::disable() {
|
||||
enabled_ = false;
|
||||
finished_ = false;
|
||||
closing_ = false;
|
||||
message_shown_ = false;
|
||||
message_timer_ms_ = 0;
|
||||
l2_was_pressed_ = false;
|
||||
r2_was_pressed_ = false;
|
||||
pad_ = nullptr;
|
||||
pad_name_.clear();
|
||||
pad_path_.clear();
|
||||
sequence_.clear();
|
||||
index_ = 0;
|
||||
}
|
||||
|
||||
void DefineButtons::resetSequence() {
|
||||
// Ordre: ATTACK, JUMP, SERVICE — replica l'estructura de
|
||||
// `clearButtons()` de coffee_crisis_arcade_edition (seqüència ordenada
|
||||
// d'accions amb la SERVICE al final) adaptada al joc actual. La label
|
||||
// de SERVICE va a una clau pròpia (`menu.items.service`) per no
|
||||
// reutilitzar la del submenú dels mandos.
|
||||
sequence_.clear();
|
||||
sequence_.push_back({"menu.items.attack", GamepadBindings::Action::ATTACK, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||||
sequence_.push_back({"menu.items.jump", GamepadBindings::Action::JUMP, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||||
sequence_.push_back({"menu.items.service", GamepadBindings::Action::SERVICE, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||||
index_ = 0;
|
||||
}
|
||||
|
||||
auto DefineButtons::isInUse(int button) const -> bool {
|
||||
return std::ranges::any_of(sequence_, [button](const ButtonAssignment& a) {
|
||||
return a.button == button;
|
||||
});
|
||||
}
|
||||
|
||||
auto DefineButtons::isReadyToClose() const -> bool {
|
||||
// Llest per tancar quan: la seqüència ha acabat, ja s'ha mostrat el
|
||||
// missatge, està en mode tancament, i el timer ha expirat. Replica el
|
||||
// patró de `coffee_crisis_arcade_edition::DefineButtons::isReadyToClose`,
|
||||
// simplificat perquè ací no tenim animació de WindowMessage.
|
||||
return finished_ && message_shown_ && closing_ && message_timer_ms_ <= 0;
|
||||
}
|
||||
|
||||
void DefineButtons::update(int delta_ms) {
|
||||
if (!enabled_) { return; }
|
||||
if (finished_ && message_shown_) {
|
||||
message_timer_ms_ -= delta_ms;
|
||||
if (message_timer_ms_ <= 0 && !closing_) {
|
||||
closing_ = true;
|
||||
message_timer_ms_ = 0; // anchor a 0 per a `isReadyToClose`.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DefineButtons::handleEvent(const SDL_Event& ev) {
|
||||
if (!enabled_) { return; }
|
||||
switch (ev.type) {
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
||||
handleButtonDown(ev.gbutton);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_UP:
|
||||
onButtonUp();
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
|
||||
handleAxisMotion(ev.gaxis);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DefineButtons::assignAndAdvance(int button) {
|
||||
if (finished_ || isInUse(button)) { return; }
|
||||
sequence_.at(index_).button = button;
|
||||
if (index_ + 1 < sequence_.size()) {
|
||||
++index_;
|
||||
} else {
|
||||
finished_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void DefineButtons::processTrigger(int virtual_button, bool& was_pressed, bool now) {
|
||||
if (now && !was_pressed) { assignAndAdvance(virtual_button); }
|
||||
if (!now && was_pressed) { onButtonUp(); }
|
||||
was_pressed = now;
|
||||
}
|
||||
|
||||
void DefineButtons::handleButtonDown(const SDL_GamepadButtonEvent& ev) {
|
||||
// Filtra: només acceptem events del mando que estem configurant.
|
||||
// (Per l'instance_id no podem comparar directament; obtenim el handle
|
||||
// i el comparem.)
|
||||
const SDL_Gamepad* source = SDL_GetGamepadFromID(ev.which);
|
||||
if (source != pad_) { return; }
|
||||
assignAndAdvance(static_cast<int>(ev.button));
|
||||
}
|
||||
|
||||
void DefineButtons::handleAxisMotion(const SDL_GamepadAxisEvent& ev) {
|
||||
const SDL_Gamepad* source = SDL_GetGamepadFromID(ev.which);
|
||||
if (source != pad_) { return; }
|
||||
|
||||
// Triggers L2/R2 com a "botons" amb edge-detect (transició inactiu →
|
||||
// actiu). Replica `doControllerAxisMotion` de
|
||||
// coffee_crisis_arcade_edition.
|
||||
const bool NOW = ev.value > TRIGGER_THRESHOLD;
|
||||
if (ev.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) {
|
||||
processTrigger(GamepadBindings::TRIGGER_L2_AS_BUTTON, l2_was_pressed_, NOW);
|
||||
} else if (ev.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
||||
processTrigger(GamepadBindings::TRIGGER_R2_AS_BUTTON, r2_was_pressed_, NOW);
|
||||
}
|
||||
}
|
||||
|
||||
void DefineButtons::onButtonUp() {
|
||||
if (!finished_ || message_shown_) { return; }
|
||||
persistAndShowComplete();
|
||||
}
|
||||
|
||||
void DefineButtons::persistAndShowComplete() {
|
||||
std::unordered_map<GamepadBindings::Action, int> mapping;
|
||||
for (const auto& a : sequence_) {
|
||||
mapping[a.action] = a.button;
|
||||
}
|
||||
GamepadBindings::get().setBindings(pad_name_, pad_path_, mapping);
|
||||
message_shown_ = true;
|
||||
message_timer_ms_ = MESSAGE_DISPLAY_DURATION_MS;
|
||||
}
|
||||
|
||||
void DefineButtons::render() const {
|
||||
if (!enabled_ || !title_font_ || !items_font_) { return; }
|
||||
|
||||
const int TITLE_H = title_font_->getLineHeight();
|
||||
const int ITEM_H = items_font_->getLineHeight();
|
||||
const int TITLE_KERNING = title_font_->getDefaultKerning();
|
||||
const int ITEM_KERNING = items_font_->getDefaultKerning();
|
||||
|
||||
// Calcular l'alçada total: títol + 2 línies (instrucció + label).
|
||||
int box_h = (PAD_Y * 2) + TITLE_H + LINE_GAP + (ITEM_H * 2) + LINE_GAP;
|
||||
int box_x = (GameCanvas::WIDTH - BOX_WIDTH) / 2;
|
||||
int box_y = (GameCanvas::HEIGHT - box_h) / 2;
|
||||
|
||||
const auto BG_ARGB = ui_palette_[UiConfig::get().menuBg()];
|
||||
const auto BORDER_ARGB = ui_palette_[UiConfig::get().menuBorder()];
|
||||
Screen::get().blendRect(box_x, box_y, BOX_WIDTH, box_h, BG_ARGB, BG_ALPHA);
|
||||
Screen::get().fillRect(box_x, box_y, BOX_WIDTH, 1, BORDER_ARGB);
|
||||
Screen::get().fillRect(box_x, box_y + box_h - 1, BOX_WIDTH, 1, BORDER_ARGB);
|
||||
Screen::get().fillRect(box_x, box_y, 1, box_h, BORDER_ARGB);
|
||||
Screen::get().fillRect(box_x + BOX_WIDTH - 1, box_y, 1, box_h, BORDER_ARGB);
|
||||
|
||||
int cursor_y = box_y + PAD_Y;
|
||||
const int CENTER_X = box_x + (BOX_WIDTH / 2);
|
||||
|
||||
// Títol: nom del mando.
|
||||
const std::string TITLE = pad_name_;
|
||||
title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
CENTER_X,
|
||||
cursor_y,
|
||||
TITLE,
|
||||
TITLE_KERNING,
|
||||
UiConfig::get().menuTitle());
|
||||
cursor_y += TITLE_H + LINE_GAP;
|
||||
|
||||
if (message_shown_) {
|
||||
// Pantalla final: missatge de configuració completa.
|
||||
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
CENTER_X,
|
||||
cursor_y,
|
||||
Locale::get().text("menu.values.define_buttons_complete"),
|
||||
ITEM_KERNING,
|
||||
UiConfig::get().menuValue());
|
||||
return;
|
||||
}
|
||||
|
||||
// Instrucció + acció pendent. Replica visualment el WindowMessage de
|
||||
// coffee_crisis_arcade_edition.
|
||||
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
CENTER_X,
|
||||
cursor_y,
|
||||
Locale::get().text("menu.values.press_button"),
|
||||
ITEM_KERNING,
|
||||
UiConfig::get().menuLabel());
|
||||
cursor_y += ITEM_H + LINE_GAP;
|
||||
|
||||
if (index_ < sequence_.size()) {
|
||||
const auto& current = sequence_[index_];
|
||||
items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
CENTER_X,
|
||||
cursor_y,
|
||||
Locale::get().text(current.label_key),
|
||||
ITEM_KERNING,
|
||||
UiConfig::get().menuCursor());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/input/gamepad_bindings.hpp"
|
||||
#include "core/rendering/surface.hpp" // Palette
|
||||
|
||||
class Text;
|
||||
|
||||
// Sub-mòdul de captura seqüencial de botons per a un mando concret.
|
||||
// Replica `DefineButtons` de coffee_crisis_arcade_edition: l'usuari obre el
|
||||
// menú de servei, escull "Definir botons P1" / "P2", i el sub-mòdul li
|
||||
// demana un per un els botons per a cada acció (JUMP → ATTACK → SERVICE).
|
||||
//
|
||||
// Cicle de vida:
|
||||
// 1. `enable(pad, name, path)`: arrenca la seqüència. `index_` = 0.
|
||||
// 2. Cada SDL_EVENT_GAMEPAD_BUTTON_DOWN (o transició de L2/R2 via
|
||||
// AXIS_MOTION) que NO duplique cap botó ja assignat dins la sessió
|
||||
// avança `index_`. Si `index_` arriba a la mida de la seqüència,
|
||||
// `finished_` es posa a true i, al pròxim BUTTON_UP, escrivim el mapeig
|
||||
// complet a `GamepadBindings`, mostrem "Configuració completa" durant
|
||||
// `MESSAGE_DISPLAY_DURATION_MS`, i posem `closing_`. Quan tot l'overlay
|
||||
// ja no és visible, `isReadyToClose()` torna `true` i el caller ens
|
||||
// fa `disable()`.
|
||||
//
|
||||
// El layer runtime de l'aplicació viu a `GamepadBindings`: aquesta classe
|
||||
// només decideix què s'escriu i quan.
|
||||
class DefineButtons {
|
||||
public:
|
||||
DefineButtons() = default;
|
||||
~DefineButtons() = default;
|
||||
DefineButtons(const DefineButtons&) = delete;
|
||||
DefineButtons(DefineButtons&&) = delete;
|
||||
auto operator=(const DefineButtons&) -> DefineButtons& = delete;
|
||||
auto operator=(DefineButtons&&) -> DefineButtons& = delete;
|
||||
|
||||
// Injecta les fonts i la paleta UI; cridat des de `ServiceMenu::init`
|
||||
// perquè comparteixin tipografia i colors de tema.
|
||||
void init(std::shared_ptr<Text> title_font,
|
||||
std::shared_ptr<Text> items_font,
|
||||
const Palette& ui_palette);
|
||||
|
||||
// Arranca la sessió per al mando indicat. `pad` ha de ser un handle
|
||||
// viu obtingut de `GamepadManager`. Retorna false si `pad` és nullptr.
|
||||
auto enable(SDL_Gamepad* pad, std::string name, std::string path) -> bool;
|
||||
|
||||
// Atura la sessió (sense persistir) i neteja l'estat.
|
||||
void disable();
|
||||
|
||||
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
|
||||
[[nodiscard]] auto isReadyToClose() const -> bool;
|
||||
|
||||
// Avança el timer de tancament (ms) — replica el `update` de
|
||||
// coffee_crisis_arcade_edition. No fa res si no està actiu.
|
||||
void update(int delta_ms);
|
||||
|
||||
// Pinta l'overlay sobre el canvas actual (centrat). No fa res si no
|
||||
// està actiu.
|
||||
void render() const;
|
||||
|
||||
// Punt d'entrada d'events: BUTTON_DOWN / BUTTON_UP / AXIS_MOTION del
|
||||
// mando objectiu. La resta s'ignoren.
|
||||
void handleEvent(const SDL_Event& ev);
|
||||
|
||||
private:
|
||||
static constexpr int MESSAGE_DISPLAY_DURATION_MS = 2000;
|
||||
|
||||
struct ButtonAssignment {
|
||||
std::string label_key;
|
||||
GamepadBindings::Action action;
|
||||
int button; // SDL_GamepadButton o trigger virtual; INVALID = sense assignar.
|
||||
};
|
||||
|
||||
// Reinicialitza la seqüència ordenada (JUMP → ATTACK → SERVICE) amb
|
||||
// tots els botons sense assignar. Cridat a `enable`.
|
||||
void resetSequence();
|
||||
|
||||
// Comprova si un botó ja ha estat assignat dins de la sessió actual.
|
||||
// Replica `checkButtonNotInUse` / `checkTriggerNotInUse`.
|
||||
[[nodiscard]] auto isInUse(int button) const -> bool;
|
||||
|
||||
void handleButtonDown(const SDL_GamepadButtonEvent& ev);
|
||||
void handleAxisMotion(const SDL_GamepadAxisEvent& ev);
|
||||
|
||||
// Helper de la lògica comuna a button-down i als triggers L2/R2:
|
||||
// assigna `button` a l'acció pendent i avança l'índex (o marca
|
||||
// finished_ si era l'última). No fa res si `button` ja està en ús.
|
||||
void assignAndAdvance(int button);
|
||||
|
||||
// Tracta una transició de trigger (L2 o R2) com a botó virtual amb
|
||||
// edge-detect. `virtual_button` és `TRIGGER_L?_AS_BUTTON`,
|
||||
// `was_pressed` és l'estat anterior (mutat per referència).
|
||||
void processTrigger(int virtual_button, bool& was_pressed, bool now);
|
||||
|
||||
// Crida `onSequenceEnd()` quan l'usuari deixa anar el botó després
|
||||
// d'haver completat la seqüència — replica la branca de
|
||||
// `SDL_EVENT_GAMEPAD_BUTTON_UP` a `DefineButtons::handleEvents`.
|
||||
void onButtonUp();
|
||||
|
||||
// Persistix les assignacions a `GamepadBindings` i posa `message_shown_`.
|
||||
void persistAndShowComplete();
|
||||
|
||||
SDL_Gamepad* pad_{nullptr};
|
||||
std::string pad_name_;
|
||||
std::string pad_path_;
|
||||
|
||||
std::shared_ptr<Text> title_font_;
|
||||
std::shared_ptr<Text> items_font_;
|
||||
Palette ui_palette_{};
|
||||
|
||||
std::vector<ButtonAssignment> sequence_;
|
||||
std::size_t index_{0};
|
||||
int message_timer_ms_{0};
|
||||
|
||||
bool enabled_{false};
|
||||
bool finished_{false};
|
||||
bool closing_{false};
|
||||
bool message_shown_{false};
|
||||
bool l2_was_pressed_{false};
|
||||
bool r2_was_pressed_{false};
|
||||
};
|
||||
@@ -0,0 +1,220 @@
|
||||
#include "core/input/gamepad_bindings.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Threshold per a considerar L2/R2 com a premuts (mateix valor que
|
||||
// coffee_crisis_arcade_edition: 16384 ≈ 50% de ±32767).
|
||||
constexpr Sint16 TRIGGER_THRESHOLD = 16384;
|
||||
|
||||
// Taules de mapejat string ↔ valor: replica BUTTON_TO_STRING /
|
||||
// STRING_TO_BUTTON / ACTION_TO_STRING / STRING_TO_ACTION del projecte
|
||||
// de referència. Els noms en lowercase per llegibilitat al YAML.
|
||||
using ActionPair = std::pair<GamepadBindings::Action, std::string_view>;
|
||||
constexpr std::array ACTION_NAMES = {
|
||||
ActionPair{GamepadBindings::Action::JUMP, std::string_view{"jump"}},
|
||||
ActionPair{GamepadBindings::Action::ATTACK, std::string_view{"attack"}},
|
||||
ActionPair{GamepadBindings::Action::SERVICE, std::string_view{"service"}},
|
||||
};
|
||||
|
||||
using ButtonPair = std::pair<int, std::string_view>;
|
||||
constexpr std::array BUTTON_NAMES = {
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH), std::string_view{"south"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_EAST), std::string_view{"east"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_WEST), std::string_view{"west"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_NORTH), std::string_view{"north"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_BACK), std::string_view{"back"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_GUIDE), std::string_view{"guide"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_START), std::string_view{"start"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_LEFT_STICK), std::string_view{"left_stick"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_RIGHT_STICK), std::string_view{"right_stick"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), std::string_view{"l1"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), std::string_view{"r1"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_UP), std::string_view{"dpad_up"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_DOWN), std::string_view{"dpad_down"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT), std::string_view{"dpad_left"}},
|
||||
ButtonPair{static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT), std::string_view{"dpad_right"}},
|
||||
ButtonPair{GamepadBindings::TRIGGER_L2_AS_BUTTON, std::string_view{"l2"}},
|
||||
ButtonPair{GamepadBindings::TRIGGER_R2_AS_BUTTON, std::string_view{"r2"}},
|
||||
};
|
||||
|
||||
auto actionToString(GamepadBindings::Action a) -> std::string_view {
|
||||
for (const auto& [act, name] : ACTION_NAMES) {
|
||||
if (act == a) { return name; }
|
||||
}
|
||||
return ACTION_NAMES.front().second;
|
||||
}
|
||||
|
||||
auto stringToAction(std::string_view s, GamepadBindings::Action& out) -> bool {
|
||||
for (const auto& [act, name] : ACTION_NAMES) {
|
||||
if (name == s) {
|
||||
out = act;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto buttonToString(int btn) -> std::string_view {
|
||||
for (const auto& [val, name] : BUTTON_NAMES) {
|
||||
if (val == btn) { return name; }
|
||||
}
|
||||
return std::string_view{""};
|
||||
}
|
||||
|
||||
auto stringToButton(std::string_view s, int& out) -> bool {
|
||||
for (const auto& [val, name] : BUTTON_NAMES) {
|
||||
if (name == s) {
|
||||
out = val;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto GamepadBindings::get() -> GamepadBindings& {
|
||||
static GamepadBindings instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
auto GamepadBindings::defaultButton(Action action) -> int {
|
||||
// Defaults històrics de l'aee_2026 (vegeu Player::accumulateGamepadMovement
|
||||
// i les consultes del Director per al menú).
|
||||
switch (action) {
|
||||
case Action::JUMP:
|
||||
return static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH);
|
||||
case Action::ATTACK:
|
||||
return static_cast<int>(SDL_GAMEPAD_BUTTON_WEST);
|
||||
case Action::SERVICE:
|
||||
return static_cast<int>(SDL_GAMEPAD_BUTTON_BACK);
|
||||
}
|
||||
return static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID);
|
||||
}
|
||||
|
||||
auto GamepadBindings::findConfig(const std::string& name, const std::string& path) const -> int {
|
||||
if (!path.empty()) {
|
||||
for (std::size_t i = 0; i < configs_.size(); ++i) {
|
||||
if (configs_[i].path == path) { return static_cast<int>(i); }
|
||||
}
|
||||
}
|
||||
if (!name.empty()) {
|
||||
for (std::size_t i = 0; i < configs_.size(); ++i) {
|
||||
if (configs_[i].name == name) { return static_cast<int>(i); }
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto GamepadBindings::getButton(const std::string& name, const std::string& path, Action action) const -> int {
|
||||
const int IDX = findConfig(name, path);
|
||||
if (IDX < 0) { return defaultButton(action); }
|
||||
const auto& m = configs_[static_cast<std::size_t>(IDX)].bindings;
|
||||
auto it = m.find(action);
|
||||
return (it != m.end()) ? it->second : defaultButton(action);
|
||||
}
|
||||
|
||||
void GamepadBindings::setBindings(const std::string& name,
|
||||
const std::string& path,
|
||||
const std::unordered_map<Action, int>& bindings) {
|
||||
int idx = findConfig(name, path);
|
||||
if (idx < 0) {
|
||||
configs_.push_back(Config{.name = name, .path = path, .bindings = bindings});
|
||||
} else {
|
||||
auto& cfg = configs_[static_cast<std::size_t>(idx)];
|
||||
cfg.name = name;
|
||||
cfg.path = path;
|
||||
cfg.bindings = bindings;
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
auto GamepadBindings::isPressed(SDL_Gamepad* pad, int button) -> bool {
|
||||
if (pad == nullptr) { return false; }
|
||||
if (button == TRIGGER_L2_AS_BUTTON) {
|
||||
return SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_THRESHOLD;
|
||||
}
|
||||
if (button == TRIGGER_R2_AS_BUTTON) {
|
||||
return SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > TRIGGER_THRESHOLD;
|
||||
}
|
||||
return SDL_GetGamepadButton(pad, static_cast<SDL_GamepadButton>(button));
|
||||
}
|
||||
|
||||
void GamepadBindings::load(const std::string& file_path) {
|
||||
file_path_ = file_path;
|
||||
configs_.clear();
|
||||
|
||||
std::ifstream file(file_path);
|
||||
if (!file) { return; }
|
||||
std::stringstream buf;
|
||||
buf << file.rdbuf();
|
||||
const std::string CONTENT = buf.str();
|
||||
if (CONTENT.empty()) { return; }
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(CONTENT);
|
||||
if (!yaml.contains("gamepads") || !yaml["gamepads"].is_sequence()) {
|
||||
std::cerr << "GamepadBindings: secció 'gamepads' absent o invàlida a "
|
||||
<< file_path << '\n';
|
||||
return;
|
||||
}
|
||||
for (const auto& entry : yaml["gamepads"]) {
|
||||
Config cfg;
|
||||
if (entry.contains("name")) { cfg.name = entry["name"].get_value<std::string>(); }
|
||||
if (entry.contains("path")) { cfg.path = entry["path"].get_value<std::string>(); }
|
||||
if (entry.contains("bindings") && entry["bindings"].is_mapping()) {
|
||||
for (const auto& kv : entry["bindings"].as_map()) {
|
||||
const auto KEY = kv.first.get_value<std::string>();
|
||||
const auto VAL = kv.second.get_value<std::string>();
|
||||
Action action{};
|
||||
int btn = 0;
|
||||
if (stringToAction(KEY, action) && stringToButton(VAL, btn)) {
|
||||
cfg.bindings[action] = btn;
|
||||
}
|
||||
}
|
||||
}
|
||||
configs_.push_back(std::move(cfg));
|
||||
}
|
||||
std::cout << "GamepadBindings: " << configs_.size() << " mandos carregats de "
|
||||
<< file_path << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "GamepadBindings: error parsejant " << file_path << ": "
|
||||
<< e.what() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
auto GamepadBindings::save() -> bool {
|
||||
if (file_path_.empty()) { return false; }
|
||||
|
||||
std::ostringstream out;
|
||||
out << "# AEE 2026 - Vinculacions de mando per dispositiu.\n";
|
||||
out << "# Auto-generat des del menú de servei. Editable a mà.\n";
|
||||
out << "gamepads:\n";
|
||||
for (const auto& cfg : configs_) {
|
||||
out << " - name: \"" << cfg.name << "\"\n";
|
||||
out << " path: \"" << cfg.path << "\"\n";
|
||||
out << " bindings:\n";
|
||||
for (const auto& [action, button] : cfg.bindings) {
|
||||
out << " " << actionToString(action) << ": \""
|
||||
<< buttonToString(button) << "\"\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream file(file_path_, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "GamepadBindings: no es pot escriure " << file_path_ << '\n';
|
||||
return false;
|
||||
}
|
||||
file << out.str();
|
||||
return file.good();
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// Vinculacions remapeables d'accions del joc a botons d'un mando concret,
|
||||
// indexades per `(name, path)` del mando — replica el patró
|
||||
// `GamepadConfig` + `GamepadConfigManager` de coffee_crisis_arcade_edition.
|
||||
//
|
||||
// Persisteix en un YAML aïllat al system folder (no a `config.yaml`) perquè
|
||||
// és per dispositiu, no per usuari/perfil: dos mandos diferents poden
|
||||
// tenir mappings diferents i cap d'ells va lligat a un slot de jugador.
|
||||
//
|
||||
// El layer runtime (consultat per Player/Director cada frame) és
|
||||
// directament aquesta classe; no hi ha caché a `GamepadManager::Entry`. El
|
||||
// nombre d'entries persistides és típicament <10, i les accions són 3
|
||||
// (JUMP/ATTACK/SERVICE), així que les consultes són trivialment ràpides.
|
||||
class GamepadBindings {
|
||||
public:
|
||||
enum class Action : std::uint8_t {
|
||||
JUMP,
|
||||
ATTACK,
|
||||
SERVICE,
|
||||
};
|
||||
|
||||
// Triggers L2/R2 representats com a "botons virtuals" — replica la
|
||||
// convenció de coffee_crisis_arcade_edition (`TRIGGER_L2_AS_BUTTON`,
|
||||
// `TRIGGER_R2_AS_BUTTON`). El threshold viu a `isPressed`.
|
||||
static constexpr int TRIGGER_L2_AS_BUTTON = 100;
|
||||
static constexpr int TRIGGER_R2_AS_BUTTON = 101;
|
||||
|
||||
static auto get() -> GamepadBindings&;
|
||||
|
||||
GamepadBindings(const GamepadBindings&) = delete;
|
||||
GamepadBindings(GamepadBindings&&) = delete;
|
||||
auto operator=(const GamepadBindings&) -> GamepadBindings& = delete;
|
||||
auto operator=(GamepadBindings&&) -> GamepadBindings& = delete;
|
||||
|
||||
// Fixa la ruta del fitxer persistent + el carrega. Si no existix
|
||||
// encara, l'estat queda buit (les consultes retornaran defaults) i
|
||||
// `save()` el crearà al primer `setBindings`.
|
||||
void load(const std::string& file_path);
|
||||
|
||||
// Persistix l'estat actual (totes les Config conegudes). Cridat
|
||||
// automàticament dins de `setBindings` — no cal cridar-la a mà.
|
||||
auto save() -> bool;
|
||||
|
||||
// Retorna el botó (o trigger virtual 100/101) lligat a `action` per al
|
||||
// mando identificat per (name, path). Si no hi ha entrada per aquest
|
||||
// mando, retorna el default global. La cerca és primer per `path`
|
||||
// (re-vincular el mateix mando al mateix port és prioritari) i, si no
|
||||
// troba, fallback per `name` (mando connectat a un altre port USB).
|
||||
[[nodiscard]] auto getButton(const std::string& name, const std::string& path, Action action) const -> int;
|
||||
|
||||
// Reescriu totes les vinculacions del mando (name, path). Si no hi
|
||||
// havia config per aquest dispositiu, en crea una nova. Persisteix a
|
||||
// disc immediatament. Replicació de `Input::saveGamepadConfigFromGamepad`.
|
||||
void setBindings(const std::string& name,
|
||||
const std::string& path,
|
||||
const std::unordered_map<Action, int>& bindings);
|
||||
|
||||
// Wrapper de polling: torna `true` si el "botó" (o trigger virtual)
|
||||
// està premut ara mateix al mando `pad`. Encapsula el threshold per
|
||||
// a L2/R2 (~50% del rang).
|
||||
[[nodiscard]] static auto isPressed(SDL_Gamepad* pad, int button) -> bool;
|
||||
|
||||
// Default de fàbrica per a una acció — usat tant pel fallback de
|
||||
// `getButton` com per inicialitzar la seqüència del DefineButtons.
|
||||
[[nodiscard]] static auto defaultButton(Action action) -> int;
|
||||
|
||||
private:
|
||||
struct Config {
|
||||
std::string name;
|
||||
std::string path;
|
||||
std::unordered_map<Action, int> bindings;
|
||||
};
|
||||
|
||||
GamepadBindings() = default;
|
||||
~GamepadBindings() = default;
|
||||
|
||||
// Localitza la Config que coincideix amb (name, path); retorna -1 si
|
||||
// no la troba. Match prioritari per path, fallback per nom.
|
||||
[[nodiscard]] auto findConfig(const std::string& name, const std::string& path) const -> int;
|
||||
|
||||
std::vector<Config> configs_;
|
||||
std::string file_path_;
|
||||
};
|
||||
@@ -7,36 +7,19 @@
|
||||
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
// El parser pure de YAML d'animacions (parseAnimationsFromYaml + helpers)
|
||||
// viu a `animated_sprite_parser.cpp` perquè és testable sense Surface ni
|
||||
// Resource::Cache. Aquesta unitat es queda amb el runtime de la classe i
|
||||
// amb el que sí toca el cache de surfaces (parseSpriteBlock).
|
||||
|
||||
namespace {
|
||||
|
||||
auto convertYamlFramesToRects(const fkyaml::node& frames_node, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector<SDL_FRect> {
|
||||
std::vector<SDL_FRect> frames;
|
||||
SDL_FRect rect = {.x = 0.0F, .y = 0.0F, .w = frame_width, .h = frame_height};
|
||||
for (const auto& frame_index_node : frames_node) {
|
||||
const int NUM_TILE = frame_index_node.get_value<int>();
|
||||
if (NUM_TILE <= max_tiles) {
|
||||
rect.x = static_cast<float>(NUM_TILE % frames_per_row) * frame_width;
|
||||
rect.y = static_cast<float>(NUM_TILE / frames_per_row) * frame_height;
|
||||
frames.emplace_back(rect);
|
||||
}
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
struct SheetParams {
|
||||
float frame_width{0.0F};
|
||||
float frame_height{0.0F};
|
||||
int frames_per_row{1};
|
||||
int max_tiles{1};
|
||||
};
|
||||
|
||||
// Llegeix el bloc `sprite: { path, size: [w, h] }` — mateix format que
|
||||
// les plantilles d'entitat. La surface es resol via Resource::Cache amb
|
||||
// el basename del fitxer.
|
||||
auto parseSpriteBlock(const fkyaml::node& yaml, std::shared_ptr<Surface>& surface) -> SheetParams {
|
||||
SheetParams params;
|
||||
auto parseSpriteBlock(const fkyaml::node& yaml, std::shared_ptr<Surface>& surface) -> AnimatedSprite::SheetParams {
|
||||
AnimatedSprite::SheetParams params;
|
||||
if (!yaml.contains("sprite")) { return params; }
|
||||
const auto& s = yaml["sprite"];
|
||||
if (s.contains("path")) {
|
||||
@@ -57,62 +40,18 @@ namespace {
|
||||
return params;
|
||||
}
|
||||
|
||||
// `frame_ms` pot ser un escalar (temps uniforme) o una llista (per-frame).
|
||||
// Convertim sempre a segons perquè `animate()` treballa amb delta_time
|
||||
// en segons. Si falta o val 0, `speeds` queda buit i l'animació es
|
||||
// considera estàtica (no avança).
|
||||
void parseFrameTimes(const fkyaml::node& anim_node, AnimatedSprite::AnimationData& animation) {
|
||||
if (!anim_node.contains("frame_ms")) { return; }
|
||||
const auto& fm = anim_node["frame_ms"];
|
||||
if (fm.is_sequence()) {
|
||||
for (const auto& v : fm) {
|
||||
animation.speeds.push_back(static_cast<float>(v.get_value<int>()) / 1000.0F);
|
||||
}
|
||||
} else {
|
||||
const float SEC = static_cast<float>(fm.get_value<int>()) / 1000.0F;
|
||||
if (SEC > 0.0F) { animation.speeds.assign(animation.frames.size(), SEC); }
|
||||
}
|
||||
}
|
||||
|
||||
// Ordre de prioritat per a la política de loop:
|
||||
// 1) `loop_from: N` → rebota al frame N en passar del final (implica loop).
|
||||
// 2) `loop: false` → animació d'un sol tret (es queda al darrer frame).
|
||||
// 3) Res → loop complet des del frame 0 (default).
|
||||
void parseLoopPolicy(const fkyaml::node& anim_node, AnimatedSprite::AnimationData& animation) {
|
||||
if (anim_node.contains("loop_from")) {
|
||||
animation.loop_from = anim_node["loop_from"].get_value<int>();
|
||||
} else if (anim_node.contains("loop") && !anim_node["loop"].get_value<bool>()) {
|
||||
animation.loop_from = -1;
|
||||
} else {
|
||||
animation.loop_from = 0;
|
||||
}
|
||||
}
|
||||
|
||||
auto parseAnimations(const fkyaml::node& yaml, const SheetParams& params) -> std::unordered_map<std::string, AnimatedSprite::AnimationData> {
|
||||
std::unordered_map<std::string, AnimatedSprite::AnimationData> animations;
|
||||
if (!yaml.contains("animations") || !yaml["animations"].is_mapping()) {
|
||||
return animations;
|
||||
}
|
||||
const auto& anims_node = yaml["animations"];
|
||||
for (auto it = anims_node.begin(); it != anims_node.end(); ++it) {
|
||||
const auto NAME = it.key().get_value<std::string>();
|
||||
const auto& anim_node = *it;
|
||||
|
||||
AnimatedSprite::AnimationData animation;
|
||||
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||
animation.frames = convertYamlFramesToRects(
|
||||
anim_node["frames"],
|
||||
params.frame_width,
|
||||
params.frame_height,
|
||||
params.frames_per_row,
|
||||
params.max_tiles);
|
||||
}
|
||||
parseFrameTimes(anim_node, animation);
|
||||
parseLoopPolicy(anim_node, animation);
|
||||
|
||||
animations.emplace(NAME, std::move(animation));
|
||||
}
|
||||
return animations;
|
||||
// Tradueix els flags lògics flip_h/flip_v al `SDL_FlipMode` que admet
|
||||
// `Surface::render`. Important: a SDL3, `SDL_FlipMode` és un enum
|
||||
// exclusiu (NONE/HORIZONTAL/VERTICAL), no un bitmask combinable. Per
|
||||
// tant, V+H simultani al mateix frame no es pot expressar amb una
|
||||
// única crida — prioritzem H (suficient per als casos pràctics
|
||||
// d'animació, com `walk_west` o `death_spin`). Si en algun moment fa
|
||||
// falta combinar, caldrà ampliar `Surface::render` per a doble-flipar
|
||||
// a baix nivell.
|
||||
auto toFlipMode(bool flip_h, bool flip_v) -> SDL_FlipMode {
|
||||
if (flip_h) { return SDL_FLIP_HORIZONTAL; }
|
||||
if (flip_v) { return SDL_FLIP_VERTICAL; }
|
||||
return SDL_FLIP_NONE;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -122,7 +61,7 @@ AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||
auto params = parseSpriteBlock(yaml, surface_);
|
||||
animations_ = parseAnimations(yaml, params);
|
||||
animations_ = parseAnimationsFromYaml(yaml, params);
|
||||
|
||||
setWidth(params.frame_width);
|
||||
setHeight(params.frame_height);
|
||||
@@ -132,7 +71,7 @@ AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
|
||||
// abans del primer `update()`, així que el valor real el posa ell.
|
||||
if (!animations_.empty()) {
|
||||
const auto& first = animations_.begin()->second;
|
||||
if (!first.frames.empty()) { setClip(first.frames[0]); }
|
||||
if (!first.frames.empty()) { setClip(first.frames[0].rect); }
|
||||
}
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
|
||||
@@ -154,11 +93,20 @@ void AnimatedSprite::animate(float delta_time) {
|
||||
|
||||
const float TOTAL = std::accumulate(anim.speeds.begin(), anim.speeds.end(), 0.0F);
|
||||
|
||||
// Helper local per a propagar el frame actual (clip + flip) al sprite.
|
||||
// El motor sempre sobreescriu el flip — si el YAML no declara `flip_h`/
|
||||
// `flip_v`, el frame té els flags a false i el resultat és `SDL_FLIP_NONE`.
|
||||
auto apply_current_frame = [&](int frame_index) {
|
||||
const auto& frame = anim.frames[frame_index];
|
||||
setClip(frame.rect);
|
||||
setFlip(toFlipMode(frame.flip_h, frame.flip_v));
|
||||
};
|
||||
|
||||
if (anim.accumulated_time >= TOTAL) {
|
||||
if (anim.loop_from < 0) {
|
||||
anim.current_frame = static_cast<int>(anim.frames.size()) - 1;
|
||||
anim.completed = true;
|
||||
setClip(anim.frames[anim.current_frame]);
|
||||
apply_current_frame(anim.current_frame);
|
||||
return;
|
||||
}
|
||||
float loop_start = 0.0F;
|
||||
@@ -172,13 +120,13 @@ void AnimatedSprite::animate(float delta_time) {
|
||||
cursor += anim.speeds[i];
|
||||
if (anim.accumulated_time < cursor) {
|
||||
anim.current_frame = i;
|
||||
setClip(anim.frames[i]);
|
||||
apply_current_frame(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
anim.current_frame = static_cast<int>(anim.frames.size()) - 1;
|
||||
setClip(anim.frames[anim.current_frame]);
|
||||
apply_current_frame(anim.current_frame);
|
||||
}
|
||||
|
||||
auto AnimatedSprite::animationIsCompleted() -> bool {
|
||||
@@ -200,7 +148,10 @@ void AnimatedSprite::setCurrentAnimation(const std::string& name) {
|
||||
anim.current_frame = 0;
|
||||
anim.accumulated_time = 0.0F;
|
||||
anim.completed = false;
|
||||
if (!anim.frames.empty()) { setClip(anim.frames[0]); }
|
||||
if (!anim.frames.empty()) {
|
||||
setClip(anim.frames[0].rect);
|
||||
setFlip(toFlipMode(anim.frames[0].flip_h, anim.frames[0].flip_v));
|
||||
}
|
||||
}
|
||||
|
||||
void AnimatedSprite::update(float delta_time) {
|
||||
|
||||
@@ -8,14 +8,41 @@
|
||||
|
||||
#include "core/rendering/sprite/moving_sprite.hpp"
|
||||
#include "core/resources/resource_types.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
class AnimatedSprite : public MovingSprite {
|
||||
public:
|
||||
// Un frame d'animació = rectangle dins el spritesheet + flags de flip
|
||||
// associats. El flip s'aplica al sprite a `animate()` quan l'animació
|
||||
// declara que ella controla el flip (vegeu `AnimationData::controls_flip`).
|
||||
struct Frame {
|
||||
SDL_FRect rect{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
|
||||
bool flip_h{false};
|
||||
bool flip_v{false};
|
||||
};
|
||||
|
||||
// Paràmetres geomètrics del spritesheet, derivats del bloc `sprite:`
|
||||
// del YAML. Es passen al parser d'animacions per a calcular els
|
||||
// rectangles de cada frame a partir dels seus índexs.
|
||||
struct SheetParams {
|
||||
float frame_width{0.0F};
|
||||
float frame_height{0.0F};
|
||||
int frames_per_row{1};
|
||||
int max_tiles{1};
|
||||
};
|
||||
|
||||
// Dades d'una animació concreta dins d'un spritesheet. Les claus del
|
||||
// mapa (`animations_`) són els noms; aquí només es guarden els fotogrames
|
||||
// i l'estat de reproducció.
|
||||
//
|
||||
// El flip és sempre propietat del motor: a cada `animate()` s'escriu el
|
||||
// flip del frame actual al sprite (default `SDL_FLIP_NONE` quan el YAML
|
||||
// no declara res). El codi del joc no pot — ni ha de — tocar el flip
|
||||
// manualment; ho expressa al YAML declarant `flip_h: true` a l'animació
|
||||
// simètrica (p. ex. `walk_west`) o, per a casos puntuals, per frame
|
||||
// amb la forma mapping `{ index, flip_h?, flip_v? }`.
|
||||
struct AnimationData {
|
||||
std::vector<SDL_FRect> frames;
|
||||
std::vector<Frame> frames;
|
||||
// Durada de cada frame en segons (convertit des de `frame_ms` al YAML).
|
||||
// Si té un sol valor, val per a tots els frames; altrament ha de tenir
|
||||
// la mateixa mida que `frames`.
|
||||
@@ -40,6 +67,12 @@ class AnimatedSprite : public MovingSprite {
|
||||
void resetAnimation();
|
||||
void animate(float delta_time);
|
||||
|
||||
// Helper de parsing exposat per a testabilitat. NO toca el cache de
|
||||
// recursos — només el bloc `animations:` del YAML i els paràmetres de
|
||||
// full ja calculats. El ctor d'`AnimatedSprite` el crida internament.
|
||||
static auto parseAnimationsFromYaml(const fkyaml::node& yaml, const SheetParams& params)
|
||||
-> std::unordered_map<std::string, AnimationData>;
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, AnimationData> animations_;
|
||||
// Nom de l'animació activa. Buit fins que algú crida `setCurrentAnimation`.
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||
|
||||
// Implementació del parser pure de YAML d'animacions. Viu en una unitat
|
||||
// separada perquè NO depèn de Resource::Cache ni de Surface — només de
|
||||
// fkyaml — i això permet als tests stand-alone enllaçar només aquest .cpp
|
||||
// per a verificar el parsing sense haver d'arrossegar tot l'stack de
|
||||
// recursos i renderitzat.
|
||||
|
||||
namespace {
|
||||
|
||||
// Construeix el rectangle d'un frame a partir del seu índex dins del
|
||||
// spritesheet. Centralitzat per a no duplicar la fórmula als dos
|
||||
// formats (escalar / mapping) que admet `frames:` al YAML.
|
||||
auto rectFromIndex(int index, const AnimatedSprite::SheetParams& params) -> SDL_FRect {
|
||||
return SDL_FRect{
|
||||
.x = static_cast<float>(index % params.frames_per_row) * params.frame_width,
|
||||
.y = static_cast<float>(index / params.frames_per_row) * params.frame_height,
|
||||
.w = params.frame_width,
|
||||
.h = params.frame_height,
|
||||
};
|
||||
}
|
||||
|
||||
// Llig un node `frames` (seqüència) i el converteix a la nova
|
||||
// representació `vector<Frame>`. Cada element pot ser:
|
||||
// - escalar (int) → hereta `default_flip_h`/`default_flip_v` de l'animació.
|
||||
// - mapping `{ index: N, flip_h?: bool, flip_v?: bool }` → fully
|
||||
// explícit. Els flags absents defaulten a false (NO hereta de l'anim).
|
||||
auto convertYamlFrames(const fkyaml::node& frames_node, const AnimatedSprite::SheetParams& params, bool default_flip_h, bool default_flip_v) -> std::vector<AnimatedSprite::Frame> {
|
||||
std::vector<AnimatedSprite::Frame> frames;
|
||||
for (const auto& frame_node : frames_node) {
|
||||
if (frame_node.is_mapping()) {
|
||||
if (!frame_node.contains("index")) { continue; }
|
||||
const int NUM_TILE = frame_node["index"].get_value<int>();
|
||||
if (NUM_TILE > params.max_tiles) { continue; }
|
||||
AnimatedSprite::Frame frame;
|
||||
frame.rect = rectFromIndex(NUM_TILE, params);
|
||||
if (frame_node.contains("flip_h")) { frame.flip_h = frame_node["flip_h"].get_value<bool>(); }
|
||||
if (frame_node.contains("flip_v")) { frame.flip_v = frame_node["flip_v"].get_value<bool>(); }
|
||||
frames.emplace_back(frame);
|
||||
} else {
|
||||
const int NUM_TILE = frame_node.get_value<int>();
|
||||
if (NUM_TILE > params.max_tiles) { continue; }
|
||||
frames.emplace_back(AnimatedSprite::Frame{
|
||||
.rect = rectFromIndex(NUM_TILE, params),
|
||||
.flip_h = default_flip_h,
|
||||
.flip_v = default_flip_v,
|
||||
});
|
||||
}
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
// `frame_ms` pot ser un escalar (temps uniforme) o una llista (per-frame).
|
||||
// Convertim sempre a segons perquè `animate()` treballa amb delta_time
|
||||
// en segons. Si falta o val 0, `speeds` queda buit i l'animació es
|
||||
// considera estàtica (no avança).
|
||||
void parseFrameTimes(const fkyaml::node& anim_node, AnimatedSprite::AnimationData& animation) {
|
||||
if (!anim_node.contains("frame_ms")) { return; }
|
||||
const auto& fm = anim_node["frame_ms"];
|
||||
if (fm.is_sequence()) {
|
||||
for (const auto& v : fm) {
|
||||
animation.speeds.push_back(static_cast<float>(v.get_value<int>()) / 1000.0F);
|
||||
}
|
||||
} else {
|
||||
const float SEC = static_cast<float>(fm.get_value<int>()) / 1000.0F;
|
||||
if (SEC > 0.0F) { animation.speeds.assign(animation.frames.size(), SEC); }
|
||||
}
|
||||
}
|
||||
|
||||
// Ordre de prioritat per a la política de loop:
|
||||
// 1) `loop_from: N` → rebota al frame N en passar del final (implica loop).
|
||||
// 2) `loop: false` → animació d'un sol tret (es queda al darrer frame).
|
||||
// 3) Res → loop complet des del frame 0 (default).
|
||||
void parseLoopPolicy(const fkyaml::node& anim_node, AnimatedSprite::AnimationData& animation) {
|
||||
if (anim_node.contains("loop_from")) {
|
||||
animation.loop_from = anim_node["loop_from"].get_value<int>();
|
||||
} else if (anim_node.contains("loop") && !anim_node["loop"].get_value<bool>()) {
|
||||
animation.loop_from = -1;
|
||||
} else {
|
||||
animation.loop_from = 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto AnimatedSprite::parseAnimationsFromYaml(const fkyaml::node& yaml, const SheetParams& params)
|
||||
-> std::unordered_map<std::string, AnimationData> {
|
||||
std::unordered_map<std::string, AnimationData> animations;
|
||||
if (!yaml.contains("animations") || !yaml["animations"].is_mapping()) {
|
||||
return animations;
|
||||
}
|
||||
const auto& anims_node = yaml["animations"];
|
||||
for (auto it = anims_node.begin(); it != anims_node.end(); ++it) {
|
||||
const auto NAME = it.key().get_value<std::string>();
|
||||
const auto& anim_node = *it;
|
||||
|
||||
// Flags de flip a nivell d'animació (defaults a false). Els frames
|
||||
// declarats com a int els hereten; els declarats com a mapping no.
|
||||
bool anim_flip_h = false;
|
||||
bool anim_flip_v = false;
|
||||
if (anim_node.contains("flip_h")) { anim_flip_h = anim_node["flip_h"].get_value<bool>(); }
|
||||
if (anim_node.contains("flip_v")) { anim_flip_v = anim_node["flip_v"].get_value<bool>(); }
|
||||
|
||||
AnimationData animation;
|
||||
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||
animation.frames = convertYamlFrames(anim_node["frames"], params, anim_flip_h, anim_flip_v);
|
||||
}
|
||||
parseFrameTimes(anim_node, animation);
|
||||
parseLoopPolicy(anim_node, animation);
|
||||
|
||||
animations.emplace(NAME, std::move(animation));
|
||||
}
|
||||
return animations;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ class MovingSprite : public Sprite {
|
||||
void setAccelY(float value) { ay_ = value; }
|
||||
|
||||
void setFlip(SDL_FlipMode flip) { flip_ = flip; }
|
||||
auto getFlip() -> SDL_FlipMode { return flip_; }
|
||||
[[nodiscard]] auto getFlip() const -> SDL_FlipMode { return flip_; }
|
||||
void flip() { flip_ = (flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL; }
|
||||
|
||||
// Moviment eix-separat amb col·lisió: aplica `dx` (i si la nova
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <utility>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/audio/sound_effects_config.hpp"
|
||||
#include "core/input/gamepad_bindings.hpp"
|
||||
#include "core/input/gamepad_manager.hpp"
|
||||
#include "core/input/key_config.hpp"
|
||||
#include "core/input/mouse.hpp"
|
||||
@@ -27,6 +29,7 @@
|
||||
#include "core/system/event_buffer.hpp"
|
||||
#include "core/ui/ui_config.hpp"
|
||||
#include "game/debug_config.hpp"
|
||||
#include "game/levels/level_manager.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "game/scenes/game_scene.hpp"
|
||||
#include "game/scenes/intro_scene.hpp"
|
||||
@@ -80,6 +83,11 @@ Director::Director() {
|
||||
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n';
|
||||
}
|
||||
|
||||
// Vinculacions de mando per dispositiu — fitxer aïllat de `config.yaml`
|
||||
// perquè és per (name, path) del mando, no per perfil/usuari. Replica
|
||||
// el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`).
|
||||
GamepadBindings::get().load(system_folder_ + "gamepad_configs.yaml");
|
||||
|
||||
// Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix
|
||||
// es crea amb presets per defecte (off, crt, ntsc).
|
||||
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
|
||||
@@ -121,10 +129,26 @@ Director::Director() {
|
||||
const std::string PLACEHOLDER_FONT_GIF_KEY = UiConfig::get().placeholderFont().name + ".gif";
|
||||
Resource::Cache::get().loadSurface(PLACEHOLDER_FONT_GIF_KEY, UiConfig::get().placeholderFont().gif);
|
||||
Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
||||
// Font del rètol de transició de sala (la consumeix `GameScene`
|
||||
// tant per la línia "Piràmide N / Sala M" de l'INTRO com pel
|
||||
// "Fase completada" de l'OUTRO). Es preloda ací per consistència
|
||||
// amb la resta de fonts d'UI; sense això, la primera entrada a
|
||||
// GAME aniria a disc.
|
||||
const std::string ROOM_TEXT_FONT_GIF_KEY = UiConfig::get().roomTextFont().name + ".gif";
|
||||
Resource::Cache::get().loadSurface(ROOM_TEXT_FONT_GIF_KEY, UiConfig::get().roomTextFont().gif);
|
||||
Resource::Cache::get().getSurface(ROOM_TEXT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
||||
|
||||
// Música de la piràmide 1 (primer i únic track de moment). Carreguem ací
|
||||
// perquè forma part del preload d'assets, com la font i la paleta.
|
||||
Resource::Cache::get().loadMusic("pyramid_1.ogg", "data/music/pyramid_1.ogg");
|
||||
// Pistes de música del cicle de sala (intro/playing/outro). Es preloden
|
||||
// ací perquè formen part del preload d'assets, com la font i la paleta;
|
||||
// la lògica de quina sona en cada estat viu a `GameScene`.
|
||||
Resource::Cache::get().loadMusic("room_intro.ogg", "data/music/room_intro.ogg");
|
||||
Resource::Cache::get().loadMusic("playing.ogg", "data/music/playing.ogg");
|
||||
Resource::Cache::get().loadMusic("room_outro.ogg", "data/music/room_outro.ogg");
|
||||
|
||||
// Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio
|
||||
// perquè els primers playSoundWith* (al començament del joc) ja
|
||||
// troben els presets disponibles.
|
||||
SoundEffectsConfig::get().load("data/sounds.yaml");
|
||||
|
||||
// Audio: singleton manual propietari d'un SDL audio device. Es destrueix
|
||||
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els
|
||||
@@ -182,6 +206,13 @@ Director::Director() {
|
||||
DebugConfig::createDefaultIfMissing(DEBUG_YAML);
|
||||
const auto START = DebugConfig::resolveStartScene(DEBUG_YAML);
|
||||
const auto STARTING_PLAYERS = DebugConfig::resolveStartingPlayers(DEBUG_YAML);
|
||||
// `starting_level` (opcional) sobreescriu l'índex inicial del
|
||||
// LevelManager abans de crear cap GameScene. Si la clau no està o el
|
||||
// (pyramid, room) no apareix al levels.yaml, el LevelManager
|
||||
// continua a la primera entrada (P1-R1).
|
||||
if (const auto SL = DebugConfig::resolveStartingLevel(DEBUG_YAML); SL.has_value()) {
|
||||
Levels::LevelManager::get().setStartingPoint(SL->pyramid, SL->room);
|
||||
}
|
||||
#else
|
||||
const auto START = DebugConfig::StartScene::LOGO;
|
||||
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
|
||||
@@ -189,6 +220,12 @@ Director::Director() {
|
||||
std::cout << "Director: start_scene=" << DebugConfig::toString(START)
|
||||
<< " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n';
|
||||
|
||||
// El branch Release inicialitza STARTING_PLAYERS amb una constant
|
||||
// literal i clang considera la captura redundant; en Debug el valor és
|
||||
// runtime i la captura sí és imprescindible. La lambda viu dins de
|
||||
// IntroScene, més enllà del scope d'aquest ctor — capturar per
|
||||
// referència seria UB.
|
||||
// NOLINTNEXTLINE(clang-diagnostic-unused-lambda-capture)
|
||||
auto make_game = [STARTING_PLAYERS]() -> std::unique_ptr<Scene> {
|
||||
auto cfg = GameScene::defaultConfig();
|
||||
cfg.starting_players = STARTING_PLAYERS;
|
||||
@@ -265,12 +302,29 @@ void Director::processEvents() {
|
||||
if (QUIT_REQUESTED) { current_scene_.reset(); }
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Resol el botó SERVICE configurat per al mando origen d'aquest event.
|
||||
// Retorna -1 si no en troba cap. La cerca passa per `GamepadManager`
|
||||
// per obtenir el (name, path) i d'ahí a `GamepadBindings`.
|
||||
auto serviceButtonForGamepadEvent(SDL_JoystickID jid) -> int {
|
||||
for (const auto& info : GamepadManager::get().getAll()) {
|
||||
if (info.pad == nullptr) { continue; }
|
||||
if (SDL_GetJoystickID(SDL_GetGamepadJoystick(info.pad)) != jid) { continue; }
|
||||
return GamepadBindings::get().getButton(info.name, info.path, GamepadBindings::Action::SERVICE);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Director::processGamepadEvents() {
|
||||
// Dues responsabilitats: (1) passar ADDED/REMOVED al GamepadManager
|
||||
// perquè obri/tanqui mandos i emeti les notificacions hot-plug; (2)
|
||||
// tractar el botó BACK com a toggle del menú quan està tancat (el
|
||||
// paral·lel de F9 per al teclat). Quan el menú està obert, el BACK
|
||||
// passa pel pipeline del `ServiceMenu::handleEvent()` via
|
||||
// tractar el botó SERVICE (per defecte BACK; remapejable per
|
||||
// `GamepadBindings`) com a toggle del menú quan està tancat (el
|
||||
// paral·lel de F9 per al teclat). Quan el menú està obert, el botó
|
||||
// SERVICE passa pel pipeline del `ServiceMenu::handleEvent()` via
|
||||
// `processMenuInput()` i allà es converteix en `close()`.
|
||||
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
|
||||
bool consumed = false;
|
||||
@@ -278,10 +332,12 @@ void Director::processGamepadEvents() {
|
||||
GamepadManager::get().handleEvent(*it);
|
||||
consumed = true;
|
||||
} else if (it->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN &&
|
||||
it->gbutton.button == SDL_GAMEPAD_BUTTON_BACK &&
|
||||
!ServiceMenu::get().isOpen()) {
|
||||
ServiceMenu::get().toggle();
|
||||
consumed = true;
|
||||
const int SERVICE_BTN = serviceButtonForGamepadEvent(it->gbutton.which);
|
||||
if (SERVICE_BTN >= 0 && static_cast<int>(it->gbutton.button) == SERVICE_BTN) {
|
||||
ServiceMenu::get().toggle();
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
if (consumed) {
|
||||
it = EventBuffer::events.erase(it);
|
||||
|
||||
@@ -85,6 +85,8 @@ UiConfig::UiConfig()
|
||||
hud_label_(Defaults::UI::HUD_LABEL),
|
||||
hud_value_(Defaults::UI::HUD_VALUE),
|
||||
hud_flash_(Defaults::UI::HUD_FLASH),
|
||||
room_text_(Defaults::UI::ROOM_TEXT),
|
||||
room_text_outline_(Defaults::UI::ROOM_TEXT_OUTLINE),
|
||||
notification_ms_(Defaults::UI::NOTIFICATION_TIME_MS),
|
||||
exit_ms_(Defaults::UI::EXIT_TIME_MS),
|
||||
hud_entry_ms_(Defaults::UI::HUD_ENTRY_MS),
|
||||
@@ -96,6 +98,7 @@ UiConfig::UiConfig()
|
||||
pause_exit_curve_(curveByName(Defaults::UI::PAUSE_EXIT_CURVE)),
|
||||
hud_font_{.name = Defaults::UI::Font::HUD_NAME, .gif = Defaults::UI::Font::HUD_GIF, .fnt = Defaults::UI::Font::HUD_FNT, .kerning = Defaults::UI::Font::HUD_KERNING},
|
||||
placeholder_font_{.name = Defaults::UI::Font::PLACEHOLDER_NAME, .gif = Defaults::UI::Font::PLACEHOLDER_GIF, .fnt = Defaults::UI::Font::PLACEHOLDER_FNT, .kerning = Defaults::UI::Font::PLACEHOLDER_KERNING},
|
||||
room_text_font_{.name = Defaults::UI::Font::ROOM_TEXT_NAME, .gif = Defaults::UI::Font::ROOM_TEXT_GIF, .fnt = Defaults::UI::Font::ROOM_TEXT_FNT, .kerning = Defaults::UI::Font::ROOM_TEXT_KERNING},
|
||||
menu_title_font_{.name = Defaults::UI::Font::MENU_TITLE_NAME, .gif = Defaults::UI::Font::MENU_TITLE_GIF, .fnt = Defaults::UI::Font::MENU_TITLE_FNT, .kerning = Defaults::UI::Font::MENU_TITLE_KERNING},
|
||||
menu_items_font_{.name = Defaults::UI::Font::MENU_ITEMS_NAME, .gif = Defaults::UI::Font::MENU_ITEMS_GIF, .fnt = Defaults::UI::Font::MENU_ITEMS_FNT, .kerning = Defaults::UI::Font::MENU_ITEMS_KERNING} {
|
||||
}
|
||||
@@ -147,6 +150,8 @@ void UiConfig::load(const std::string& file_path) {
|
||||
readColorIndex(c, "hud_label", hud_label_);
|
||||
readColorIndex(c, "hud_value", hud_value_);
|
||||
readColorIndex(c, "hud_flash", hud_flash_);
|
||||
readColorIndex(c, "room_text", room_text_);
|
||||
readColorIndex(c, "room_text_outline", room_text_outline_);
|
||||
}
|
||||
|
||||
if (yaml.contains("timings")) {
|
||||
@@ -159,6 +164,7 @@ void UiConfig::load(const std::string& file_path) {
|
||||
const auto& f = yaml["fonts"];
|
||||
readFont(f, "hud", hud_font_);
|
||||
readFont(f, "placeholder", placeholder_font_);
|
||||
readFont(f, "room_text", room_text_font_);
|
||||
readFont(f, "menu_title", menu_title_font_);
|
||||
readFont(f, "menu_items", menu_items_font_);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ class UiConfig {
|
||||
[[nodiscard]] auto hudLabel() const -> Uint8 { return hud_label_; }
|
||||
[[nodiscard]] auto hudValue() const -> Uint8 { return hud_value_; }
|
||||
[[nodiscard]] auto hudFlash() const -> Uint8 { return hud_flash_; }
|
||||
[[nodiscard]] auto roomText() const -> Uint8 { return room_text_; }
|
||||
[[nodiscard]] auto roomTextOutline() const -> Uint8 { return room_text_outline_; }
|
||||
|
||||
// Signatura d'una funció d'easing: t ∈ [0, 1] → valor ∈ [0, 1].
|
||||
// Permet triar corbes des de data/ui.yaml per nom (cadena) i guardar
|
||||
@@ -77,6 +79,7 @@ class UiConfig {
|
||||
// Fonts de UI.
|
||||
[[nodiscard]] auto hudFont() const -> const Font& { return hud_font_; }
|
||||
[[nodiscard]] auto placeholderFont() const -> const Font& { return placeholder_font_; }
|
||||
[[nodiscard]] auto roomTextFont() const -> const Font& { return room_text_font_; }
|
||||
[[nodiscard]] auto menuTitleFont() const -> const Font& { return menu_title_font_; }
|
||||
[[nodiscard]] auto menuItemsFont() const -> const Font& { return menu_items_font_; }
|
||||
|
||||
@@ -105,6 +108,8 @@ class UiConfig {
|
||||
Uint8 hud_label_;
|
||||
Uint8 hud_value_;
|
||||
Uint8 hud_flash_;
|
||||
Uint8 room_text_;
|
||||
Uint8 room_text_outline_;
|
||||
|
||||
int notification_ms_;
|
||||
int exit_ms_;
|
||||
@@ -118,6 +123,7 @@ class UiConfig {
|
||||
|
||||
Font hud_font_;
|
||||
Font placeholder_font_;
|
||||
Font room_text_font_;
|
||||
Font menu_title_font_;
|
||||
Font menu_items_font_;
|
||||
|
||||
|
||||
@@ -103,6 +103,34 @@ namespace DebugConfig {
|
||||
return parseStartingPlayers(CONTENT);
|
||||
}
|
||||
|
||||
auto parseStartingLevel(const std::string& yaml_content) -> std::optional<StartingLevel> {
|
||||
if (yaml_content.empty()) { return std::nullopt; }
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||
if (!yaml.contains("starting_level")) { return std::nullopt; }
|
||||
const auto& sl = yaml["starting_level"];
|
||||
if (!sl.contains("pyramid") || !sl.contains("room")) {
|
||||
std::cerr << "debug.yaml: starting_level necessita els camps `pyramid` i `room` — ignorat\n";
|
||||
return std::nullopt;
|
||||
}
|
||||
return StartingLevel{
|
||||
.pyramid = sl["pyramid"].get_value<int>(),
|
||||
.room = sl["room"].get_value<int>(),
|
||||
};
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "debug.yaml: error parsejant starting_level: " << e.what() << " — ignorat\n";
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
auto resolveStartingLevel(const std::string& file_path) -> std::optional<StartingLevel> {
|
||||
std::error_code error;
|
||||
if (!std::filesystem::exists(file_path, error)) { return std::nullopt; }
|
||||
const auto CONTENT = readFile(file_path);
|
||||
if (CONTENT.empty()) { return std::nullopt; }
|
||||
return parseStartingLevel(CONTENT);
|
||||
}
|
||||
|
||||
auto toString(StartScene scene) -> const char* {
|
||||
switch (scene) {
|
||||
case StartScene::LOGO:
|
||||
@@ -140,6 +168,9 @@ namespace DebugConfig {
|
||||
file << "# debug.yaml — ajust personal de dev (no versionat).\n";
|
||||
file << "# start_scene: LOGO | TITLE | INTRO | GAME\n";
|
||||
file << "# starting_players: P1 | P2 | BOTH\n";
|
||||
file << "# starting_level (opcional): { pyramid: N, room: M } — salta a la sala\n";
|
||||
file << "# corresponent del levels.yaml. Sense la\n";
|
||||
file << "# clau, comença a la primera (P1-R1).\n";
|
||||
file << "start_scene: GAME\n";
|
||||
file << "starting_players: BOTH\n";
|
||||
std::cout << "debug.yaml: creat a " << file_path << " amb start_scene=GAME starting_players=BOTH\n";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
// Configuració personal de desenvolupament, carregada de `debug.yaml` dins la
|
||||
@@ -37,6 +38,18 @@ namespace DebugConfig {
|
||||
[[nodiscard]] auto parseStartingPlayers(const std::string& yaml_content) -> StartingPlayers;
|
||||
[[nodiscard]] auto resolveStartingPlayers(const std::string& file_path) -> StartingPlayers;
|
||||
|
||||
// Punt d'arrencada dins de la progressió de sales (consumit pel
|
||||
// `Levels::LevelManager`). YAML:
|
||||
// starting_level: { pyramid: 2, room: 3 }
|
||||
// L'absència de la clau és el cas habitual i deixa la progressió a
|
||||
// la primera entrada del `levels.yaml`.
|
||||
struct StartingLevel {
|
||||
int pyramid{0};
|
||||
int room{0};
|
||||
};
|
||||
[[nodiscard]] auto parseStartingLevel(const std::string& yaml_content) -> std::optional<StartingLevel>;
|
||||
[[nodiscard]] auto resolveStartingLevel(const std::string& file_path) -> std::optional<StartingLevel>;
|
||||
|
||||
// Nom canònic de l'enum (útil per a logs): "LOGO" / "TITLE" / "INTRO" / "GAME".
|
||||
[[nodiscard]] auto toString(StartScene scene) -> const char*;
|
||||
[[nodiscard]] auto toString(StartingPlayers players) -> const char*;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "core/system/event_buffer.hpp"
|
||||
#include "game/levels/level_manager.hpp"
|
||||
#include "game/map/tilemap.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
#include "utils/defines.hpp"
|
||||
@@ -76,11 +77,13 @@ void Editor::exit(Tilemap& tilemap) {
|
||||
if (!active_) { return; }
|
||||
active_ = false;
|
||||
picker_open_ = false;
|
||||
// Resolem la ruta via Resource::Helper perquè en Debug el root de
|
||||
// dades és AEE_SOURCE_DIR (ruta absoluta al repo), no el CWD. Sense
|
||||
// això, el writer obriria `data/maps/pyramid.yaml` relatiu al CWD
|
||||
// i fallaria sempre excepte si el binari es llança des del repo root.
|
||||
const auto ABS_PATH = Resource::Helper::resolve(Defaults::Editor::SAVE_PATH);
|
||||
// El writer salva sempre sobre la sala actualment carregada (segons
|
||||
// el LevelManager): així el "9" de l'editor desa el que tens davant,
|
||||
// no una constant fixa. Resolem la ruta via Resource::Helper perquè
|
||||
// en Debug el root de dades és AEE_SOURCE_DIR (ruta absoluta al repo),
|
||||
// no el CWD; sense això, el writer obriria el path relatiu al CWD i
|
||||
// fallaria si el binari no es llança des del repo root.
|
||||
const auto ABS_PATH = Resource::Helper::resolve(Levels::LevelManager::get().currentPath());
|
||||
const bool SAVED = tilemap.save(ABS_PATH);
|
||||
Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error"));
|
||||
}
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
#include <utility>
|
||||
|
||||
AnimationComponent::AnimationComponent(std::vector<int> frames, float fps, bool loop)
|
||||
: AnimationComponent(std::move(frames), fps, loop, false, false) {}
|
||||
|
||||
AnimationComponent::AnimationComponent(std::vector<int> frames, float fps, bool loop, bool flip_h, bool flip_v)
|
||||
: frames_(std::move(frames)),
|
||||
fps_(fps > 0.0F ? fps : 1.0F),
|
||||
loop_(loop) {
|
||||
loop_(loop),
|
||||
flip_h_(flip_h),
|
||||
flip_v_(flip_v) {
|
||||
// Una animació d'1 sol frame amb loop=false és una animació que ja s'ha
|
||||
// acabat de reproduir: no hi ha cap transició per mostrar. Marcant-la
|
||||
// com a completada des de l'inici permet als consumidors (p.ex. Door)
|
||||
@@ -46,3 +51,9 @@ auto AnimationComponent::currentFrame() const -> int {
|
||||
if (frames_.empty()) { return 0; }
|
||||
return frames_[static_cast<std::size_t>(current_)];
|
||||
}
|
||||
|
||||
auto AnimationComponent::flipMode() const -> SDL_FlipMode {
|
||||
if (flip_h_) { return SDL_FLIP_HORIZONTAL; }
|
||||
if (flip_v_) { return SDL_FLIP_VERTICAL; }
|
||||
return SDL_FLIP_NONE;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
@@ -13,10 +15,18 @@
|
||||
// algun dia aterri l'art definitiu, el renderer consultarà `currentFrame()`
|
||||
// i traduirà l'índex al rectangle del sprite corresponent. De moment els
|
||||
// placeholders tenen un únic frame i `currentFrame()` retorna sempre 0.
|
||||
//
|
||||
// El flip (H/V) és propietat de l'animació, no del codi del joc: el YAML
|
||||
// declara `flip_h`/`flip_v` al bloc d'animació i el component els exposa
|
||||
// via `flipMode()`. Les entitats apliquen aquest valor al seu `flip_` quan
|
||||
// canvien d'animació, sense decidir-lo per direcció. Així una mòmia que va
|
||||
// cap a l'oest reprodueix `walk_west` (frames de l'est + `flip_h: true`)
|
||||
// sense que el codi de la mòmia toqui flip directament.
|
||||
class AnimationComponent {
|
||||
public:
|
||||
AnimationComponent() = default;
|
||||
AnimationComponent(std::vector<int> frames, float fps, bool loop);
|
||||
AnimationComponent(std::vector<int> frames, float fps, bool loop, bool flip_h, bool flip_v);
|
||||
|
||||
// Avanç del temps. Si `frames_` està buit o té un sol element, no fa
|
||||
// res. En mode loop, `current_frame_` rebota a 0 en passar del final;
|
||||
@@ -37,10 +47,19 @@ class AnimationComponent {
|
||||
[[nodiscard]] auto completed() const -> bool { return completed_; }
|
||||
[[nodiscard]] auto loops() const -> bool { return loop_; }
|
||||
|
||||
[[nodiscard]] auto flipH() const -> bool { return flip_h_; }
|
||||
[[nodiscard]] auto flipV() const -> bool { return flip_v_; }
|
||||
// Combinació dels dos flags al `SDL_FlipMode` que entén el render.
|
||||
// SDL3 no admet bitmask: si fos VH simultani, prioritzem H (cap cas
|
||||
// pràctic actual el necessita combinat).
|
||||
[[nodiscard]] auto flipMode() const -> SDL_FlipMode;
|
||||
|
||||
private:
|
||||
std::vector<int> frames_;
|
||||
float fps_{1.0F};
|
||||
bool loop_{true};
|
||||
bool flip_h_{false};
|
||||
bool flip_v_{false};
|
||||
int current_{0};
|
||||
float accumulator_{0.0F};
|
||||
bool completed_{false};
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
@@ -26,11 +26,15 @@ namespace {
|
||||
// Llegeix un bloc d'animació del YAML amb fallbacks tolerants (mateix
|
||||
// patró que Door/Sarcophagus). Si la clau no apareix, retorna una
|
||||
// animació d'1 frame loopant, neutra — la momia es podrà moure però
|
||||
// visualment es quedarà estàtica.
|
||||
// visualment es quedarà estàtica. Llegeix `flip_h`/`flip_v` (default
|
||||
// false) i els passa al component perquè el motor sàpiga aplicar-los
|
||||
// al sprite quan aquesta animació estigui activa.
|
||||
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
||||
std::vector<int> frames = {0};
|
||||
float fps = 8.0F;
|
||||
bool loop = true;
|
||||
bool flip_h = false;
|
||||
bool flip_v = false;
|
||||
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
|
||||
const auto& anim = yaml["animations"][key];
|
||||
if (anim.contains("frames")) {
|
||||
@@ -43,8 +47,10 @@ namespace {
|
||||
}
|
||||
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
|
||||
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
|
||||
if (anim.contains("flip_h")) { flip_h = anim["flip_h"].get_value<bool>(); }
|
||||
if (anim.contains("flip_v")) { flip_v = anim["flip_v"].get_value<bool>(); }
|
||||
}
|
||||
return {frames, fps, loop};
|
||||
return {frames, fps, loop, flip_h, flip_v};
|
||||
}
|
||||
|
||||
// Busca si el segment [prev, prev+delta] travessa una lane del patró
|
||||
@@ -73,57 +79,83 @@ namespace {
|
||||
} // namespace
|
||||
|
||||
Mummy::Mummy(const fkyaml::node& yaml)
|
||||
: anim_walk_down_(buildAnimation(yaml, "walk_down")),
|
||||
anim_walk_up_(buildAnimation(yaml, "walk_up")),
|
||||
anim_walk_lateral_(buildAnimation(yaml, "walk")) {
|
||||
: anim_walk_south_(buildAnimation(yaml, "walk_south")),
|
||||
anim_walk_north_(buildAnimation(yaml, "walk_north")),
|
||||
anim_walk_east_(buildAnimation(yaml, "walk_east")),
|
||||
anim_walk_west_(buildAnimation(yaml, "walk_west")) {
|
||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
|
||||
collides_ = true;
|
||||
interactable_ = false;
|
||||
|
||||
// Velocitat i dany des del bloc `stats` (si existeix).
|
||||
// Velocitat i dany des del bloc `stats` (si existeix). El flag
|
||||
// `kills_outright` també hi viu: per defecte true (la mòmia mata
|
||||
// d'un sol toc); posat a false al YAML, el contacte només publica
|
||||
// `player_hit` sense disparar el cicle DYING.
|
||||
if (yaml.contains("stats")) {
|
||||
const auto& s = yaml["stats"];
|
||||
if (s.contains("speed")) { setSpeed(s["speed"].get_value<float>()); }
|
||||
if (s.contains("damage")) { setDamage(s["damage"].get_value<int>()); }
|
||||
if (s.contains("kills_outright")) { kills_outright_ = s["kills_outright"].get_value<bool>(); }
|
||||
}
|
||||
|
||||
// Direcció inicial llegida del bloc `ai:`. Per defecte dreta, per
|
||||
// coherència amb el que fa RollingRock. Valors acceptats: right, left,
|
||||
// up, down. Qualsevol altre s'interpreta com a "sense direcció" (la
|
||||
// momia no es mou fins que step() no triï una direcció).
|
||||
std::string initial = "right";
|
||||
// Direcció inicial llegida del bloc `ai:`. Per defecte est, per
|
||||
// coherència amb el que fa RollingRock. Valors acceptats:
|
||||
// east, west, north, south. Qualsevol altre s'interpreta com a
|
||||
// "sense direcció" (la momia no es mou fins que step() no triï una
|
||||
// direcció).
|
||||
std::string initial = "east";
|
||||
if (yaml.contains("ai") && yaml["ai"].contains("initial_direction")) {
|
||||
initial = yaml["ai"]["initial_direction"].get_value<std::string>();
|
||||
}
|
||||
if (initial == "right") {
|
||||
if (initial == "east") {
|
||||
changeDirection(1.0F, 0.0F);
|
||||
} else if (initial == "left") {
|
||||
} else if (initial == "west") {
|
||||
changeDirection(-1.0F, 0.0F);
|
||||
} else if (initial == "up") {
|
||||
} else if (initial == "north") {
|
||||
changeDirection(0.0F, -1.0F);
|
||||
} else if (initial == "down") {
|
||||
} else if (initial == "south") {
|
||||
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) {
|
||||
setDirection(dx, dy);
|
||||
// Prevalença lateral: si hi ha component horitzontal s'usa l'anim
|
||||
// lateral + flip segons el signe. Només direccions purament verticals
|
||||
// disparen walk_up/walk_down. Quan la direcció queda (0,0) (cul-de-sac
|
||||
// amb NONE) no toquem l'animació: la momia es quedarà al frame actual
|
||||
// fins que step() li torni a assignar direcció.
|
||||
if (dx != 0.0F) {
|
||||
anim_ = anim_walk_lateral_;
|
||||
flip_ = (dx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
||||
// Una animació per cada cardinal — el flip viu al YAML
|
||||
// (`walk_west: flip_h: true`) i el codi només selecciona quina
|
||||
// animació mostrar. Prevalença lateral en diagonals; quan la direcció
|
||||
// queda (0,0) (cul-de-sac amb NONE) no toquem l'animació: la mòmia es
|
||||
// quedarà al frame actual fins que step() li torni a assignar direcció.
|
||||
if (dx > 0.0F) {
|
||||
anim_ = anim_walk_east_;
|
||||
} else if (dx < 0.0F) {
|
||||
anim_ = anim_walk_west_;
|
||||
} else if (dy < 0.0F) {
|
||||
anim_ = anim_walk_up_;
|
||||
flip_ = SDL_FLIP_NONE;
|
||||
anim_ = anim_walk_north_;
|
||||
} else if (dy > 0.0F) {
|
||||
anim_ = anim_walk_down_;
|
||||
flip_ = SDL_FLIP_NONE;
|
||||
anim_ = anim_walk_south_;
|
||||
}
|
||||
if (anim_.has_value()) {
|
||||
anim_->reset();
|
||||
// Propaguem el flip que l'animació declara al YAML al camp
|
||||
// `flip_` que el render llegeix (entity_manager_loadlevel.cpp).
|
||||
flip_ = anim_->flipMode();
|
||||
}
|
||||
if (anim_.has_value()) { anim_->reset(); }
|
||||
}
|
||||
|
||||
auto Mummy::chooseDirection(const std::vector<Direction>& allowed) -> Direction {
|
||||
@@ -218,10 +250,10 @@ auto Mummy::detectLaneCrossing(float dx, float dy) const -> std::optional<float>
|
||||
}
|
||||
|
||||
auto Mummy::currentDirection() const -> Direction {
|
||||
if (dir_x_ > 0.0F) { return Direction::RIGHT; }
|
||||
if (dir_x_ < 0.0F) { return Direction::LEFT; }
|
||||
if (dir_y_ > 0.0F) { return Direction::DOWN; }
|
||||
if (dir_y_ < 0.0F) { return Direction::UP; }
|
||||
if (dir_x_ > 0.0F) { return Direction::EAST; }
|
||||
if (dir_x_ < 0.0F) { return Direction::WEST; }
|
||||
if (dir_y_ > 0.0F) { return Direction::SOUTH; }
|
||||
if (dir_y_ < 0.0F) { return Direction::NORTH; }
|
||||
return Direction::NONE;
|
||||
}
|
||||
|
||||
@@ -252,10 +284,10 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
||||
Direction dir;
|
||||
};
|
||||
const std::array<Candidate, 4> ALL{{
|
||||
{.dx = step, .dy = 0.0F, .dir = Direction::RIGHT},
|
||||
{.dx = -step, .dy = 0.0F, .dir = Direction::LEFT},
|
||||
{.dx = 0.0F, .dy = -step, .dir = Direction::UP},
|
||||
{.dx = 0.0F, .dy = step, .dir = Direction::DOWN},
|
||||
{.dx = step, .dy = 0.0F, .dir = Direction::EAST},
|
||||
{.dx = -step, .dy = 0.0F, .dir = Direction::WEST},
|
||||
{.dx = 0.0F, .dy = -step, .dir = Direction::NORTH},
|
||||
{.dx = 0.0F, .dy = step, .dir = Direction::SOUTH},
|
||||
}};
|
||||
DirectionPools pools;
|
||||
for (const auto& c : ALL) {
|
||||
@@ -277,16 +309,16 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
||||
|
||||
void Mummy::applyChoice(Direction dir) {
|
||||
switch (dir) {
|
||||
case Direction::UP:
|
||||
case Direction::NORTH:
|
||||
changeDirection(0.0F, -1.0F);
|
||||
break;
|
||||
case Direction::DOWN:
|
||||
case Direction::SOUTH:
|
||||
changeDirection(0.0F, 1.0F);
|
||||
break;
|
||||
case Direction::LEFT:
|
||||
case Direction::WEST:
|
||||
changeDirection(-1.0F, 0.0F);
|
||||
break;
|
||||
case Direction::RIGHT:
|
||||
case Direction::EAST:
|
||||
changeDirection(1.0F, 0.0F);
|
||||
break;
|
||||
case Direction::NONE:
|
||||
@@ -296,13 +328,141 @@ void Mummy::applyChoice(Direction dir) {
|
||||
}
|
||||
|
||||
void Mummy::onPlayerContact(Player* player) {
|
||||
if (bus_ != nullptr) {
|
||||
if (bus_ == nullptr) { return; }
|
||||
const int PID = (player != nullptr) ? player->getId() : -1;
|
||||
bus_->publish(Event{
|
||||
.name = "player_hit",
|
||||
.payload = getDamage(),
|
||||
.player_id = PID,
|
||||
});
|
||||
// Path letal: si la mòmia està marcada com a `kills_outright`
|
||||
// (default a Oh Mummy!), notifiquem al GameScene que el jugador
|
||||
// ha de morir. La transició concreta (`Player::enterDying`) la fa
|
||||
// el subscriptor — així mummy.cpp no toca cap símbol del Player més
|
||||
// enllà de la forward-decl, i els tests d'entitats no han d'arrossegar
|
||||
// l'stack pesat del jugador per linkar-se. El subscriptor (GameScene)
|
||||
// és qui resol el `player_id` a la instància concreta.
|
||||
if (kills_outright_) {
|
||||
bus_->publish(Event{
|
||||
.name = "player_hit",
|
||||
.name = "player_killed",
|
||||
.payload = getDamage(),
|
||||
.player_id = (player != nullptr) ? player->getId() : -1,
|
||||
.player_id = PID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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::NORTH:
|
||||
dy = -STEP;
|
||||
break;
|
||||
case Direction::SOUTH:
|
||||
dy = STEP;
|
||||
break;
|
||||
case Direction::WEST:
|
||||
dx = -STEP;
|
||||
break;
|
||||
case Direction::EAST:
|
||||
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::NORTH:
|
||||
left = Direction::WEST;
|
||||
right = Direction::EAST;
|
||||
break;
|
||||
case Direction::SOUTH:
|
||||
left = Direction::EAST;
|
||||
right = Direction::WEST;
|
||||
break;
|
||||
case Direction::WEST:
|
||||
left = Direction::SOUTH;
|
||||
right = Direction::NORTH;
|
||||
break;
|
||||
case Direction::EAST:
|
||||
left = Direction::NORTH;
|
||||
right = Direction::SOUTH;
|
||||
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)
|
||||
@@ -16,10 +16,25 @@
|
||||
class Mummy : public DynamicHazard {
|
||||
public:
|
||||
enum class Direction : std::uint8_t { NONE,
|
||||
UP,
|
||||
DOWN,
|
||||
LEFT,
|
||||
RIGHT };
|
||||
NORTH,
|
||||
SOUTH,
|
||||
WEST,
|
||||
EAST };
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -39,9 +54,23 @@ class Mummy : public DynamicHazard {
|
||||
// En ambdós casos el frame de gir no consumeix avenç extra.
|
||||
void step(float delta_time) override;
|
||||
|
||||
// Update sencer: afegim la branca HURT (avanç del timer + step recta).
|
||||
// En estat ROAMING delega a `step()` com sempre via la base.
|
||||
void update(float delta_time) override;
|
||||
|
||||
// Contacte amb el jugador: publica `player_hit` amb el dany.
|
||||
void onPlayerContact(Player* player) override;
|
||||
|
||||
// Activa l'estat HURT amb la direcció de fuga preferida `flee_dir`.
|
||||
// Si està bloquejada, prova perpendiculars; si totes estan bloquejades,
|
||||
// la mòmia queda quieta però en HURT durant tot el timer.
|
||||
void enterHurt(Direction flee_dir);
|
||||
|
||||
[[nodiscard]] auto getState() const -> State { return state_; }
|
||||
[[nodiscard]] auto isWhippable() const -> bool { return whippable_; }
|
||||
[[nodiscard]] auto getHurtRemainingS() const -> float { return hurt_remaining_s_; }
|
||||
[[nodiscard]] auto killsOutright() const -> bool { return kills_outright_; }
|
||||
|
||||
private:
|
||||
// Caixa de col·lisió candidata si l'entitat es desplaçara (dx, dy).
|
||||
[[nodiscard]] auto candidateBox(float dx, float dy) const -> SDL_FRect;
|
||||
@@ -63,10 +92,11 @@ class Mummy : public DynamicHazard {
|
||||
};
|
||||
[[nodiscard]] auto collectDirectionPools(float step) const -> DirectionPools;
|
||||
|
||||
// Canvi de direcció + aplicació de l'animació i flip corresponents.
|
||||
// Tots els camins que trien nova direcció (ctor amb `initial_direction`,
|
||||
// gir després d'un bloqueig) passen per ací per mantenir anim_ i
|
||||
// flip_ en sincronia amb (dir_x_, dir_y_).
|
||||
// Canvi de direcció + selecció de l'animació corresponent (un cardinal
|
||||
// per branch). Tots els camins que trien nova direcció (ctor amb
|
||||
// `initial_direction`, gir després d'un bloqueig) passen per ací; el
|
||||
// flip viu al YAML (`walk_west: flip_h: true`) i es propaga des de
|
||||
// l'animació al `flip_` heretat.
|
||||
void changeDirection(float dx, float dy);
|
||||
|
||||
// Aplica la direcció triada (wrapper del switch en una sola crida).
|
||||
@@ -90,9 +120,37 @@ class Mummy : public DynamicHazard {
|
||||
// S'usa al pool de creuament per afegir la recta com a candidata.
|
||||
[[nodiscard]] auto currentDirection() const -> Direction;
|
||||
|
||||
// Animacions carregades del YAML, una per cada facing. Per a horitzontal
|
||||
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta.
|
||||
AnimationComponent anim_walk_down_;
|
||||
AnimationComponent anim_walk_up_;
|
||||
AnimationComponent anim_walk_lateral_;
|
||||
// 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 cardinal. La variant
|
||||
// `walk_west` reaprofita els frames de l'est però amb `flip_h: true`
|
||||
// declarat al YAML — el motor d'animació aplica el flip al sprite
|
||||
// automàticament; aquesta classe no toca mai `flip_` directament.
|
||||
AnimationComponent anim_walk_south_;
|
||||
AnimationComponent anim_walk_north_;
|
||||
AnimationComponent anim_walk_east_;
|
||||
AnimationComponent anim_walk_west_;
|
||||
|
||||
// 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};
|
||||
|
||||
// Si true, el contacte amb el jugador dispara `Player::enterDying`
|
||||
// (mort d'un sol toc, bypass del flux `hurt`/knockback). Llegit del
|
||||
// bloc `stats.kills_outright` del YAML; default true per coherència
|
||||
// amb el comportament d'Oh Mummy! (les mòmies maten).
|
||||
bool kills_outright_{true};
|
||||
};
|
||||
@@ -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::NORTH;
|
||||
case WhipAttack::Direction::SOUTH:
|
||||
return Mummy::Direction::SOUTH;
|
||||
case WhipAttack::Direction::EAST:
|
||||
return Mummy::Direction::EAST;
|
||||
case WhipAttack::Direction::WEST:
|
||||
return Mummy::Direction::WEST;
|
||||
}
|
||||
return Mummy::Direction::NONE;
|
||||
}
|
||||
|
||||
auto whipDirToPlayerCardinal(WhipAttack::Direction d) -> Player::Cardinal {
|
||||
switch (d) {
|
||||
case WhipAttack::Direction::NORTH:
|
||||
return Player::Cardinal::NORTH;
|
||||
case WhipAttack::Direction::SOUTH:
|
||||
return Player::Cardinal::SOUTH;
|
||||
case WhipAttack::Direction::EAST:
|
||||
return Player::Cardinal::EAST;
|
||||
case WhipAttack::Direction::WEST:
|
||||
return Player::Cardinal::WEST;
|
||||
}
|
||||
return Player::Cardinal::NONE;
|
||||
}
|
||||
|
||||
auto playerIsTargetable(const Player* p) -> bool {
|
||||
if (p == nullptr) { return false; }
|
||||
const auto STATE = p->getState();
|
||||
return STATE != Player::State::HURT && STATE != Player::State::EXITED && STATE != Player::State::DISABLED;
|
||||
}
|
||||
|
||||
// Tracta una mòmia candidata: si és whippable i la seua caja solapa
|
||||
// amb `damage_rect`, la posa a HURT i la marca al set. Extret per
|
||||
// acotar la complexitat cognitiva d'`applyDamage` (clang-tidy llindar).
|
||||
void hitMummyIfApplicable(
|
||||
Entity* e,
|
||||
const SDL_FRect& damage_rect,
|
||||
WhipAttack::Direction dir,
|
||||
std::unordered_set<void*>& already_hit) {
|
||||
if (e == nullptr || e->isDead()) { return; }
|
||||
if (already_hit.contains(static_cast<void*>(e))) { return; }
|
||||
auto* mummy = dynamic_cast<Mummy*>(e);
|
||||
if (mummy == nullptr || !mummy->isWhippable()) { return; }
|
||||
const auto BOX = mummy->getCollisionRect();
|
||||
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
|
||||
mummy->enterHurt(whipDirToMummyDir(dir));
|
||||
already_hit.insert(static_cast<void*>(e));
|
||||
}
|
||||
|
||||
void hitPlayerIfApplicable(
|
||||
Player* p,
|
||||
const Player* owner,
|
||||
const SDL_FRect& damage_rect,
|
||||
WhipAttack::Direction dir,
|
||||
std::unordered_set<void*>& already_hit) {
|
||||
if (p == owner || !playerIsTargetable(p)) { return; }
|
||||
if (already_hit.contains(static_cast<void*>(p))) { return; }
|
||||
const auto BOX = p->getBaseBox();
|
||||
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
|
||||
p->enterHurt(whipDirToPlayerCardinal(dir));
|
||||
already_hit.insert(static_cast<void*>(p));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void WhipAttack::applyDamage() {
|
||||
if (entity_manager_ != nullptr) {
|
||||
for (auto* e : entity_manager_->getEntities()) {
|
||||
hitMummyIfApplicable(e, damage_rect_, direction_, already_hit_);
|
||||
}
|
||||
}
|
||||
if (players_ != nullptr) {
|
||||
for (auto* p : *players_) {
|
||||
hitPlayerIfApplicable(p, owner_, damage_rect_, direction_, already_hit_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto WhipAttack::isFrameActive() const -> bool {
|
||||
if (!anim_.has_value()) { return false; }
|
||||
const int FRAME = anim_->currentFrame();
|
||||
return std::ranges::any_of(active_frames_, [FRAME](int f) { return f == FRAME; });
|
||||
}
|
||||
|
||||
REGISTER_ENTITY("whip_attack", WhipAttack)
|
||||
@@ -0,0 +1,162 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/animation_component.hpp"
|
||||
#include "game/entities/attack.hpp"
|
||||
|
||||
class Entity;
|
||||
class EntityManager;
|
||||
class Player;
|
||||
|
||||
// Atac de látigo: spawnejat pel jugador en entrar a `ATTACKING`. Viu la
|
||||
// durada de la seua animació (`swing`) i s'autoelimina al final.
|
||||
//
|
||||
// Sprites: 4 fitxers GIF distints, un per cardinal (sense flips). La
|
||||
// mida del frame canvia entre direccions (E/W: 80×32, N/S: 16×96),
|
||||
// cosa que el sistema d'animació actual no permet expressar amb una
|
||||
// sola surface. Per això el YAML llista les 4 entrades sota `sprites:`
|
||||
// i `configure()` tria la que toca segons direcció.
|
||||
//
|
||||
// Dos rectangles distints, intencionadament diferenciats:
|
||||
// - `pixel_rect_` (heretat d'`Entity`) = `draw_rect`. Cobreix tot el
|
||||
// sprite del látigo: 16 px de wind-up darrere del jugador, 16 px
|
||||
// de solapament amb el jugador, i 48 px d'atac frontal (per E/W;
|
||||
// proporcions equivalents per N/S amb 32 px de solapament). NO es
|
||||
// retalla per bloquejadors — només render visual.
|
||||
// - `damage_rect_` = caixa de dany al nivell dels peus del jugador
|
||||
// (centrada sobre la base 16×16). 48×16 (E/W) o 16×48 (N/S),
|
||||
// pegada al borde del jugador en la direcció d'atac. SÍ es retalla
|
||||
// pel primer bloquejador (mur extern del playfield o sarcòfag).
|
||||
//
|
||||
// Z-order: `getFeetY()` delega al `owner_` per garantir que el látigo
|
||||
// quede al mateix bucket d'y-sort que el jugador que l'ha llançat. Si
|
||||
// no, l'altura física del sprite (esp. en N/S, que té 96 px) crearia
|
||||
// solapaments raros amb mòmies a la trajectòria.
|
||||
class WhipAttack : public Attack {
|
||||
public:
|
||||
// Direcció cardinal del swing. Coincideix valors amb
|
||||
// `Player::Cardinal` (1=N, 2=S, 3=E, 4=W) per facilitar la
|
||||
// conversió a la frontera de l'event `whip_spawn`.
|
||||
enum class Direction : std::uint8_t {
|
||||
NORTH = 1,
|
||||
SOUTH = 2,
|
||||
EAST = 3,
|
||||
WEST = 4,
|
||||
};
|
||||
|
||||
// Definició estàtica d'una direcció: clau al `Resource::Cache` i mida
|
||||
// del frame en píxels. Llegida del YAML una vegada al ctor; usada per
|
||||
// configure() per omplir surface_ i pixel_rect_. Pública perquè el
|
||||
// helper d'anàlisi del YAML viu en un namespace anònim al .cpp.
|
||||
struct DirSprite {
|
||||
std::string surface_key;
|
||||
float frame_w{0.0F};
|
||||
float frame_h{0.0F};
|
||||
};
|
||||
|
||||
explicit WhipAttack(const fkyaml::node& yaml);
|
||||
|
||||
// Configura l'estat runtime de la instància. Cal cridar-lo abans del
|
||||
// primer `update`. A partir del `owner_box` (pixel_rect del jugador,
|
||||
// típicament 16×32) i la direcció d'atac, deriva:
|
||||
// - `pixel_rect_` (= draw_rect): cantonada i mida del sprite del
|
||||
// látigo segons la taula d'offsets (E: -16, 0, 80×32; etc.).
|
||||
// - `damage_rect_`: caixa de dany 48×16 (o 16×48) pegada al
|
||||
// borde del jugador, retallada contra murs i sarcs.
|
||||
// Convenció implícita: el jugador té collision rect 16×16 als
|
||||
// últims 16 px del seu pixel_rect (els "peus"); aquesta funció
|
||||
// n'usa la geometria per al damage_rect i el feet_y delegat.
|
||||
void configure(Direction dir, const SDL_FRect& owner_box, float range_px);
|
||||
|
||||
// Injecta el context que necessita la fase de dany: punter a
|
||||
// l'`EntityManager` (per iterar Hazards) i a la llista de jugadors
|
||||
// (per al friendly fire). Si no es crida, els frames actius no
|
||||
// apliquen dany — el whip queda merament visual. Cridar abans del
|
||||
// primer `update` (típicament just després de `configure`).
|
||||
void setWorldContext(EntityManager* manager, const std::vector<Player*>* players);
|
||||
|
||||
// Avança animació; durant frames actius, aplica dany; quan
|
||||
// l'animació es completa, marca dead.
|
||||
void update(float delta_time) override;
|
||||
|
||||
[[nodiscard]] auto getDirection() const -> Direction { return direction_; }
|
||||
|
||||
// True si el frame actual de l'animació és un dels `active_frames`
|
||||
// declarats al YAML (els frames durant els quals la hitbox aplica
|
||||
// dany). Wind-up i recovery retornen false.
|
||||
[[nodiscard]] auto isFrameActive() const -> bool;
|
||||
|
||||
// Llistat dels frames actius (lectura del YAML, immutable per
|
||||
// instància). Exposat per als smoke tests.
|
||||
[[nodiscard]] auto getActiveFrames() const -> const std::vector<int>& { return active_frames_; }
|
||||
|
||||
// Llargada de la hitbox abans de retallar contra murs/sarcs (en píxels).
|
||||
[[nodiscard]] auto getRangePx() const -> float { return range_px_; }
|
||||
|
||||
// Hitbox efectiva de dany (ja retallada pel primer bloquejador en la
|
||||
// línia d'atac). Exposada per als smoke tests; en producció només la
|
||||
// consumeix la pròpia `applyDamage`.
|
||||
[[nodiscard]] auto getDamageRect() const -> SDL_FRect { return damage_rect_; }
|
||||
|
||||
// Y dels peus delegada al `owner_box` capturat a `configure()`: el
|
||||
// látigo es z-ordena al mateix bucket que el jugador que l'ha
|
||||
// llançat (= base inferior del seu pixel_rect, que coincideix amb
|
||||
// el feet_y del jugador a la convenció estàndard del player AEE).
|
||||
// Snapshot, no consulta dinàmica: el jugador queda quiet durant
|
||||
// ATTACKING, així que snapshot ≡ live per a la vida del whip.
|
||||
[[nodiscard]] auto getFeetY() const -> float override;
|
||||
|
||||
// Clau al `Resource::Cache` per a la surface de la direcció actual
|
||||
// (la que ha triat `configure()`). El caller (típicament `GameScene`)
|
||||
// resol la surface i la passa via `setSurface()`. Aquesta separació
|
||||
// manté el codi de l'entitat sense dependència directa al
|
||||
// `Resource::Cache`, igual que la resta d'entitats del joc.
|
||||
[[nodiscard]] auto getSurfaceKey() const -> const std::string&;
|
||||
|
||||
private:
|
||||
[[nodiscard]] auto spriteFor(Direction dir) const -> const DirSprite&;
|
||||
|
||||
// Calcula la `damage_rect_` final: retalla la longitud de l'atac
|
||||
// contra el primer bloquejador (mur extern del playfield o sarc) en
|
||||
// la línia d'atac. Implementació iterativa píxel a píxel — el rang
|
||||
// típic és ~48 px, així que el cost és menyspreable.
|
||||
void computeDamageRect(Direction dir, float spawn_x, float spawn_y, float range_px);
|
||||
|
||||
// Aplica dany aquest frame: itera Hazards (Mummy whippable) i
|
||||
// jugadors (excepte `owner_`) que solapen amb `damage_rect_` i
|
||||
// estan en estat "atacable", crida `enterHurt` amb la direcció
|
||||
// adequada i els marca a `already_hit_` per garantir un sol
|
||||
// impacte per swing per objectiu.
|
||||
void applyDamage();
|
||||
|
||||
Direction direction_{Direction::EAST};
|
||||
AnimationComponent anim_swing_;
|
||||
std::vector<int> active_frames_;
|
||||
// Set d'objectius ja colpejats aquest swing: usem `void*` perquè
|
||||
// hi guardem tant `Entity*` (Mummy) com `Player*` (que NO hereta
|
||||
// d'Entity) sense `reinterpret_cast` semànticament dubtós.
|
||||
std::unordered_set<void*> already_hit_;
|
||||
float range_px_{0.0F};
|
||||
SDL_FRect damage_rect_{0.0F, 0.0F, 0.0F, 0.0F};
|
||||
|
||||
// Snapshot del feet_y de l'owner pres a `configure()` (= base
|
||||
// inferior del seu pixel_rect, que coincideix amb el `getFeetY()`
|
||||
// del Player en la convenció estàndard d'AEE). El retornem des
|
||||
// del nostre `getFeetY()` per delegar el bucket d'y-sort al jugador.
|
||||
float owner_feet_y_{0.0F};
|
||||
|
||||
EntityManager* entity_manager_{nullptr};
|
||||
const std::vector<Player*>* players_{nullptr};
|
||||
|
||||
DirSprite sprite_north_;
|
||||
DirSprite sprite_south_;
|
||||
DirSprite sprite_east_;
|
||||
DirSprite sprite_west_;
|
||||
};
|
||||
@@ -140,8 +140,10 @@ class Entity {
|
||||
|
||||
// Y dels peus: la vora inferior de la caixa de col·lisió. La usarà el
|
||||
// Z-order per ordenar entitats i jugador de menys a més profunditat
|
||||
// (REND-01, quan s'implemente).
|
||||
[[nodiscard]] auto getFeetY() const -> float;
|
||||
// (REND-01, quan s'implemente). Virtual perquè entitats efímeres
|
||||
// (com `WhipAttack`) poden delegar al seu owner per quedar al
|
||||
// mateix bucket d'y-sort que el jugador que les ha llançat.
|
||||
[[nodiscard]] virtual auto getFeetY() const -> float;
|
||||
|
||||
// El loader li assigna una caixa explícita llegida del YAML; sense
|
||||
// aquesta crida, l'entitat queda sense `CollisionBox` i la caixa de
|
||||
@@ -153,10 +155,11 @@ class Entity {
|
||||
// el sprite) amb el tile. Nullopt si l'entitat no en té.
|
||||
[[nodiscard]] auto getCollisionBox() const -> const std::optional<CollisionBox>& { return collision_box_; }
|
||||
|
||||
// Flip del sprite. Les entitats estàtiques mai no el toquen (queda a
|
||||
// SDL_FLIP_NONE); les dinàmiques que miren cap als costats (p. ex.
|
||||
// Mummy en direcció horitzontal) el canvien quan gira. El manager
|
||||
// el passa a Surface::render.
|
||||
// Flip del sprite, propagat des del motor d'animació
|
||||
// (`AnimationComponent::flipMode()`): cap entitat el decideix per
|
||||
// direcció — el YAML declara `flip_h: true` a l'animació simètrica
|
||||
// (p. ex. `walk_west`) i el setter d'animació l'aplica al `flip_`.
|
||||
// El manager el passa a `Surface::render`.
|
||||
[[nodiscard]] auto getFlip() const -> SDL_FlipMode { return flip_; }
|
||||
void setFlip(SDL_FlipMode flip) { flip_ = flip; }
|
||||
|
||||
|
||||
@@ -65,6 +65,10 @@ void EntityManager::setExpectedPlayerCount(int n) {
|
||||
level_complete_emitted_ = false;
|
||||
}
|
||||
|
||||
void EntityManager::forceOpenDoor() {
|
||||
if (door_ != nullptr) { door_->open(); }
|
||||
}
|
||||
|
||||
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
||||
if (entity == nullptr) { return nullptr; }
|
||||
entity->setEventBus(&bus_);
|
||||
|
||||
@@ -112,6 +112,13 @@ class EntityManager {
|
||||
[[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; }
|
||||
[[nodiscard]] auto exitedPlayerCount() const -> std::size_t { return exited_players_; }
|
||||
|
||||
// Força l'obertura de la porta sense que els sarcòfags estiguin
|
||||
// oberts. Pensat com a hook de debug per a la GameScene: el flux
|
||||
// posterior (creuar la porta → `player_exited` → `level_complete`
|
||||
// → OUTRO) segueix exactament igual perquè depèn de Door, no de
|
||||
// sarcs. No-op si la porta ja està oberta o si el nivell no en té.
|
||||
void forceOpenDoor();
|
||||
|
||||
private:
|
||||
EventBus bus_;
|
||||
std::vector<std::unique_ptr<Entity>> entities_;
|
||||
|
||||
@@ -65,7 +65,10 @@ void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
|
||||
const auto BOX = entity->getCollisionRect();
|
||||
const int E_HEIGHT = entity->getHeight();
|
||||
for (auto* p : players) {
|
||||
if (p == nullptr || p->isExited()) { continue; }
|
||||
// Filtrem també DYING: el jugador en cicle de mort no rep
|
||||
// contacte amb hazards (un segon toc de mòmia mentre la roda
|
||||
// ja és no-op; cap altra entitat l'ha de fer transicionar).
|
||||
if (p == nullptr || p->isOutOfPlay() || p->isDying()) { continue; }
|
||||
// Filtre per altura: si el jugador està en l'aire i està per
|
||||
// damunt de l'alçada del cos de l'entitat, no hi ha contacte.
|
||||
// És el mecanisme del salt per esquivar momies, roques, etc.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
@@ -12,6 +14,7 @@
|
||||
// Forward: els events poden dur un punter a `Entity` (p. ex. quin sarcòfag
|
||||
// s'ha obert). No cal incloure el header complet aquí.
|
||||
class Entity;
|
||||
class Player;
|
||||
|
||||
// Event pub/sub minimalista. Hi ha un únic `EventBus` per mundus (propietat
|
||||
// de l'`EntityManager`); les entitats que el necessiten en reben un punter
|
||||
@@ -49,9 +52,31 @@ struct WorldPos {
|
||||
float y{0.0F};
|
||||
};
|
||||
|
||||
// Payload de `whip_spawn`: el `Player` el publica quan entra a ATTACKING
|
||||
// i el `GameScene` el consumeix per crear un `WhipAttack` via
|
||||
// `EntityManager::add`. Així el Player no depèn ni de `EntityManager` ni de
|
||||
// `WhipAttack`, i el contracte queda dins del bus.
|
||||
// - `owner` : Player que ha llançat l'atac (no rep dany propi).
|
||||
// - `direction` : Player::Cardinal castat a int (1=N, 2=S, 3=E, 4=W) per
|
||||
// evitar dependència cíclica al header.
|
||||
// - `owner_box` : pixel_rect del jugador (típicament 16×32 a AEE). El
|
||||
// WhipAttack en deriva tant la `draw_rect_` (sprite del
|
||||
// látigo, més gran que el jugador per cobrir wind-up i
|
||||
// solapament) com la `damage_rect_` (caixa de dany al
|
||||
// nivell dels peus, retallada per murs/sarcs).
|
||||
// - `range_px` : longitud nominal de la hitbox de dany abans de
|
||||
// retallar contra murs i sarcòfags. Llegida del YAML
|
||||
// del jugador.
|
||||
struct WhipSpawnPayload {
|
||||
Player* owner{nullptr};
|
||||
int direction{0};
|
||||
SDL_FRect owner_box{0.0F, 0.0F, 0.0F, 0.0F};
|
||||
float range_px{0.0F};
|
||||
};
|
||||
|
||||
struct Event {
|
||||
std::string name;
|
||||
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile, WorldPos>;
|
||||
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile, WorldPos, WhipSpawnPayload>;
|
||||
Payload payload;
|
||||
// Jugador que ha originat (o provocat) l'event. -1 = sense jugador
|
||||
// associat (events del món, com `sarcophagus_opened` des de la pròpia
|
||||
@@ -101,6 +126,9 @@ class EventBus {
|
||||
void publish(const std::string& name, WorldPos pos) {
|
||||
publish(Event{.name = name, .payload = pos});
|
||||
}
|
||||
void publish(const std::string& name, const WhipSpawnPayload& whip) {
|
||||
publish(Event{.name = name, .payload = whip});
|
||||
}
|
||||
|
||||
private:
|
||||
struct Subscription {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/input/gamepad_bindings.hpp"
|
||||
#include "core/input/gamepad_manager.hpp"
|
||||
#include "core/input/input_state.hpp"
|
||||
#include "core/rendering/screen.hpp"
|
||||
@@ -16,6 +17,7 @@
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
|
||||
namespace {
|
||||
@@ -68,6 +70,8 @@ Player::TurnConfig Player::turn_config;
|
||||
Player::InertiaConfig Player::inertia_config;
|
||||
Player::JumpConfig Player::jump_config;
|
||||
Player::HurtConfig Player::hurt_config;
|
||||
Player::WhipConfig Player::whip_config;
|
||||
Player::DeathConfig Player::death_config;
|
||||
|
||||
void Player::loadFrom(const std::string& yaml_path) {
|
||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||
@@ -114,6 +118,16 @@ void Player::loadFrom(const std::string& yaml_path) {
|
||||
// Bloc opcional `hurt:` — estat post-impacte. Si manca o el speed
|
||||
// és 0, l'estat HURT mai s'activa (cas degenerat).
|
||||
readHurtBlock(yaml);
|
||||
|
||||
// Bloc opcional `whip:` — paràmetres del látigo. Si manca, el
|
||||
// jugador no atacarà efectivament (ATTACKING dura 0 ms i el flanc
|
||||
// d'input es perd).
|
||||
readWhipBlock(yaml);
|
||||
|
||||
// Bloc opcional `death:` — pausa de respawn i comptador inicial de
|
||||
// vides. Si manca, el respawn és immediat i les vides queden a 3
|
||||
// per defecte (no és un valor crític: avui no hi ha game over).
|
||||
readDeathBlock(yaml);
|
||||
}
|
||||
|
||||
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
|
||||
@@ -173,25 +187,53 @@ void Player::readHurtBlock(const fkyaml::node& yaml) {
|
||||
const auto& h = yaml["hurt"];
|
||||
if (h.contains("speed")) { hurt_config.initial_speed_px_per_s = h["speed"].get_value<float>(); }
|
||||
if (h.contains("brake_time_ms")) { hurt_config.brake_time_ms = h["brake_time_ms"].get_value<int>(); }
|
||||
// So d'entrada a HURT — mateix patró que `jump.sound` i `whip.sound`.
|
||||
if (h.contains("sound")) {
|
||||
hurt_config.sound_key = h["sound"].get_value<std::string>();
|
||||
Resource::Cache::get().loadSound(hurt_config.sound_key, "data/sounds/" + hurt_config.sound_key);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::readDeathBlock(const fkyaml::node& yaml) {
|
||||
death_config = DeathConfig{};
|
||||
if (!yaml.contains("death")) { return; }
|
||||
const auto& d = yaml["death"];
|
||||
if (d.contains("respawn_delay_ms")) { death_config.respawn_delay_ms = d["respawn_delay_ms"].get_value<int>(); }
|
||||
if (d.contains("lives")) { death_config.initial_lives = d["lives"].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>(); }
|
||||
// So del swing: mateix patró que `jump.sound` — basename val de
|
||||
// clau al cache i de path. Si manca, queda buit i no es dispara.
|
||||
if (w.contains("sound")) {
|
||||
whip_config.sound_key = w["sound"].get_value<std::string>();
|
||||
Resource::Cache::get().loadSound(whip_config.sound_key, "data/sounds/" + whip_config.sound_key);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::applyCardinal(Cardinal c) {
|
||||
// El flip del sprite el governa l'animació al motor: només cal posar
|
||||
// el `facing_` que tocaria; `animationName()` ja resol el sufix
|
||||
// (`_west` per Cardinal::WEST) i el YAML hi declara `flip_h: true`.
|
||||
switch (c) {
|
||||
case Cardinal::NORTH:
|
||||
facing_ = Facing::UP;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
facing_ = Facing::NORTH;
|
||||
break;
|
||||
case Cardinal::SOUTH:
|
||||
facing_ = Facing::DOWN;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
facing_ = Facing::SOUTH;
|
||||
break;
|
||||
case Cardinal::EAST:
|
||||
facing_ = Facing::SIDE;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
facing_ = Facing::EAST;
|
||||
break;
|
||||
case Cardinal::WEST:
|
||||
facing_ = Facing::SIDE;
|
||||
setFlip(SDL_FLIP_HORIZONTAL);
|
||||
facing_ = Facing::WEST;
|
||||
break;
|
||||
case Cardinal::NONE:
|
||||
break;
|
||||
@@ -209,10 +251,10 @@ auto Player::cardinalFromInput(float vx, float vy) -> Cardinal {
|
||||
}
|
||||
|
||||
auto Player::cardinalFromString(const std::string& s) -> Cardinal {
|
||||
if (s == "north" || s == "up") { return Cardinal::NORTH; }
|
||||
if (s == "south" || s == "down") { return Cardinal::SOUTH; }
|
||||
if (s == "east" || s == "right") { return Cardinal::EAST; }
|
||||
if (s == "west" || s == "left") { return Cardinal::WEST; }
|
||||
if (s == "north") { return Cardinal::NORTH; }
|
||||
if (s == "south") { return Cardinal::SOUTH; }
|
||||
if (s == "east") { return Cardinal::EAST; }
|
||||
if (s == "west") { return Cardinal::WEST; }
|
||||
throw std::runtime_error("Player: cardinal desconegut '" + s + "' al bloc turn.transitions");
|
||||
}
|
||||
|
||||
@@ -286,17 +328,14 @@ void Player::updateFacingAndTurn(float delta_time, float vx, float vy) {
|
||||
turning_remaining_ms_ = static_cast<float>(turn_config.duration_ms);
|
||||
applyCardinal(intermediate_cardinal_);
|
||||
} else if (vx != 0.0F) {
|
||||
// Tria de facing original: prevalença lateral, flip segons el
|
||||
// signe de vx; verticals purs van a UP/DOWN amb flip NONE; en
|
||||
// aturar-se conservem `facing_` per a l'animació stand.
|
||||
facing_ = Facing::SIDE;
|
||||
setFlip(vx < 0.0F ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
|
||||
// Tria de facing: prevalença lateral, EAST/WEST segons el signe
|
||||
// de vx; verticals purs van a NORTH/SOUTH. En aturar-se
|
||||
// conservem `facing_` per a l'animació stand.
|
||||
facing_ = (vx < 0.0F) ? Facing::WEST : Facing::EAST;
|
||||
} else if (vy < 0.0F) {
|
||||
facing_ = Facing::UP;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
facing_ = Facing::NORTH;
|
||||
} else if (vy > 0.0F) {
|
||||
facing_ = Facing::DOWN;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
facing_ = Facing::SOUTH;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,11 +354,18 @@ Player::Player(const std::string& anim_key, int id, float x, float y, const Cont
|
||||
paint_tile_index_(paint_tile_index) {
|
||||
setPos(x, y);
|
||||
// Postura inicial mirant cap al jugador (cara avall), no de costat:
|
||||
// forcem facing_ a DOWN i l'animació `stand_down` abans del primer
|
||||
// forcem facing_ a SOUTH i l'animació `stand_south` abans del primer
|
||||
// frame d'input — el primer `update` la preservarà si l'usuari no
|
||||
// prem cap tecla.
|
||||
facing_ = Facing::DOWN;
|
||||
setCurrentAnimation("stand_down");
|
||||
facing_ = Facing::SOUTH;
|
||||
setCurrentAnimation("stand_south");
|
||||
// Comptador d'intents inicialitzat amb el valor del YAML (default 3
|
||||
// si el bloc `death:` manca). Fora del cicle DYING no toca a ningú.
|
||||
lives_remaining_ = death_config.initial_lives;
|
||||
// Spawn point inicial: la posició del ctor val per defecte; el
|
||||
// GameScene el reescriu via `setSpawnPoint` quan canvia de sala.
|
||||
spawn_x_ = x;
|
||||
spawn_y_ = y;
|
||||
}
|
||||
|
||||
void Player::update(float delta_time) {
|
||||
@@ -328,10 +374,10 @@ void Player::update(float delta_time) {
|
||||
}
|
||||
|
||||
auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
||||
// EXITED: jugador despawnat. No xoca amb res ni res xoca amb ell.
|
||||
// També salta jugadors EXITED del check actor↔actor més avall via
|
||||
// el filtre de `isExited` (vegeu el predicat any_of).
|
||||
if (state_ == State::EXITED) { return false; }
|
||||
// EXITED/DISABLED: jugador fora del joc. No xoca amb res ni res
|
||||
// xoca amb ell. També es filtren els altres jugadors fora de joc
|
||||
// al check actor↔actor més avall via el predicat `isOutOfPlay`.
|
||||
if (isOutOfPlay()) { return false; }
|
||||
if (map_.collides(box)) { return true; }
|
||||
if (all_players_ == nullptr) { return false; }
|
||||
// Per al contacte actor-actor usem la hit-box petita a banda i banda:
|
||||
@@ -355,9 +401,13 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
||||
const bool MY_AIR = (state_ == State::AIRBORNE);
|
||||
const bool MY_HURT = (state_ == State::HURT);
|
||||
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) {
|
||||
const bool MY_DYING = (state_ == State::DYING);
|
||||
return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT, MY_AIR, MY_HURT, MY_DYING, MY_Z](const Player* p) {
|
||||
if (p == nullptr || p == this) { return false; }
|
||||
if (p->isExited()) { return false; }
|
||||
if (p->isOutOfPlay()) { return false; }
|
||||
// DYING és intangible per a PvP: el jugador mort no actua de
|
||||
// mur, i un altre jugador en DYING no pot empentar tampoc.
|
||||
if (MY_DYING || p->isDying()) { return false; }
|
||||
if (MY_HURT || p->isHurt()) { return false; }
|
||||
if (MY_AIR != p->isAirborne()) { return false; }
|
||||
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
|
||||
@@ -407,10 +457,30 @@ void Player::integrateAxis(float& phys, float input, float delta_time, float mot
|
||||
// llegibilitat. La complexitat puja 1 per la guarda EXITED.
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
||||
void Player::tick(float delta_time) {
|
||||
// EXITED: jugador despawnat, no fa res. Cap input, cap física, cap
|
||||
// bus event. Continua sent al vector de jugadors per simplicitat
|
||||
// (no s'esborra) però queda completament inert fins al respawn.
|
||||
if (state_ == State::EXITED) { return; }
|
||||
// EXITED/DISABLED: jugador fora del joc, no fa res. Cap input, cap
|
||||
// física, cap bus event. Continua sent al vector de jugadors per
|
||||
// simplicitat (no s'esborra) però queda completament inert fins
|
||||
// que GameScene el reactiva (respawn / spawnFromAir).
|
||||
if (isOutOfPlay()) { return; }
|
||||
|
||||
// DYING: animació `death_spinning` + pausa de respawn. Ignorem tot
|
||||
// l'input (ni moviment, ni salt, ni atac). El moviment queda
|
||||
// congelat al lloc de la mort; `applyMovement` avança l'animació i
|
||||
// detecta la fi per disparar el respawn. Mantenim els flancs de
|
||||
// botó actualitzats perquè una tecla sostinguda no es comporte com
|
||||
// a flanc nou en sortir del cicle.
|
||||
if (state_ == State::DYING) {
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -439,6 +509,7 @@ void Player::tick(float delta_time) {
|
||||
// durant el stun: així si l'usuari l'ha estat prement abans, no
|
||||
// se'l consumeix com a salt nou en sortir del stun.
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -448,11 +519,36 @@ void Player::tick(float delta_time) {
|
||||
// pulsació quan torne a terra es detecte com a flanc nou.
|
||||
if (state_ == State::AIRBORNE) {
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
setVelX(intended_vx_);
|
||||
setVelY(intended_vy_);
|
||||
return;
|
||||
}
|
||||
|
||||
// En ATTACKING el jugador queda bloquejat tot el `swing_ms +
|
||||
// cooldown_ms`: ni input direccional, ni salt, ni atac, ni canvi
|
||||
// de facing. El timer descompta amb `delta_time * 1000` per
|
||||
// mantenir la convenció de delta_ms de la resta d'estats temporals.
|
||||
// Mantenim els flancs dels botons actualitzats perquè una
|
||||
// tecla mantinguda no es comporte com a flanc nou en sortir.
|
||||
if (state_ == State::ATTACKING) {
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
attacking_remaining_ms_ -= delta_time * 1000.0F;
|
||||
if (attacking_remaining_ms_ <= 0.0F) {
|
||||
attacking_remaining_ms_ = 0.0F;
|
||||
state_ = State::GROUNDED;
|
||||
}
|
||||
input_vx_ = 0.0F;
|
||||
input_vy_ = 0.0F;
|
||||
intended_vx_ = 0.0F;
|
||||
intended_vy_ = 0.0F;
|
||||
motion_time_ms_ = 0.0F;
|
||||
setVelX(0.0F);
|
||||
setVelY(0.0F);
|
||||
return;
|
||||
}
|
||||
|
||||
// En HURT el jugador està projectat sense control: descomptem
|
||||
// velocitat per inèrcia (decaiment lineal fins a 0). Quan arriba a
|
||||
// zero, tornem a GROUNDED i el frame següent ja torna a llegir input.
|
||||
@@ -460,6 +556,7 @@ void Player::tick(float delta_time) {
|
||||
// estats), que també gestiona el xoc contra murs.
|
||||
if (state_ == State::HURT) {
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
const float DECEL = (hurt_config.brake_time_ms > 0)
|
||||
? hurt_config.initial_speed_px_per_s / (static_cast<float>(hurt_config.brake_time_ms) / 1000.0F)
|
||||
: 0.0F;
|
||||
@@ -483,6 +580,12 @@ void Player::tick(float delta_time) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresca les vinculacions des d'Options en calent: així un remap fet
|
||||
// al menú de servei té efecte immediatament, sense esperar al pròxim
|
||||
// GameScene::onEnter. Replica el comportament d'AEE
|
||||
// (`KeyRemap::update` que llig `Options::keys_game` cada frame).
|
||||
refreshControlsFromOptions();
|
||||
|
||||
// Via `InputState` en lloc de `SDL_GetKeyboardState` directe: quan el
|
||||
// service menu (o qualsevol UI modal) està obert, el wrapper retorna
|
||||
// un buffer tot-a-zero perquè el jugador no es mogui mentre l'usuari
|
||||
@@ -534,6 +637,18 @@ void Player::tick(float delta_time) {
|
||||
// només disparen amb intenció explícita del jugador.
|
||||
updateFacingAndTurn(delta_time, input_vx_, input_vy_);
|
||||
|
||||
// Detecció de flanc del botó d'atac. Es processa ABANS del salt
|
||||
// perquè ATTACKING i AIRBORNE són mútuament excloents i, davant un
|
||||
// mateix-frame, l'atac té prioritat (millor castigar la pulsació
|
||||
// doble per excés que perdre l'atac per un toc accidental al salt).
|
||||
// En entrar a ATTACKING, descartem el flanc del salt per no que
|
||||
// queda penjat per a quan es desbloque.
|
||||
if (consumeAttackEdge() && whip_config.swing_ms > 0) {
|
||||
enterAttacking(currentFacingCardinal());
|
||||
(void)consumeJumpEdge();
|
||||
return;
|
||||
}
|
||||
|
||||
// Detecció de flanc del botó de salt. Es processa DESPRÉS d'aplicar
|
||||
// el facing perquè el sprite del salt usa el facing del moment del
|
||||
// despegue. `being_pushed_` no ho pot bloquejar ací (encara no s'ha
|
||||
@@ -546,6 +661,18 @@ void Player::tick(float delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
void Player::refreshControlsFromOptions() {
|
||||
if (id_ != 0 && id_ != 1) { return; }
|
||||
const auto& kb = (id_ == 0) ? Options::controls().keyboard_p1
|
||||
: Options::controls().keyboard_p2;
|
||||
controls_.up = kb.up;
|
||||
controls_.down = kb.down;
|
||||
controls_.left = kb.left;
|
||||
controls_.right = kb.right;
|
||||
controls_.jump = kb.jump;
|
||||
controls_.attack = kb.attack;
|
||||
}
|
||||
|
||||
auto Player::consumeJumpEdge() -> bool {
|
||||
// Edge-detection independent per teclat i mando: si qualsevol dels
|
||||
// dos passa de no-premut a premut aquest frame, comptem com a salt.
|
||||
@@ -557,13 +684,103 @@ auto Player::consumeJumpEdge() -> bool {
|
||||
jump_held_keyboard_ = KEY_NOW;
|
||||
|
||||
SDL_Gamepad* pad = GamepadManager::get().getForPlayer(id_);
|
||||
const bool PAD_NOW = (pad != nullptr) && InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_SOUTH);
|
||||
const auto& slot = (id_ == 0) ? Options::controls().gamepad_p1
|
||||
: Options::controls().gamepad_p2;
|
||||
const int JUMP_BTN = GamepadBindings::get().getButton(slot.name, slot.path, GamepadBindings::Action::JUMP);
|
||||
const bool PAD_NOW = (pad != nullptr) && !InputState::isBlocked() &&
|
||||
GamepadBindings::isPressed(pad, JUMP_BTN);
|
||||
if (PAD_NOW && !jump_held_gamepad_) { edge = true; }
|
||||
jump_held_gamepad_ = PAD_NOW;
|
||||
|
||||
return edge;
|
||||
}
|
||||
|
||||
auto Player::consumeAttackEdge() -> bool {
|
||||
// Mateix patró que `consumeJumpEdge` però per al botó d'atac. El botó
|
||||
// físic ve d'`GamepadBindings`, persistit per dispositiu — l'usuari
|
||||
// l'ha pogut remapejar des del menú de servei.
|
||||
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 auto& slot = (id_ == 0) ? Options::controls().gamepad_p1
|
||||
: Options::controls().gamepad_p2;
|
||||
const int ATK_BTN = GamepadBindings::get().getButton(slot.name, slot.path, GamepadBindings::Action::ATTACK);
|
||||
const bool PAD_NOW = (pad != nullptr) && !InputState::isBlocked() &&
|
||||
GamepadBindings::isPressed(pad, ATK_BTN);
|
||||
if (PAD_NOW && !attack_held_gamepad_) { edge = true; }
|
||||
attack_held_gamepad_ = PAD_NOW;
|
||||
|
||||
return edge;
|
||||
}
|
||||
|
||||
auto Player::currentFacingCardinal() const -> Cardinal {
|
||||
switch (facing_) {
|
||||
case Facing::NORTH:
|
||||
return Cardinal::NORTH;
|
||||
case Facing::SOUTH:
|
||||
return Cardinal::SOUTH;
|
||||
case Facing::EAST:
|
||||
return Cardinal::EAST;
|
||||
case Facing::WEST:
|
||||
return Cardinal::WEST;
|
||||
}
|
||||
return 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; }
|
||||
|
||||
// So del swing — disparat ací (no des del WhipAttack) perquè és el
|
||||
// jugador qui executa l'acció; si demà el spawn falla per qualsevol
|
||||
// motiu, el so del gest segueix sonant.
|
||||
if (!whip_config.sound_key.empty() && Audio::get() != nullptr) {
|
||||
Audio::get()->playSound(whip_config.sound_key, Audio::Group::GAME);
|
||||
}
|
||||
|
||||
// Mapa de Cardinal → int al payload (vegeu `WhipSpawnPayload`).
|
||||
int dir_int = 0;
|
||||
switch (facing) {
|
||||
case Cardinal::NORTH:
|
||||
dir_int = 1;
|
||||
break;
|
||||
case Cardinal::SOUTH:
|
||||
dir_int = 2;
|
||||
break;
|
||||
case Cardinal::EAST:
|
||||
dir_int = 3;
|
||||
break;
|
||||
case Cardinal::WEST:
|
||||
dir_int = 4;
|
||||
break;
|
||||
case Cardinal::NONE:
|
||||
return;
|
||||
}
|
||||
|
||||
// Passem el pixel_rect (16×32 a sam/morcus) al payload; el WhipAttack
|
||||
// en deriva tant la draw_rect (sprite del látigo, més gran) com la
|
||||
// damage_rect (al nivell dels peus, retallada per bloquejadors).
|
||||
if (bus_ != nullptr) {
|
||||
const float RANGE_PX = whip_config.range_tiles * static_cast<float>(Map::tile_size);
|
||||
bus_->publish("whip_spawn", WhipSpawnPayload{
|
||||
.owner = this,
|
||||
.direction = dir_int,
|
||||
.owner_box = getPosition(),
|
||||
.range_px = RANGE_PX,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Player::startJump() {
|
||||
// No saltem si l'estat no ho permet. `being_pushed_` no és segur de
|
||||
// consultar ací (es pot haver marcat o no, segons l'ordre del frame),
|
||||
@@ -576,7 +793,7 @@ void Player::startJump() {
|
||||
// Un gir de 180° en curs es compromet immediatament: si l'usuari
|
||||
// ha disparat un giro (oest→est, p.ex.) i salta abans que s'esgote
|
||||
// el timer, l'animació del salt ha de mostrar la direcció final
|
||||
// (jump lateral), no la intermèdia (jump_down). Aplem el pending
|
||||
// (jump lateral), no la intermèdia (jump_south). Aplem el pending
|
||||
// ara mateix i resetem l'estat del gir. Si no hi havia gir en
|
||||
// curs, és no-op.
|
||||
if (turning_remaining_ms_ > 0.0F) {
|
||||
@@ -671,6 +888,9 @@ void Player::enterHurt(Cardinal preferred) {
|
||||
|
||||
// Activem l'estat i fixem la velocitat segons la direcció triada.
|
||||
state_ = State::HURT;
|
||||
if (!hurt_config.sound_key.empty() && Audio::get() != nullptr) {
|
||||
Audio::get()->playSound(hurt_config.sound_key, Audio::Group::GAME);
|
||||
}
|
||||
applyCardinal(CHOSEN);
|
||||
last_input_cardinal_ = CHOSEN;
|
||||
turning_remaining_ms_ = 0.0F;
|
||||
@@ -696,7 +916,15 @@ auto Player::integrateJump(float delta_time) -> bool {
|
||||
state_ = State::LANDING_STUN;
|
||||
landing_stun_remaining_ms_ = static_cast<float>(jump_config.landing_stun_ms);
|
||||
if (!jump_config.land_sound_key.empty() && Audio::get() != nullptr) {
|
||||
Audio::get()->playSound(jump_config.land_sound_key, Audio::Group::GAME);
|
||||
// Consumeix el preset armat per GameScene al spawn aeri
|
||||
// d'INTRO (single-shot): el primer LAND el sona reverberat,
|
||||
// els salts següents cauen al camí sec.
|
||||
if (next_land_reverb_preset_.empty()) {
|
||||
Audio::get()->playSound(jump_config.land_sound_key, Audio::Group::GAME);
|
||||
} else {
|
||||
Audio::get()->playSoundWithReverb(jump_config.land_sound_key, next_land_reverb_preset_, Audio::Group::GAME);
|
||||
next_land_reverb_preset_.clear();
|
||||
}
|
||||
}
|
||||
if (bus_ != nullptr) {
|
||||
bus_->publish(Event{
|
||||
@@ -719,6 +947,11 @@ void Player::resolveLandingOverlap() {
|
||||
if (all_players_ == nullptr) { return; }
|
||||
for (Player* other : *all_players_) {
|
||||
if (other == nullptr || other == this) { continue; }
|
||||
// Salta jugadors fora del joc: un caigut sobre un slot DISABLED
|
||||
// (o sobre un EXITED encara al vector) no els ha de projectar.
|
||||
// També DYING — un jugador en cicle de mort no rep cap més
|
||||
// efecte fins que respawneja.
|
||||
if (other->isOutOfPlay() || other->isDying()) { continue; }
|
||||
// Test amb la hit-box (8×8 de contacte precís), no la base-box.
|
||||
// Coherent amb la resta de col·lisions actor-actor del joc:
|
||||
// momies, fuetades futures, resolver PvP… tots usen hit-box. Un
|
||||
@@ -753,8 +986,8 @@ void Player::resolveLandingOverlap() {
|
||||
}
|
||||
|
||||
void Player::applyMovement(float delta_time) {
|
||||
// EXITED: cap moviment ni col·lisió. Idèntic al gating de `tick`.
|
||||
if (state_ == State::EXITED) { return; }
|
||||
// EXITED/DISABLED: cap moviment ni col·lisió. Idèntic al gating de `tick`.
|
||||
if (isOutOfPlay()) { return; }
|
||||
|
||||
// Integració del salt: si estem AIRBORNE, avança jump_z_/jump_vz_.
|
||||
// El facing i intended_v* es mantenen del moment del despegue (no es
|
||||
@@ -766,6 +999,11 @@ void Player::applyMovement(float delta_time) {
|
||||
resolveLandingOverlap();
|
||||
}
|
||||
|
||||
if (state_ == State::DYING) {
|
||||
applyDyingMovement(delta_time);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state_ == State::HURT) {
|
||||
applyHurtMovement(delta_time);
|
||||
return;
|
||||
@@ -776,6 +1014,15 @@ void Player::applyMovement(float delta_time) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state_ == State::ATTACKING) {
|
||||
// Cap moviment durant ATTACKING. Animem (perquè avance la
|
||||
// seqüència `attack_*`) i actualitzem la clau d'animació segons
|
||||
// la sub-fase (swing vs cooldown).
|
||||
animate(delta_time);
|
||||
setCurrentAnimation(animationName(false));
|
||||
return;
|
||||
}
|
||||
|
||||
// El resolver del GameScene pot haver escalat `intended_vx_/vy_`
|
||||
// (cas push: velocitat * push_factor) o haver-les anul·lat (cas
|
||||
// head-on). Consumim-les aquí com a velocitat definitiva del frame.
|
||||
@@ -828,6 +1075,75 @@ void Player::applyMovement(float delta_time) {
|
||||
if (state_ == State::GROUNDED) { publishTileEnteredIfChanged(); }
|
||||
}
|
||||
|
||||
void Player::enterDying() {
|
||||
// Idempotent. Un segon contacte de momia mentre el jugador ja està
|
||||
// en DYING no reinicia l'animació ni el comptador. Tampoc disparem
|
||||
// el cicle si el jugador està fora de joc (EXITED al passar la
|
||||
// porta, DISABLED durant les transicions de sala).
|
||||
if (state_ == State::DYING || isOutOfPlay()) { return; }
|
||||
|
||||
state_ = State::DYING;
|
||||
intended_vx_ = 0.0F;
|
||||
intended_vy_ = 0.0F;
|
||||
input_vx_ = 0.0F;
|
||||
input_vy_ = 0.0F;
|
||||
motion_time_ms_ = 0.0F;
|
||||
being_pushed_ = false;
|
||||
jump_z_ = 0.0F;
|
||||
jump_vz_ = 0.0F;
|
||||
landing_stun_remaining_ms_ = 0.0F;
|
||||
attacking_remaining_ms_ = 0.0F;
|
||||
turning_remaining_ms_ = 0.0F;
|
||||
pending_cardinal_ = Cardinal::NONE;
|
||||
intermediate_cardinal_ = Cardinal::NONE;
|
||||
last_input_cardinal_ = Cardinal::NONE;
|
||||
setVelX(0.0F);
|
||||
setVelY(0.0F);
|
||||
|
||||
death_animation_complete_ = false;
|
||||
respawn_remaining_ms_ = 0.0F;
|
||||
setCurrentAnimation("death_spinning");
|
||||
|
||||
// So del cop letal: reaprofitem `hurt.wav` (ja precarregat per
|
||||
// `readHurtBlock`). El bloc `death:` no té camp de so propi —
|
||||
// semànticament l'impacte mortal és la versió forta de `hurt`.
|
||||
if (!hurt_config.sound_key.empty() && Audio::get() != nullptr) {
|
||||
Audio::get()->playSound(hurt_config.sound_key, Audio::Group::GAME);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::applyDyingMovement(float delta_time) {
|
||||
// Avancem l'animació; el sprite no es mou (la posició queda
|
||||
// congelada al punt de la mort). `animationIsCompleted()` flanqueja
|
||||
// un cop la roda arriba al darrer frame (loop:false). Llavors
|
||||
// descomptem `respawn_delay_ms` mantenint el sprite al darrer frame
|
||||
// i, en arribar a 0, disparem el respawn.
|
||||
animate(delta_time);
|
||||
if (!death_animation_complete_ && animationIsCompleted()) {
|
||||
death_animation_complete_ = true;
|
||||
respawn_remaining_ms_ = static_cast<float>(death_config.respawn_delay_ms);
|
||||
}
|
||||
if (death_animation_complete_) {
|
||||
respawn_remaining_ms_ -= delta_time * 1000.0F;
|
||||
if (respawn_remaining_ms_ <= 0.0F) {
|
||||
respawnFromDying();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Player::respawnFromDying() {
|
||||
// Vides: decreix sempre. Pot anar a negatius — no hi ha game over.
|
||||
--lives_remaining_;
|
||||
death_animation_complete_ = false;
|
||||
respawn_remaining_ms_ = 0.0F;
|
||||
// Cinemàtica idèntica a la d'INTRO de sala: el jugador cau des de
|
||||
// dalt fins al passadís d'spawn (`spawnFromAir` reseteja primer i
|
||||
// després posa AIRBORNE amb l'altura derivada de la geometria).
|
||||
// Així el respawn post-mort comparteix tota la sensació visual i
|
||||
// física del primer spawn — incloent el LANDING_STUN final.
|
||||
spawnFromAir(spawn_x_, spawn_y_);
|
||||
}
|
||||
|
||||
void Player::applyHurtMovement(float delta_time) {
|
||||
// En HURT el jugador es desplaça segons `intended_v*` (escrit a
|
||||
// `enterHurt` i decremental a `tick`). Si `tryMove` no avança
|
||||
@@ -875,9 +1191,17 @@ void Player::publishTileEnteredIfChanged() {
|
||||
}
|
||||
|
||||
void Player::render() {
|
||||
// EXITED: invisible. La crida al render base es salta sencera; el
|
||||
// jugador no apareix al z-sort tot i seguir vivint al vector.
|
||||
if (state_ == State::EXITED) { return; }
|
||||
// EXITED/DISABLED: invisible. La crida al render base es salta
|
||||
// sencera; el jugador no apareix al z-sort tot i seguir vivint al
|
||||
// vector.
|
||||
if (isOutOfPlay()) { return; }
|
||||
|
||||
// Limbo post-mort: durant `respawn_delay_ms` entre la fi de
|
||||
// `death_spinning` i el `spawnFromAir`, el jugador queda invisible.
|
||||
// Així el cicle complet és: animació rodant → desapareix → ressuscita
|
||||
// caient des de dalt. Sense aquest gat, el sprite quedaria pintat al
|
||||
// darrer frame de la roda mentre passa el delay.
|
||||
if (state_ == State::DYING && death_animation_complete_) { return; }
|
||||
|
||||
// Mentre el jugador està enlaire, el sprite es desplaça visualment
|
||||
// cap amunt `jump_z_` píxels. Modifiquem `pos_.y` (la posició entera
|
||||
@@ -933,60 +1257,56 @@ void Player::renderShadow() const {
|
||||
Screen::get().blendRect(SX, SY, SHADOW_W, SHADOW_H, SHADOW_ARGB, SHADOW_ALPHA);
|
||||
}
|
||||
|
||||
auto Player::animationName(bool moving) const -> const char* {
|
||||
// En l'aire forcem la variant `jump_*` (per direcció), independent de
|
||||
// si hi ha velocitat horitzontal: per al jugador, l'estat visual del
|
||||
// salt és per direcció, no per velocitat.
|
||||
if (state_ == State::AIRBORNE) {
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return "jump_up";
|
||||
case Facing::DOWN:
|
||||
return "jump_down";
|
||||
case Facing::SIDE:
|
||||
break;
|
||||
}
|
||||
return "jump";
|
||||
// Sufix d'animació per cardinal — empat 1:1 amb l'enum `Facing` i el
|
||||
// sufix declarat als YAMLs de jugador (sam.yaml, morcus.yaml).
|
||||
auto Player::facingSuffix(Facing facing) -> const char* {
|
||||
switch (facing) {
|
||||
case Facing::NORTH:
|
||||
return "_north";
|
||||
case Facing::SOUTH:
|
||||
return "_south";
|
||||
case Facing::EAST:
|
||||
return "_east";
|
||||
case Facing::WEST:
|
||||
return "_west";
|
||||
}
|
||||
// En LANDING_STUN, `stun_*` (per direcció): mostrem el frame
|
||||
// d'aterratge fins que s'esgota el timer. La direcció ve fixada del
|
||||
// moment del despegue (no es canvia mentre estem en l'aire).
|
||||
if (state_ == State::LANDING_STUN) {
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return "stun_up";
|
||||
case Facing::DOWN:
|
||||
return "stun_down";
|
||||
case Facing::SIDE:
|
||||
break;
|
||||
}
|
||||
return "stun";
|
||||
}
|
||||
// En HURT, `hurt_*` (per direcció): el jugador llisca sense
|
||||
// control en la direcció triada per `enterHurt`. Mateixa estructura
|
||||
// que walk per facilitat (ara mateix els frames són clons).
|
||||
if (state_ == State::HURT) {
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return "hurt_up";
|
||||
case Facing::DOWN:
|
||||
return "hurt_down";
|
||||
case Facing::SIDE:
|
||||
break;
|
||||
}
|
||||
return "hurt";
|
||||
}
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return moving ? "walk_up" : "stand_up";
|
||||
case Facing::DOWN:
|
||||
return moving ? "walk_down" : "stand_down";
|
||||
case Facing::SIDE:
|
||||
return "_east";
|
||||
}
|
||||
|
||||
// Prefix d'animació per estat. ATTACKING té dues sub-fases: durant
|
||||
// `swing_ms` és `attack_*` (animació del braç llançant el látigo); durant
|
||||
// `cooldown_ms` posterior, `recover_*`. A AIRBORNE/LANDING_STUN/HURT, la
|
||||
// variant és per direcció, no per velocitat: l'estat bloqueja el `moving`
|
||||
// argument i no fa servir walk/stand.
|
||||
auto Player::statePrefix(State state, bool moving, bool in_attack_swing) -> const char* {
|
||||
switch (state) {
|
||||
case State::AIRBORNE:
|
||||
return "jump";
|
||||
case State::LANDING_STUN:
|
||||
return "stun";
|
||||
case State::HURT:
|
||||
return "hurt";
|
||||
case State::ATTACKING:
|
||||
return in_attack_swing ? "attack" : "recover";
|
||||
case State::DYING:
|
||||
// L'animació concreta (`death_spinning`) la fixa `enterDying`;
|
||||
// `applyDyingMovement` no torna a cridar `setCurrentAnimation`,
|
||||
// així que aquest valor no s'usa mai. El retornem només per
|
||||
// exhaustivitat del switch.
|
||||
return "death";
|
||||
case State::GROUNDED:
|
||||
case State::EXITED:
|
||||
case State::DISABLED:
|
||||
break;
|
||||
}
|
||||
return moving ? "walk" : "stand";
|
||||
}
|
||||
|
||||
auto Player::animationName(bool moving) const -> std::string {
|
||||
const bool IN_ATTACK_SWING = (attacking_remaining_ms_ > static_cast<float>(whip_config.cooldown_ms));
|
||||
return std::string(statePrefix(state_, moving, IN_ATTACK_SWING)) + facingSuffix(facing_);
|
||||
}
|
||||
|
||||
auto Player::getCurrentTile() const -> std::pair<int, int> {
|
||||
// El grid del tilemap és de `tile_size` píxels i està 16-alineat
|
||||
// respecte a l'origen del tilemap (pot ser negatiu per overscan). El
|
||||
@@ -1040,20 +1360,73 @@ void Player::respawn(float x, float y) {
|
||||
jump_z_ = 0.0F;
|
||||
jump_vz_ = 0.0F;
|
||||
landing_stun_remaining_ms_ = 0.0F;
|
||||
attacking_remaining_ms_ = 0.0F;
|
||||
turning_remaining_ms_ = 0.0F;
|
||||
pending_cardinal_ = Cardinal::NONE;
|
||||
intermediate_cardinal_ = Cardinal::NONE;
|
||||
last_input_cardinal_ = Cardinal::NONE;
|
||||
// Flags del cicle DYING (que `respawnFromDying` també neteja, però
|
||||
// si algú crida `respawn` directament des d'un altre estat, han de
|
||||
// quedar coherents per al pròxim cicle de mort).
|
||||
death_animation_complete_ = false;
|
||||
respawn_remaining_ms_ = 0.0F;
|
||||
// Reseteig del facing al d'spawn original (mirant cap a la càmera):
|
||||
// sense això, un EXITED que entrava per la porta amb facing UP
|
||||
// sense això, un EXITED que entrava per la porta amb facing NORTH
|
||||
// re-aparix d'esquena al pròxim cicle. Mateixa lògica que el ctor.
|
||||
facing_ = Facing::DOWN;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
setCurrentAnimation("stand_down");
|
||||
facing_ = Facing::SOUTH;
|
||||
setCurrentAnimation("stand_south");
|
||||
setPosX(x);
|
||||
setPosY(y);
|
||||
}
|
||||
|
||||
void Player::disable() {
|
||||
if (state_ == State::DISABLED) { return; }
|
||||
state_ = State::DISABLED;
|
||||
intended_vx_ = 0.0F;
|
||||
intended_vy_ = 0.0F;
|
||||
input_vx_ = 0.0F;
|
||||
input_vy_ = 0.0F;
|
||||
being_pushed_ = false;
|
||||
jump_z_ = 0.0F;
|
||||
jump_vz_ = 0.0F;
|
||||
landing_stun_remaining_ms_ = 0.0F;
|
||||
turning_remaining_ms_ = 0.0F;
|
||||
pending_cardinal_ = Cardinal::NONE;
|
||||
intermediate_cardinal_ = Cardinal::NONE;
|
||||
last_input_cardinal_ = Cardinal::NONE;
|
||||
}
|
||||
|
||||
void Player::spawnFromAir(float x, float y) {
|
||||
// Net total: equival a un `respawn` seguit d'una entrada en l'aire.
|
||||
// Així el flag de gir, l'animació, els flancs del salt i les
|
||||
// velocitats residuals queden a un estat conegut abans de promocionar
|
||||
// a AIRBORNE. Si hi havia un cardinal d'input penjat de la sessió
|
||||
// anterior, no contamina la pròxima caiguda.
|
||||
respawn(x, y);
|
||||
|
||||
// Altura derivada de la geometria: prou per arrencar amb el sprite
|
||||
// sencer per damunt del bord superior del canvas (recordem que
|
||||
// visualment es pinta a `pos_.y - jump_z_`). Afegim un xicotet marge
|
||||
// perquè el primer frame ja siga clarament off-screen, no just al ras.
|
||||
constexpr float OFFSCREEN_MARGIN_PX = 8.0F;
|
||||
const float SPAWN_HEIGHT = y + static_cast<float>(sprite_h) + OFFSCREEN_MARGIN_PX;
|
||||
|
||||
state_ = State::AIRBORNE;
|
||||
jump_z_ = SPAWN_HEIGHT;
|
||||
// Caiguda recta: sense impuls inicial, deixem que la gravetat (la
|
||||
// mateixa que la del salt normal) faça la feina. Velocitat horitzontal
|
||||
// a zero perquè no es desviï del passadís d'spawn.
|
||||
jump_vz_ = 0.0F;
|
||||
intended_vx_ = 0.0F;
|
||||
intended_vy_ = 0.0F;
|
||||
|
||||
// Mirant cap avall durant la caiguda — `animationName(AIRBORNE)` amb
|
||||
// facing SOUTH ja resol a `jump_south`. `respawn` ja ha posat facing a
|
||||
// SOUTH; ho deixem explícit ací per claredat de la cinemàtica.
|
||||
facing_ = Facing::SOUTH;
|
||||
setCurrentAnimation("jump_south");
|
||||
}
|
||||
|
||||
auto Player::getBaseBox() const -> SDL_FRect {
|
||||
return {
|
||||
.x = getPosX() + static_cast<float>(collision_x_offset),
|
||||
@@ -1144,6 +1517,7 @@ auto Player::tryPushSnapBlockers(
|
||||
const SDL_FRect MY_HIT = getHitBox();
|
||||
for (Player* p : *all_players_) {
|
||||
if (p == nullptr || p == this) { continue; }
|
||||
if (p->isOutOfPlay()) { continue; }
|
||||
const SDL_FRect OTHER_HIT = p->getHitBox();
|
||||
if (SDL_HasRectIntersectionFloat(&MY_HIT, &OTHER_HIT)) {
|
||||
blockers.push_back(p);
|
||||
|
||||
@@ -24,6 +24,7 @@ class Player : public AnimatedSprite {
|
||||
SDL_Scancode left;
|
||||
SDL_Scancode right;
|
||||
SDL_Scancode jump;
|
||||
SDL_Scancode attack;
|
||||
};
|
||||
|
||||
// Estat d'alt nivell del jugador. Ortogonal a `Facing`/`Cardinal`
|
||||
@@ -44,12 +45,36 @@ class Player : public AnimatedSprite {
|
||||
AIRBORNE,
|
||||
LANDING_STUN,
|
||||
HURT,
|
||||
// Atacant amb látigo. Estat mútuament exclusiu amb AIRBORNE: no
|
||||
// es pot atacar saltant, ni saltar atacant. Cobreix swing +
|
||||
// cooldown internament: durant `whip_swing_ms` es spawneja
|
||||
// l'entitat WhipAttack i corre l'animació `attack_*`; durant
|
||||
// `whip_cooldown_ms` posterior, el jugador segueix bloquejat
|
||||
// (no input, no salt, no atac) però visualment torna a
|
||||
// `stand_*`. En total, ATTACKING dura `swing_ms + cooldown_ms`;
|
||||
// en acabar, torna a GROUNDED.
|
||||
ATTACKING,
|
||||
// Jugador que ha eixit per la porta de la sala. Despawn lògic:
|
||||
// no es renderitza, no processa input, no es mou, no col·lisiona
|
||||
// amb res ni rep contacte d'entitats. Idempotent: les crides
|
||||
// posteriors a `exit()` són no-op. El reset el porta `respawn()`
|
||||
// (cridada per GameScene en transició OUTRO → INTRO).
|
||||
EXITED,
|
||||
// Slot de jugador inactiu: instanciat però fora de partida. Mateix
|
||||
// perfil físic que EXITED (invisible, sense input, sense col·lisió,
|
||||
// ignorat pel resolver PvP), però amb semàntica diferent: no
|
||||
// compta com a "ha completat la sala" ni dispara `level_complete`.
|
||||
// Usat per (a) el slot del segon jugador en single-player, (b)
|
||||
// tots els slots durant les transicions de sala abans del spawn
|
||||
// aeri, i (c) en el futur, l'estat post-mort abans del respawn.
|
||||
DISABLED,
|
||||
// Mort en curs: animació `death_spinning` (3 voltes horàries) +
|
||||
// pausa `respawn_delay_ms` abans del respawn. Bloqueja input i
|
||||
// moviment; no rep contacte amb hazards (cf. `isDying`). Quan el
|
||||
// timer s'esgota, `respawn(spawn_x_, spawn_y_)` torna a GROUNDED
|
||||
// i decrementa el comptador de vides (pot anar a negatius — no
|
||||
// hi ha game over).
|
||||
DYING,
|
||||
};
|
||||
|
||||
// Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`).
|
||||
@@ -184,6 +209,16 @@ class Player : public AnimatedSprite {
|
||||
[[nodiscard]] auto isAirborne() const -> bool { return state_ == State::AIRBORNE; }
|
||||
[[nodiscard]] auto isHurt() const -> bool { return state_ == State::HURT; }
|
||||
[[nodiscard]] auto isExited() const -> bool { return state_ == State::EXITED; }
|
||||
[[nodiscard]] auto isDisabled() const -> bool { return state_ == State::DISABLED; }
|
||||
[[nodiscard]] auto isDying() const -> bool { return state_ == State::DYING; }
|
||||
// Resum dels estats "fora del món": EXITED i DISABLED comparteixen el
|
||||
// mateix perfil físic (no es pinten, no llegeixen input, no
|
||||
// col·lisionen, no participen al resolver PvP). Tots dos gates de
|
||||
// l'update i el render comproven aquesta combinació amb una sola
|
||||
// crida per evitar oblits si demà afegim un tercer estat-fora-del-món.
|
||||
[[nodiscard]] auto isOutOfPlay() const -> bool {
|
||||
return state_ == State::EXITED || state_ == State::DISABLED;
|
||||
}
|
||||
// Tangible per a contactes jugador↔jugador: només els estats a terra
|
||||
// amb cos sòlid. AIRBORNE i HURT són intangibles (es travessen).
|
||||
// EXITED tampoc — el jugador ha eixit lògicament i ja no és al món.
|
||||
@@ -213,12 +248,66 @@ class Player : public AnimatedSprite {
|
||||
being_pushed_ = false;
|
||||
}
|
||||
|
||||
// Mata el jugador d'un sol toc: transició a DYING, animació
|
||||
// `death_spinning`, posició congelada, input ignorat. Idempotent —
|
||||
// si ja està DYING, EXITED o DISABLED, és no-op (un segon toc de
|
||||
// momia mentre la roda no la reinicia). Quan l'animació acaba i
|
||||
// `respawn_delay_ms` expira, `respawn(spawn_x_, spawn_y_)` torna a
|
||||
// GROUNDED i `lives_remaining_` decreix en 1 (pot anar a negatius).
|
||||
void enterDying();
|
||||
|
||||
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
||||
// sprite, mateixa convenció que `respawn(x, y)`). El GameScene les
|
||||
// assigna després de `computeSpawn` al ctor i a cada `loadRoom`. Sense
|
||||
// això, el respawn enviaria el jugador a (0, 0).
|
||||
void setSpawnPoint(float x, float y) {
|
||||
spawn_x_ = x;
|
||||
spawn_y_ = y;
|
||||
}
|
||||
|
||||
// Comptador d'intents (estil Mario, NO HP). Decreix a cada respawn;
|
||||
// no es clampa a 0 — permet anar a negatius perquè avui no hi ha
|
||||
// game over. Públic per a HUD i tests.
|
||||
[[nodiscard]] auto getLivesRemaining() const -> int { return lives_remaining_; }
|
||||
|
||||
// Reset complet a l'estat inicial: posició a (x, y), velocitats a 0,
|
||||
// estat GROUNDED, qualsevol flag de control reinicialitzat. Cridat
|
||||
// per GameScene a la transició ROOM_OUTRO → ROOM_INTRO. Idempotent
|
||||
// (es pot cridar des de qualsevol estat).
|
||||
// (es pot cridar des de qualsevol estat). Es manté disponible per a
|
||||
// qualsevol cas futur on calgui aparèixer el jugador ja a terra
|
||||
// sense la cinemàtica de caiguda; el flux estàndard d'arrencada de
|
||||
// sala usa `disable()` + `spawnFromAir()`.
|
||||
void respawn(float x, float y);
|
||||
|
||||
// Posa el jugador en el slot inactiu (DISABLED). Idempotent: la
|
||||
// segona crida és no-op. Mateix patró que `exit()` per simetria.
|
||||
// No publica res al bus — el GameScene decideix quan disparar el
|
||||
// `spawnFromAir` posterior, i la transició DISABLED↔jugant és
|
||||
// ortogonal a `level_complete`. Útil per:
|
||||
// - el slot del segon jugador en single-player,
|
||||
// - el període entre setState(ROOM_INTRO) i el trigger del spawn,
|
||||
// - el futur estat post-mort (abans del respawn temporitzat).
|
||||
void disable();
|
||||
|
||||
// Cinemàtica d'aparició: posiciona el jugador al seu tile final
|
||||
// (x, y al sistema de coordenades del sprite) i el deixa en
|
||||
// AIRBORNE a una altura derivada de la geometria del playfield —
|
||||
// prou per arrencar fora del marc i caure recte fins al sòl. La
|
||||
// velocitat horitzontal queda a zero (caiguda perfectament
|
||||
// vertical). En tocar terra, el flux normal d'AIRBORNE→
|
||||
// LANDING_STUN→GROUNDED el reactiva amb input. Reutilitzable per
|
||||
// al respawn post-mort: mateixa entrada cinemàtica.
|
||||
void spawnFromAir(float x, float y);
|
||||
|
||||
// Arma un preset de reverb (catàleg a `data/sounds.yaml`) per al
|
||||
// pròxim aterratge d'aquest jugador. Pensat per a la cinemàtica
|
||||
// d'INTRO de sala: GameScene crida aquesta funció just després de
|
||||
// `spawnFromAir`, i el primer LAND consumeix el preset (queda buit
|
||||
// immediatament). Els salts posteriors del joc, els respawns
|
||||
// post-mort i el reset general no rearmen res. `preset` buit
|
||||
// desactiva el reverb del proper LAND (no afecta cap LAND posterior).
|
||||
void armNextLandReverb(std::string preset) { next_land_reverb_preset_ = std::move(preset); }
|
||||
|
||||
// Alçada actual del salt (píxels visuals per damunt del sòl).
|
||||
// 0 quan està a terra. L'usa el render i el sistema de col·lisions
|
||||
// per altura.
|
||||
@@ -292,9 +381,12 @@ class Player : public AnimatedSprite {
|
||||
void snapToHorizontalLane(float delta_time);
|
||||
void snapToVerticalLane(float delta_time);
|
||||
|
||||
// Nom d'animació per al `facing_` actual. Si `moving` és cert s'usa
|
||||
// la variant `walk*`, altrament la variant `stand*`.
|
||||
[[nodiscard]] auto animationName(bool moving) const -> const char*;
|
||||
// Nom d'animació per al `facing_` actual. Composat com a `prefix +
|
||||
// suffix` via els dos helpers estàtics (declarats junt amb `Facing`
|
||||
// més avall) — el prefix depèn de l'estat (walk/stand/jump/stun/hurt/
|
||||
// attack/recover), el sufix del cardinal (`_north`/`_south`/`_east`/
|
||||
// `_west`).
|
||||
[[nodiscard]] auto animationName(bool moving) const -> std::string;
|
||||
void snapToLane(
|
||||
float delta_time,
|
||||
float base,
|
||||
@@ -315,6 +407,12 @@ class Player : public AnimatedSprite {
|
||||
float (Player::*get_pos)() const,
|
||||
void (Player::*set_pos)(float)) -> bool;
|
||||
|
||||
// Refresca `controls_` des d'`Options::controls()` cada frame. Replica
|
||||
// el patró d'AEE on `KeyRemap::update` reflecteix Options::keys_game al
|
||||
// buffer del teclat cada tick: així un remap fet al menú de servei té
|
||||
// efecte immediat sense haver de tornar a entrar a la GameScene.
|
||||
void refreshControlsFromOptions();
|
||||
|
||||
int id_{-1};
|
||||
Controls controls_;
|
||||
|
||||
@@ -350,6 +448,14 @@ class Player : public AnimatedSprite {
|
||||
// gestiona `intended_vx_/vy_` escalats pel resolver.
|
||||
bool being_pushed_{false};
|
||||
|
||||
// Preset de reverb (catàleg de `data/sounds.yaml`) que el pròxim
|
||||
// LAND ha de fer servir; buit = so sec. L'arma `armNextLandReverb`
|
||||
// (cridada per GameScene al spawn aeri d'INTRO) i el consumeix
|
||||
// `integrateJump` quan el jugador toca terra. Cap altre punt
|
||||
// (respawn, exit, disable) el toca: així només sona reverberat el
|
||||
// primer LAND de la cinemàtica d'entrada.
|
||||
std::string next_land_reverb_preset_;
|
||||
|
||||
// Escala d'empenta usada per `snapToLane` quan descobreix un blocker
|
||||
// (mateixa del resolver). 1.0 = sense escalar (comportament si
|
||||
// GameScene no ha cridat setPushFactor).
|
||||
@@ -361,25 +467,34 @@ class Player : public AnimatedSprite {
|
||||
// reinstanciar-lo.
|
||||
Map::Info map_;
|
||||
|
||||
// Direcció "cap on mira" el jugador. Governa quin conjunt d'animacions
|
||||
// s'aplica (lateral, amunt, avall) i si el sprite es flipeja o no. Els
|
||||
// moviments purament horitzontals o diagonals prenen `SIDE` (prevalença
|
||||
// lateral), els purament verticals fan `UP`/`DOWN`. En aturar-se es
|
||||
// preserva l'última orientació perquè la versió stand coincideixi.
|
||||
enum class Facing : std::uint8_t { SIDE,
|
||||
UP,
|
||||
DOWN };
|
||||
Facing facing_{Facing::SIDE};
|
||||
// Direcció "cap on mira" el jugador. Una entrada per cada cardinal: el
|
||||
// codi del joc no toca mai el flip — l'oest reprodueix `*_west`, que al
|
||||
// YAML declara `flip_h: true`, i és el motor d'animació qui aplica el
|
||||
// flip al sprite. Els moviments diagonals prenen el component lateral
|
||||
// (EAST/WEST); en aturar-se es preserva l'última orientació perquè la
|
||||
// versió stand coincideixi.
|
||||
enum class Facing : std::uint8_t { EAST,
|
||||
WEST,
|
||||
NORTH,
|
||||
SOUTH };
|
||||
Facing facing_{Facing::EAST};
|
||||
|
||||
// Helpers de composició de `animationName`. Han de viure ací (després
|
||||
// de la declaració de `Facing`) perquè la signatura el referencia.
|
||||
static auto facingSuffix(Facing facing) -> const char*;
|
||||
static auto statePrefix(State state, bool moving, bool in_attack_swing) -> const char*;
|
||||
|
||||
// Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°.
|
||||
// El parell (Facing, flip) només té 3+1 estats; aquesta enum n'afegeix
|
||||
// la 4a sense reorganitzar el sistema d'animacions.
|
||||
// Pública perquè el `WhipAttack` la passa a `enterHurt` quan colpeja
|
||||
// un altre jugador (friendly fire).
|
||||
public:
|
||||
enum class Cardinal : std::uint8_t { NONE,
|
||||
NORTH,
|
||||
SOUTH,
|
||||
EAST,
|
||||
WEST };
|
||||
|
||||
private:
|
||||
// Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb
|
||||
// `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i
|
||||
// el comportament és instantani: soltar input para el jugador en sec.
|
||||
@@ -440,12 +555,66 @@ class Player : public AnimatedSprite {
|
||||
struct HurtConfig {
|
||||
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
|
||||
int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
|
||||
// Fitxer .wav reproduït en entrar a HURT (relatiu a `data/sounds/`).
|
||||
// Mateixa convenció que la resta de sons del jugador: el basename val
|
||||
// de clau al Resource::Cache i de path. Buit o absent = entrada
|
||||
// silenciosa.
|
||||
std::string sound_key;
|
||||
};
|
||||
static HurtConfig hurt_config;
|
||||
|
||||
// Llig el bloc `hurt:` del YAML i n'omple `hurt_config`.
|
||||
static void readHurtBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Configuració del látigo del jugador llegida del bloc `whip:` del
|
||||
// YAML. `range_tiles` defineix la longitud de la hitbox abans de
|
||||
// retallar; `swing_ms` és la durada de l'animació d'atac (des
|
||||
// d'`enterAttacking` fins a desbloquejar el cooldown); `cooldown_ms`
|
||||
// afegeix una fase posterior durant la qual el jugador segueix
|
||||
// bloquejat però la hitbox ja no fa dany. `damage` queda llegit per
|
||||
// a futur; per ara els objectius reben sempre `enterHurt`.
|
||||
struct WhipConfig {
|
||||
float range_tiles{0.0F};
|
||||
int swing_ms{0};
|
||||
int cooldown_ms{0};
|
||||
int damage{0};
|
||||
// Fitxer .wav reproduït al moment de disparar el swing (relatiu a
|
||||
// `data/sounds/`). Convenció: el basename val tant de clau al
|
||||
// Resource::Cache com de path. Buit o absent = swing silenciós.
|
||||
std::string sound_key;
|
||||
};
|
||||
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);
|
||||
|
||||
// Configuració de mort/respawn llegida del bloc `death:` del YAML.
|
||||
// `respawn_delay_ms` és la pausa entre la fi de `death_spinning` i la
|
||||
// reaparició al spawn; `initial_lives` és el comptador d'intents amb
|
||||
// què naix cada Player. Si el bloc manca, defaults segurs (0/3).
|
||||
struct DeathConfig {
|
||||
int respawn_delay_ms{0};
|
||||
int initial_lives{3};
|
||||
};
|
||||
static DeathConfig death_config;
|
||||
|
||||
// Llig el bloc `death:` del YAML i n'omple `death_config`. Si manca,
|
||||
// `respawn_delay_ms` queda a 0 (el respawn és immediat al final de
|
||||
// l'animació) i `initial_lives` es manté al default (3).
|
||||
static void readDeathBlock(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 corresponent al `facing_` actual del sprite — la direcció
|
||||
// cap on mira el jugador. Mapeig 1:1 amb `Facing` (mai NONE).
|
||||
[[nodiscard]] auto currentFacingCardinal() const -> Cardinal;
|
||||
|
||||
// Activa l'estat HURT: el jugador queda projectat amb velocitat
|
||||
// inicial `hurt_config.initial_speed_px_per_s`. La direcció es tria
|
||||
// així:
|
||||
@@ -456,8 +625,12 @@ class Player : public AnimatedSprite {
|
||||
// i tria el que dóna més recorregut lliure.
|
||||
// Decelera linealment fins a parar i, en eixe instant, torna a
|
||||
// GROUNDED. Sense col·lisió amb altres jugadors mentre dura.
|
||||
// Pública: la dispara tant la cinemàtica de "caure damunt" com el
|
||||
// `WhipAttack` quan colpeja un altre jugador (friendly fire).
|
||||
public:
|
||||
void enterHurt(Cardinal preferred = Cardinal::NONE);
|
||||
|
||||
private:
|
||||
// Tria la direcció definitiva del HURT. Si `preferred` cabe el
|
||||
// recorregut natural complet, la retorna; d'altra manera tria la
|
||||
// direcció amb més recorregut lliure entre els 4 cardinals.
|
||||
@@ -492,10 +665,53 @@ class Player : public AnimatedSprite {
|
||||
bool jump_held_keyboard_{false};
|
||||
bool jump_held_gamepad_{false};
|
||||
|
||||
// Mateixa idea per al botó d'atac: edge-detection independent
|
||||
// teclat/mando, perquè mantenir la tecla no genere atacs en cadena.
|
||||
bool attack_held_keyboard_{false};
|
||||
bool attack_held_gamepad_{false};
|
||||
|
||||
// Temps que queda dins de l'estat ATTACKING (swing + cooldown). Quan
|
||||
// arriba a 0, transició a GROUNDED. Mentre dura, el jugador queda
|
||||
// bloquejat i no rep input direccional ni de salt ni d'atac.
|
||||
float attacking_remaining_ms_{0.0F};
|
||||
|
||||
// Estat de DYING. `death_animation_complete_` es marca quan
|
||||
// `animationIsCompleted()` retorna true per primera vegada al cicle
|
||||
// actual; només llavors comença a descomptar `respawn_remaining_ms_`.
|
||||
// Quan el delay arriba a 0, `respawnFromDying()` aplica `respawn(...)`
|
||||
// i decreix el comptador de vides.
|
||||
bool death_animation_complete_{false};
|
||||
float respawn_remaining_ms_{0.0F};
|
||||
|
||||
// Comptador d'intents (estil Mario). Inicialitzat a `death_config.
|
||||
// initial_lives` al ctor; decreix una vegada per cada respawn
|
||||
// post-mort (no per respawns de canvi de sala). Pot anar a negatius:
|
||||
// avui no hi ha game over.
|
||||
int lives_remaining_{0};
|
||||
|
||||
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
||||
// sprite). Les assigna el GameScene via `setSpawnPoint`. 0/0 fins que
|
||||
// el GameScene n'invoque el setter al ctor / `loadRoom`.
|
||||
float spawn_x_{0.0F};
|
||||
float spawn_y_{0.0F};
|
||||
|
||||
// Aplica el respawn al final del cicle DYING: decreix vides (sense
|
||||
// clamp), neteja els flags interns del cicle i delega a `respawn`
|
||||
// amb les coordenades del spawn.
|
||||
void respawnFromDying();
|
||||
|
||||
// Branca de `applyMovement` per a DYING. Avança l'animació, detecta
|
||||
// la fi i descompta el delay; al final, dispara el respawn.
|
||||
void applyDyingMovement(float delta_time);
|
||||
|
||||
// Llig el botó de salt aquest frame (teclat + mando assignat) i,
|
||||
// amb edge-detection, retorna true si s'ha de despegar el salt.
|
||||
[[nodiscard]] auto consumeJumpEdge() -> bool;
|
||||
|
||||
// Anàleg per a l'atac: edge-detection del botó d'atac (teclat
|
||||
// `controls_.attack` + `SDL_GAMEPAD_BUTTON_WEST`).
|
||||
[[nodiscard]] auto consumeAttackEdge() -> bool;
|
||||
|
||||
// Inicia un salt: copia `intended_v*` com a velocitat fixada del salt,
|
||||
// posa `jump_vz_ = impulse`, transiciona a AIRBORNE.
|
||||
void startJump();
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "game/levels/level_manager.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
|
||||
namespace Levels {
|
||||
|
||||
auto LevelManager::get() -> LevelManager& {
|
||||
static LevelManager instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
LevelManager::LevelManager() { loadIndex(); }
|
||||
|
||||
void LevelManager::loadIndex() {
|
||||
const std::string PATH = Defaults::Pyramid::LEVELS_YAML;
|
||||
auto bytes = Resource::Helper::loadFile(PATH);
|
||||
if (bytes.empty()) {
|
||||
throw std::runtime_error("LevelManager: no s'ha pogut obrir " + PATH);
|
||||
}
|
||||
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||
const auto* end = begin + bytes.size();
|
||||
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||
|
||||
if (!yaml.contains("sequence") || !yaml["sequence"].is_sequence()) {
|
||||
throw std::runtime_error("LevelManager: manca o és invàlid `sequence:` a " + PATH);
|
||||
}
|
||||
const auto& seq = yaml["sequence"];
|
||||
entries_.clear();
|
||||
entries_.reserve(seq.size());
|
||||
for (const auto& e : seq) {
|
||||
Entry entry{
|
||||
.pyramid = e["pyramid"].get_value<int>(),
|
||||
.room = e["room"].get_value<int>(),
|
||||
.file = e["file"].get_value<std::string>(),
|
||||
};
|
||||
entries_.push_back(std::move(entry));
|
||||
}
|
||||
if (entries_.empty()) {
|
||||
throw std::runtime_error("LevelManager: la seqüència de " + PATH + " és buida");
|
||||
}
|
||||
index_ = 0;
|
||||
std::cout << "LevelManager: " << entries_.size() << " sales carregades de "
|
||||
<< PATH << " (començant a " << currentLabel() << ")\n";
|
||||
}
|
||||
|
||||
auto LevelManager::currentPath() const -> const std::string& {
|
||||
return entries_[index_].file;
|
||||
}
|
||||
|
||||
auto LevelManager::currentPyramid() const -> int { return entries_[index_].pyramid; }
|
||||
auto LevelManager::currentRoom() const -> int { return entries_[index_].room; }
|
||||
|
||||
auto LevelManager::currentLabel() const -> std::string {
|
||||
return "P" + std::to_string(currentPyramid()) + "-R" + std::to_string(currentRoom());
|
||||
}
|
||||
|
||||
auto LevelManager::index() const -> int { return index_; }
|
||||
auto LevelManager::count() const -> int { return static_cast<int>(entries_.size()); }
|
||||
|
||||
auto LevelManager::advance() -> bool {
|
||||
if (index_ + 1 >= count()) { return false; }
|
||||
++index_;
|
||||
std::cout << "LevelManager: avança a " << currentLabel()
|
||||
<< " (" << currentPath() << ")\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
void LevelManager::restart() {
|
||||
index_ = 0;
|
||||
std::cout << "LevelManager: restart a " << currentLabel() << '\n';
|
||||
}
|
||||
|
||||
void LevelManager::setIndex(int new_index) {
|
||||
const int CLAMPED = std::clamp(new_index, 0, count() - 1);
|
||||
if (CLAMPED != index_) {
|
||||
index_ = CLAMPED;
|
||||
std::cout << "LevelManager: setIndex(" << new_index << ") → "
|
||||
<< currentLabel() << " (" << currentPath() << ")\n";
|
||||
}
|
||||
}
|
||||
|
||||
auto LevelManager::setStartingPoint(int pyramid, int room) -> bool {
|
||||
for (std::size_t i = 0; i < entries_.size(); ++i) {
|
||||
if (entries_[i].pyramid == pyramid && entries_[i].room == room) {
|
||||
index_ = static_cast<int>(i);
|
||||
std::cout << "LevelManager: starting_point P" << pyramid << "-R" << room
|
||||
<< " trobat a l'índex " << i << " (" << currentPath() << ")\n";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
std::cerr << "LevelManager: starting_point P" << pyramid << "-R" << room
|
||||
<< " no trobat a levels.yaml — es manté l'índex actual ("
|
||||
<< currentLabel() << ")\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Levels
|
||||
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Levels {
|
||||
|
||||
// Font de veritat de la progressió de sales del joc. Carrega
|
||||
// `data/maps/levels.yaml` a la primera crida a `get()` i exposa l'estat
|
||||
// "on sóc ara" + les operacions per moure'm. La GameScene, l'Editor i
|
||||
// els salts de debug consulten aquest singleton; ningú no manté el seu
|
||||
// propi índex de progressió.
|
||||
//
|
||||
// Meyers singleton: la destrucció estàtica només toca std::string i
|
||||
// std::vector, així que és segura sense ordenació explícita.
|
||||
class LevelManager {
|
||||
public:
|
||||
static auto get() -> LevelManager&;
|
||||
|
||||
LevelManager(const LevelManager&) = delete;
|
||||
LevelManager(LevelManager&&) = delete;
|
||||
auto operator=(const LevelManager&) -> LevelManager& = delete;
|
||||
auto operator=(LevelManager&&) -> LevelManager& = delete;
|
||||
|
||||
// Una entrada del `levels.yaml`. `file` és la ruta relativa al data
|
||||
// root (mateixa convenció que la resta de YAML del joc); pyramid i
|
||||
// room són purament informatius (logs i text d'intro).
|
||||
struct Entry {
|
||||
int pyramid{0};
|
||||
int room{0};
|
||||
std::string file;
|
||||
};
|
||||
|
||||
// Ruta del nivell actualment apuntat. Sempre vàlida un cop carregat
|
||||
// l'índex (entries_ no pot estar buit). Pensada per a `Map::loadFrom`,
|
||||
// `EntityManager::reload` i l'Editor.
|
||||
[[nodiscard]] auto currentPath() const -> const std::string&;
|
||||
[[nodiscard]] auto currentPyramid() const -> int;
|
||||
[[nodiscard]] auto currentRoom() const -> int;
|
||||
// "P1-R3" — útil per a logs i el text d'intro de sala.
|
||||
[[nodiscard]] auto currentLabel() const -> std::string;
|
||||
|
||||
// Posició dins de la seqüència. `count()` és el total de sales
|
||||
// carregades del YAML (no necessàriament 30: si afegeixes/treus
|
||||
// entrades de `levels.yaml`, el manager s'adapta).
|
||||
[[nodiscard]] auto index() const -> int;
|
||||
[[nodiscard]] auto count() const -> int;
|
||||
|
||||
// Avança al següent nivell. Retorna true si avança, false si ja
|
||||
// estava a l'últim. NO fa loop — la GameScene decideix la política
|
||||
// (loop a la primera, fi de joc, etc.) segons el context.
|
||||
auto advance() -> bool;
|
||||
|
||||
// Torna al primer nivell.
|
||||
void restart();
|
||||
|
||||
// Salta a un índex concret. Clamp als bordes [0, count()-1].
|
||||
// Pensat per als hotkeys debug.
|
||||
void setIndex(int new_index);
|
||||
|
||||
// Posiciona l'índex a la primera entrada que coincideix amb
|
||||
// (pyramid, room). Retorna true si trobada; si no, l'índex no
|
||||
// es modifica i el caller decideix què fer (típicament ignorar i
|
||||
// arrencar des de la primera sala). Pensat per al
|
||||
// `debug.yaml.starting_level` i per a futures lookups per coords.
|
||||
auto setStartingPoint(int pyramid, int room) -> bool;
|
||||
|
||||
private:
|
||||
LevelManager();
|
||||
~LevelManager() = default;
|
||||
|
||||
// Carrega `data/maps/levels.yaml` via `Resource::Helper::loadFile`.
|
||||
// Si manca, és invàlid o té la seqüència buida, llança — no té sentit
|
||||
// continuar sense progressió de sales.
|
||||
void loadIndex();
|
||||
|
||||
std::vector<Entry> entries_;
|
||||
int index_{0};
|
||||
};
|
||||
|
||||
} // namespace Levels
|
||||