Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11a909b58d | ||
|
|
17cf6e0636 | ||
|
|
7d0aca506a | ||
|
|
7cd7b4dc4f | ||
|
|
6d4f606eeb | ||
|
|
1ccd444aad | ||
|
|
3bfca35f3a | ||
|
|
3e5217066f | ||
|
|
4da50571eb | ||
|
|
d4593d0686 | ||
|
|
a53cf4b797 | ||
|
|
bcbf990a5f | ||
|
|
27cf2f032d | ||
|
|
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
|
||||
@@ -47,21 +50,33 @@ set(ENTITY_SOURCES
|
||||
source/game/entities/concrete/rolling_rock.cpp
|
||||
source/game/entities/concrete/wall_trap.cpp
|
||||
source/game/entities/concrete/floor_trap.cpp
|
||||
source/game/entities/concrete/hole.cpp
|
||||
source/game/entities/concrete/dust.cpp
|
||||
)
|
||||
|
||||
# Entitats que criden directament símbols de Player (com WhipAttack via
|
||||
# `enterHurt`/`getBaseBox`): només van a l'executable principal.
|
||||
set(ENTITY_SOURCES
|
||||
${ENTITY_SOURCES_BASE}
|
||||
source/game/entities/concrete/whip_attack.cpp
|
||||
)
|
||||
|
||||
set(APP_SOURCES
|
||||
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 +91,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,10 +117,12 @@ 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
|
||||
# En Release els fitxers compilen a object buit i l'enllaçador no
|
||||
# arrossega cap símbol del joc final.
|
||||
source/game/editor/editor.cpp
|
||||
source/game/editor/editor_config.cpp
|
||||
${ENTITY_SOURCES}
|
||||
# Fitxer només del binari principal: `loadFromLevel` depèn de
|
||||
# Resource::Cache (→ Surface → GIF → àudio). Separat per no arrossegar
|
||||
@@ -163,6 +181,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()
|
||||
@@ -323,10 +350,11 @@ option(BUILD_TESTS "Build smoke tests" ON)
|
||||
if(BUILD_TESTS)
|
||||
add_executable(test_entities_smoke
|
||||
tests/test_entities_smoke.cpp
|
||||
tests/hole_player_stubs.cpp
|
||||
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 +367,30 @@ 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
|
||||
tests/hole_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 +451,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
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Tema visual de l'editor de tilemap (només Debug). Carregat per
|
||||
# EditorConfig a l'arrencada; les claus absents cauen als valors compilats
|
||||
# a Defaults::Editor (veure source/utils/defaults.hpp).
|
||||
|
||||
picker:
|
||||
margin: 8 # px entre canvas i caixa del picker
|
||||
spacing: 1 # px de separació entre tiles dins del picker
|
||||
|
||||
# Colors ARGB en hex. Accepta enter (0xFF2244FF) o cadena ("#FF2244FF").
|
||||
colors:
|
||||
border: "0xFF2244FF" # vora 1 px que envolta canvas i picker
|
||||
picker_bg: "0xFF101824" # fons del picker
|
||||
picker_hover: "0xFFFFFFFF" # vora 1 px del tile sota el cursor
|
||||
|
After Width: | Height: | Size: 182 B |
@@ -0,0 +1,26 @@
|
||||
name: hole
|
||||
type: static_hazard
|
||||
sprite:
|
||||
path: hole.gif
|
||||
size: [16, 16]
|
||||
tile_size: [1, 1]
|
||||
collision:
|
||||
# Forat: ocupa el tile sencer (16×16). `height: 0` deixa que un salt
|
||||
# AIRBORNE el creui sense disparar el contacte (el filtre d'altura de
|
||||
# `checkPlayerCollisions` el descarta quan `jump_z_ > height`). El
|
||||
# bucle de contacte admet l'entitat per `flags.triggers_on_overlap`,
|
||||
# encara que `flags.collides` sigui false.
|
||||
size: [16, 16]
|
||||
height: 0
|
||||
flags:
|
||||
collides: false
|
||||
interactable: false
|
||||
# Avui el loader no llig el bloc `flags:` (és declaratiu); el flag
|
||||
# real es fixa al constructor C++ de `Hole`. Mantenim el camp per
|
||||
# coherència amb la resta de plantilles i per quan es cablege la
|
||||
# lectura genèrica de flags.
|
||||
triggers_on_overlap: true
|
||||
animations:
|
||||
idle: { frames: [0], fps: 1, loop: true }
|
||||
stats:
|
||||
damage: 0
|
||||
@@ -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,152 @@
|
||||
# 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
|
||||
|
||||
|
||||
- type: mummy
|
||||
position: [10, 7]
|
||||
- type: mummy
|
||||
position: [7, 9]
|
||||
|
||||
- type: hole
|
||||
position: [11, 5]
|
||||
- type: hole
|
||||
position: [4, 7]
|
||||
- type: hole
|
||||
position: [8, 11]
|
||||
- type: hole
|
||||
position: [15, 11]
|
||||
- type: hole
|
||||
position: [15, 5]
|
||||
@@ -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,104 @@
|
||||
# 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
|
||||
|
||||
# Mort per caiguda a un agujero (DeathCause::HOLE). `loop: false` perquè
|
||||
# `animationIsCompleted()` dispari el respawn timer determinísticament.
|
||||
death_hole:
|
||||
frames: [15]
|
||||
frame_ms: 60
|
||||
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 +107,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,104 @@
|
||||
# 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
|
||||
|
||||
# Mort per caiguda a un agujero (DeathCause::HOLE). `loop: false` perquè
|
||||
# `animationIsCompleted()` dispari el respawn timer determinísticament.
|
||||
death_hole:
|
||||
frames: [15]
|
||||
frame_ms: 60
|
||||
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 +107,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,10 @@
|
||||
#include "core/system/event_buffer.hpp"
|
||||
#include "core/ui/ui_config.hpp"
|
||||
#include "game/debug_config.hpp"
|
||||
#ifdef _DEBUG
|
||||
#include "game/editor/editor_config.hpp"
|
||||
#endif
|
||||
#include "game/levels/level_manager.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "game/scenes/game_scene.hpp"
|
||||
#include "game/scenes/intro_scene.hpp"
|
||||
@@ -80,6 +86,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);
|
||||
@@ -111,6 +122,12 @@ Director::Director() {
|
||||
// data/ui.yaml cau al valor compilat a Defaults::UI.
|
||||
UiConfig::get().load("data/ui.yaml");
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Tema visual de l'editor de tilemap (Debug-only). Claus absents
|
||||
// cauen als valors compilats a Defaults::Editor.
|
||||
EditorConfig::get().load("data/editor/editor.yaml");
|
||||
#endif
|
||||
|
||||
// Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els
|
||||
// overlays perquè tots els reaprofiten sense duplicar la lògica. Els
|
||||
// noms de font viuen al fitxer data/ui.yaml via UiConfig.
|
||||
@@ -121,10 +138,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 +215,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 +229,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 +311,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 +341,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,17 +14,28 @@
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "core/system/event_buffer.hpp"
|
||||
#include "game/editor/editor_config.hpp"
|
||||
#include "game/levels/level_manager.hpp"
|
||||
#include "game/map/tilemap.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
#include "utils/defines.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Gruix del border 1 px del picker: reservat dins del rectangle perquè
|
||||
// no envaeixi els tiles ni se surti fora de la caixa. La cella efectiva
|
||||
// de cada tile té `pitch = tile_size + spacing` i la graella s'inicia
|
||||
// un píxel cap dins de cada vora.
|
||||
constexpr int PICKER_BORDER = 1;
|
||||
|
||||
// Geometria del TilePicker en espai canvas. `cols × rows_visible` tiles
|
||||
// encapsulats dins d'un rectangle amb `PICKER_MARGIN` per vora. Com que
|
||||
// el tileset real (p.ex. 32×16 tiles de 16 px = 512×256) no cabria a
|
||||
// 1× dins del canvas 336×240, linealitzem els índexs i els repartim
|
||||
// en una graella que SÍ cap, amb scroll vertical per veure la resta.
|
||||
// encapsulats dins d'un rectangle amb `picker.margin` per vora i un
|
||||
// border 1 px que el delimita. Si el tileset cap en amplada (la suma
|
||||
// de tiles + spacing + border ≤ INNER_W), en respectem la graella
|
||||
// natural — els tiles surten ordenats igual que al GIF; si no,
|
||||
// linealitzem els índexs en una graella més ampla que sí cap. Quan el
|
||||
// contingut cap sense scroll, la caixa s'arrossega a alçada mínima i
|
||||
// es centra verticalment dins de l'àrea interna.
|
||||
struct PickerLayout {
|
||||
int x{0};
|
||||
int y{0};
|
||||
@@ -33,6 +44,8 @@ namespace {
|
||||
int cols{0};
|
||||
int rows_visible{0};
|
||||
int tile_size{0};
|
||||
int spacing{0};
|
||||
int pitch{0}; // tile_size + spacing — pas entre orígens de tiles veïns.
|
||||
int total_tiles{0};
|
||||
int max_scroll{0};
|
||||
};
|
||||
@@ -40,27 +53,59 @@ namespace {
|
||||
auto layoutPicker(const Tilemap& tm) -> PickerLayout {
|
||||
PickerLayout lay{};
|
||||
lay.tile_size = tm.getTileSize();
|
||||
const int M = Defaults::Editor::PICKER_MARGIN;
|
||||
lay.spacing = std::max(0, EditorConfig::get().pickerSpacing());
|
||||
lay.pitch = lay.tile_size + lay.spacing;
|
||||
const int M = EditorConfig::get().pickerMargin();
|
||||
const int INNER_W = GameCanvas::WIDTH - (2 * M);
|
||||
const int INNER_H = GameCanvas::HEIGHT - (2 * M);
|
||||
if (lay.tile_size <= 0) { return lay; }
|
||||
lay.cols = INNER_W / lay.tile_size;
|
||||
lay.rows_visible = INNER_H / lay.tile_size;
|
||||
lay.x = M;
|
||||
lay.y = M;
|
||||
lay.w = lay.cols * lay.tile_size;
|
||||
lay.h = lay.rows_visible * lay.tile_size;
|
||||
if (lay.tile_size <= 0 || lay.pitch <= 0) { return lay; }
|
||||
|
||||
// Espai útil per a tiles, descomptat el border 1 px de cada vora.
|
||||
// Inversió de N·ts + (N-1)·spacing ≤ AVAIL → N ≤ (AVAIL+spacing)/pitch.
|
||||
const int AVAIL_W = INNER_W - (2 * PICKER_BORDER);
|
||||
const int AVAIL_H = INNER_H - (2 * PICKER_BORDER);
|
||||
const int MAX_COLS = std::max(0, (AVAIL_W + lay.spacing) / lay.pitch);
|
||||
const int MAX_ROWS = std::max(0, (AVAIL_H + lay.spacing) / lay.pitch);
|
||||
|
||||
int tcols = 0;
|
||||
const auto TILESET = Resource::Cache::get().getSurface(tm.getTilesetPath());
|
||||
if (TILESET != nullptr) {
|
||||
const int TCOLS = TILESET->getWidth() / lay.tile_size;
|
||||
tcols = TILESET->getWidth() / lay.tile_size;
|
||||
const int TROWS = TILESET->getHeight() / lay.tile_size;
|
||||
lay.total_tiles = TCOLS * TROWS;
|
||||
lay.total_tiles = tcols * TROWS;
|
||||
}
|
||||
// Com que els índexs del tileset són row-major (idx = row·TCOLS + col),
|
||||
// fer servir cols = TCOLS reprodueix exactament la disposició del GIF.
|
||||
const bool FITS_W = (tcols > 0 && tcols <= MAX_COLS);
|
||||
lay.cols = FITS_W ? tcols : MAX_COLS;
|
||||
|
||||
const int TOTAL_ROWS = (lay.cols > 0) ? ((lay.total_tiles + lay.cols - 1) / lay.cols) : 0;
|
||||
// Si tot el contingut cap sense scroll, encongim la caixa a les
|
||||
// files reals; així podem centrar-la verticalment.
|
||||
lay.rows_visible = std::min(MAX_ROWS, TOTAL_ROWS);
|
||||
lay.max_scroll = std::max(0, TOTAL_ROWS - lay.rows_visible);
|
||||
|
||||
// Mida real de la caixa: tiles + spacing intern + 1 px de border per cara.
|
||||
const int TILES_W = (lay.cols > 0) ? ((lay.cols * lay.tile_size) + ((lay.cols - 1) * lay.spacing)) : 0;
|
||||
const int TILES_H = (lay.rows_visible > 0) ? ((lay.rows_visible * lay.tile_size) + ((lay.rows_visible - 1) * lay.spacing)) : 0;
|
||||
lay.w = TILES_W + (2 * PICKER_BORDER);
|
||||
lay.h = TILES_H + (2 * PICKER_BORDER);
|
||||
// Centrat horitzontal i vertical dins de l'àrea interna; quan el
|
||||
// picker arriba a l'alçada màxima, el centrat vertical degenera a
|
||||
// origen=M (no hi ha espai extra que repartir).
|
||||
lay.x = M + ((INNER_W - lay.w) / 2);
|
||||
lay.y = M + ((INNER_H - lay.h) / 2);
|
||||
return lay;
|
||||
}
|
||||
|
||||
// Origen (cantonada superior-esquerra) del tile a la cel·la (vc, vr)
|
||||
// dins del picker. Centralitzat perquè render i hit-test no divergeixin.
|
||||
auto pickerTileOrigin(const PickerLayout& lay, int vc, int vr) -> std::pair<int, int> {
|
||||
const int PX = lay.x + PICKER_BORDER + (vc * lay.pitch);
|
||||
const int PY = lay.y + PICKER_BORDER + (vr * lay.pitch);
|
||||
return {PX, PY};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Editor::enter() {
|
||||
@@ -76,11 +121,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"));
|
||||
}
|
||||
@@ -168,11 +215,21 @@ void Editor::updateMap(Tilemap& tilemap, int canvas_x, int canvas_y) {
|
||||
void Editor::updatePicker(const Tilemap& tilemap, int canvas_x, int canvas_y) {
|
||||
const auto LAY = layoutPicker(tilemap);
|
||||
picker_hover_ = -1;
|
||||
if (canvas_x >= LAY.x && canvas_x < LAY.x + LAY.w && canvas_y >= LAY.y && canvas_y < LAY.y + LAY.h && LAY.tile_size > 0) {
|
||||
const int VC = (canvas_x - LAY.x) / LAY.tile_size;
|
||||
const int VR = (canvas_y - LAY.y) / LAY.tile_size;
|
||||
const int IDX = ((picker_scroll_ + VR) * LAY.cols) + VC;
|
||||
if (IDX < LAY.total_tiles) { picker_hover_ = IDX; }
|
||||
// Espai útil per als tiles dins de la caixa (descomptat el border 1 px).
|
||||
const int INNER_X = LAY.x + PICKER_BORDER;
|
||||
const int INNER_Y = LAY.y + PICKER_BORDER;
|
||||
const int RX = canvas_x - INNER_X;
|
||||
const int RY = canvas_y - INNER_Y;
|
||||
if (RX >= 0 && RY >= 0 && LAY.tile_size > 0 && LAY.pitch > 0 && LAY.cols > 0) {
|
||||
const int VC = RX / LAY.pitch;
|
||||
const int VR = RY / LAY.pitch;
|
||||
// Píxels de spacing entre cel·les: el clic cau a la "vora morta".
|
||||
const bool IN_SPACING_X = (RX % LAY.pitch) >= LAY.tile_size;
|
||||
const bool IN_SPACING_Y = (RY % LAY.pitch) >= LAY.tile_size;
|
||||
if (!IN_SPACING_X && !IN_SPACING_Y && VC < LAY.cols && VR < LAY.rows_visible) {
|
||||
const int IDX = ((picker_scroll_ + VR) * LAY.cols) + VC;
|
||||
if (IDX < LAY.total_tiles) { picker_hover_ = IDX; }
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& ev : EventBuffer::events) {
|
||||
@@ -203,7 +260,7 @@ void Editor::render(const Tilemap& tilemap) const {
|
||||
tilemap.renderTilePreview(PX, PY, brush_);
|
||||
}
|
||||
|
||||
constexpr auto BORDER = Defaults::Editor::BORDER_ARGB;
|
||||
const auto BORDER = EditorConfig::get().borderArgb();
|
||||
screen.fillRect(0, 0, W, 1, BORDER);
|
||||
screen.fillRect(0, H - 1, W, 1, BORDER);
|
||||
screen.fillRect(0, 0, 1, H, BORDER);
|
||||
@@ -212,34 +269,43 @@ void Editor::render(const Tilemap& tilemap) const {
|
||||
|
||||
void Editor::renderPicker(const Tilemap& tilemap) const {
|
||||
const auto LAY = layoutPicker(tilemap);
|
||||
if (LAY.cols <= 0 || LAY.rows_visible <= 0) { return; }
|
||||
auto& screen = Screen::get();
|
||||
screen.fillRect(LAY.x, LAY.y, LAY.w, LAY.h, Defaults::Editor::PICKER_BG_ARGB);
|
||||
const auto& cfg = EditorConfig::get();
|
||||
screen.fillRect(LAY.x, LAY.y, LAY.w, LAY.h, cfg.pickerBgArgb());
|
||||
|
||||
for (int vr = 0; vr < LAY.rows_visible; ++vr) {
|
||||
const int LOGICAL_ROW = picker_scroll_ + vr;
|
||||
for (int vc = 0; vc < LAY.cols; ++vc) {
|
||||
const int IDX = (LOGICAL_ROW * LAY.cols) + vc;
|
||||
if (IDX >= LAY.total_tiles) { break; }
|
||||
const int PX = LAY.x + (vc * LAY.tile_size);
|
||||
const int PY = LAY.y + (vr * LAY.tile_size);
|
||||
const auto [PX, PY] = pickerTileOrigin(LAY, vc, vr);
|
||||
tilemap.renderTilePreview(PX, PY, IDX);
|
||||
}
|
||||
}
|
||||
|
||||
if (picker_hover_ >= 0 && LAY.cols > 0) {
|
||||
if (picker_hover_ >= 0) {
|
||||
const int VR = (picker_hover_ / LAY.cols) - picker_scroll_;
|
||||
const int VC = picker_hover_ % LAY.cols;
|
||||
if (VR >= 0 && VR < LAY.rows_visible) {
|
||||
const int PX = LAY.x + (VC * LAY.tile_size);
|
||||
const int PY = LAY.y + (VR * LAY.tile_size);
|
||||
const auto [PX, PY] = pickerTileOrigin(LAY, VC, VR);
|
||||
const int TS = LAY.tile_size;
|
||||
constexpr auto C = Defaults::Editor::PICKER_HOVER_ARGB;
|
||||
const auto C = cfg.pickerHoverArgb();
|
||||
screen.fillRect(PX, PY, TS, 1, C);
|
||||
screen.fillRect(PX, PY + TS - 1, TS, 1, C);
|
||||
screen.fillRect(PX, PY, 1, TS, C);
|
||||
screen.fillRect(PX + TS - 1, PY, 1, TS, C);
|
||||
}
|
||||
}
|
||||
|
||||
// Border 1 px del picker, dibuixat al damunt del fons i dels tiles
|
||||
// perquè delimite clarament la caixa també quan hi ha tiles que
|
||||
// arriben a la vora.
|
||||
const auto BORDER = cfg.borderArgb();
|
||||
screen.fillRect(LAY.x, LAY.y, LAY.w, 1, BORDER);
|
||||
screen.fillRect(LAY.x, LAY.y + LAY.h - 1, LAY.w, 1, BORDER);
|
||||
screen.fillRect(LAY.x, LAY.y, 1, LAY.h, BORDER);
|
||||
screen.fillRect(LAY.x + LAY.w - 1, LAY.y, 1, LAY.h, BORDER);
|
||||
}
|
||||
|
||||
#endif // _DEBUG
|
||||
@@ -0,0 +1,85 @@
|
||||
#include "game/editor/editor_config.hpp"
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T>
|
||||
void readField(const fkyaml::node& node, const char* key, T& dst) {
|
||||
if (node.contains(key)) { dst = node[key].get_value<T>(); }
|
||||
}
|
||||
|
||||
// Llegeix un color ARGB. Accepta enter (0xFF2244FF al YAML) o cadena
|
||||
// ("0xFF2244FF" / "#FF2244FF"). std::stoul amb base=0 detecta el prefix
|
||||
// "0x" automàticament; si el valor és invàlid, deixem `dst` intacte i
|
||||
// avisem perquè l'error no passi en silenci.
|
||||
void readArgb(const fkyaml::node& node, const char* key, std::uint32_t& dst) {
|
||||
if (!node.contains(key)) { return; }
|
||||
try {
|
||||
const auto& v = node[key];
|
||||
if (v.is_integer()) {
|
||||
dst = static_cast<std::uint32_t>(v.get_value<std::uint64_t>());
|
||||
return;
|
||||
}
|
||||
auto s = v.get_value<std::string>();
|
||||
if (!s.empty() && s.front() == '#') { s.erase(0, 1); }
|
||||
dst = static_cast<std::uint32_t>(std::stoul(s, nullptr, 0));
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "EditorConfig: color '" << key << "' invàlid (" << e.what() << ") — ignorat\n";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditorConfig::EditorConfig()
|
||||
: picker_margin_(Defaults::Editor::PICKER_MARGIN),
|
||||
picker_spacing_(Defaults::Editor::PICKER_SPACING),
|
||||
border_argb_(Defaults::Editor::BORDER_ARGB),
|
||||
picker_bg_argb_(Defaults::Editor::PICKER_BG_ARGB),
|
||||
picker_hover_argb_(Defaults::Editor::PICKER_HOVER_ARGB) {
|
||||
}
|
||||
|
||||
auto EditorConfig::get() -> EditorConfig& {
|
||||
static EditorConfig instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
void EditorConfig::load(const std::string& file_path) {
|
||||
auto bytes = Resource::Helper::loadFile(file_path);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "EditorConfig: no s'ha pogut obrir " << file_path << " — s'usaran els defaults compilats\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("picker")) {
|
||||
const auto& p = yaml["picker"];
|
||||
readField(p, "margin", picker_margin_);
|
||||
readField(p, "spacing", picker_spacing_);
|
||||
}
|
||||
if (yaml.contains("colors")) {
|
||||
const auto& c = yaml["colors"];
|
||||
readArgb(c, "border", border_argb_);
|
||||
readArgb(c, "picker_bg", picker_bg_argb_);
|
||||
readArgb(c, "picker_hover", picker_hover_argb_);
|
||||
}
|
||||
|
||||
std::cout << "EditorConfig: carregat des de " << file_path << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "EditorConfig: error parsejant " << file_path << ": " << e.what() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
#endif // _DEBUG
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// Configuració visual de l'editor de tilemap (Debug-only). Carregada des
|
||||
// de data/editor/editor.yaml; les claus absents cauen als valors compilats
|
||||
// a Defaults::Editor. Mateix patró que UiConfig (Meyers singleton + load).
|
||||
class EditorConfig {
|
||||
public:
|
||||
static auto get() -> EditorConfig&;
|
||||
|
||||
EditorConfig(const EditorConfig&) = delete;
|
||||
EditorConfig(EditorConfig&&) = delete;
|
||||
auto operator=(const EditorConfig&) -> EditorConfig& = delete;
|
||||
auto operator=(EditorConfig&&) -> EditorConfig& = delete;
|
||||
|
||||
void load(const std::string& file_path);
|
||||
|
||||
[[nodiscard]] auto pickerMargin() const -> int { return picker_margin_; }
|
||||
[[nodiscard]] auto pickerSpacing() const -> int { return picker_spacing_; }
|
||||
[[nodiscard]] auto borderArgb() const -> std::uint32_t { return border_argb_; }
|
||||
[[nodiscard]] auto pickerBgArgb() const -> std::uint32_t { return picker_bg_argb_; }
|
||||
[[nodiscard]] auto pickerHoverArgb() const -> std::uint32_t { return picker_hover_argb_; }
|
||||
|
||||
private:
|
||||
EditorConfig();
|
||||
~EditorConfig() = default;
|
||||
|
||||
int picker_margin_;
|
||||
int picker_spacing_;
|
||||
std::uint32_t border_argb_;
|
||||
std::uint32_t picker_bg_argb_;
|
||||
std::uint32_t picker_hover_argb_;
|
||||
};
|
||||
|
||||
#endif // _DEBUG
|
||||
@@ -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};
|
||||
};
|
||||
@@ -10,6 +10,10 @@ FloorTrap::FloorTrap(const fkyaml::node& yaml) {
|
||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("floor_trap");
|
||||
collides_ = false;
|
||||
interactable_ = false;
|
||||
// Trigger d'àrea: la trampa no bloqueja el pas però vol rebre
|
||||
// `onPlayerContact` quan el jugador la solapa. Tanca el gap latent
|
||||
// que la deixava muda (`checkPlayerCollisions` filtrava pel `collides`).
|
||||
triggers_on_overlap_ = true;
|
||||
|
||||
if (yaml.contains("cycle")) {
|
||||
const auto& c = yaml["cycle"];
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "game/entities/concrete/hole.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/player.hpp"
|
||||
|
||||
Hole::Hole(const fkyaml::node& yaml) {
|
||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("hole");
|
||||
// Sense xoc físic ni interacció — només trigger d'àrea. El bucle de
|
||||
// contacte ens admet via `triggers_on_overlap_=true`. Damage queda a
|
||||
// 0 (no publiquem `player_hit`); la mort la dispara `integrateJump`
|
||||
// del jugador quan supera `HOLE_FALL_DEPTH`.
|
||||
collides_ = false;
|
||||
interactable_ = false;
|
||||
triggers_on_overlap_ = true;
|
||||
// Decal de sòl: `Entity::getFeetY()` torna sentinel mínim si aquest
|
||||
// flag està actiu — així el forat es pinta abans que qualsevol actor.
|
||||
is_floor_decal_ = true;
|
||||
setDamage(0);
|
||||
}
|
||||
|
||||
void Hole::onPlayerContact(Player* player) {
|
||||
if (player == nullptr) { return; }
|
||||
// Només mossega quan el jugador trepitja el forat de peu (GROUNDED).
|
||||
// En l'aire, el filtre d'altura del bucle ja l'ha rebutjat (height_=0
|
||||
// i jump_z_>0 prou per superar-ho); però mantenim el gat per defensa
|
||||
// davant de futurs canvis al filtre.
|
||||
if (player->getState() != Player::State::GROUNDED) { return; }
|
||||
|
||||
// Re-verificació estricta: la hit-box interna del jugador (8×8) ha
|
||||
// d'estar COMPLETAMENT continguda dins del rect del forat (16×16). El
|
||||
// bucle de col·lisió del manager ja ens ha cridat per qualsevol solape;
|
||||
// ací filtrem perquè la caiguda només es dispari quan els "dos peus"
|
||||
// estan dins del forat, no quan només la vora frega — sinó es percep
|
||||
// com un imant que succiona el jugador al centre.
|
||||
const SDL_FRect HIT = player->getHitBox();
|
||||
const SDL_FRect BOX = getCollisionRect();
|
||||
const bool CONTAINED = HIT.x >= BOX.x && HIT.y >= BOX.y && HIT.x + HIT.w <= BOX.x + BOX.w && HIT.y + HIT.h <= BOX.y + BOX.h;
|
||||
if (!CONTAINED) { return; }
|
||||
|
||||
// Snap al centre XY del forat + Y de la base del tile (per al clip
|
||||
// rect del sprite mentre cau). `enterHoleFall` rep coordenades
|
||||
// absolutes en el canvas — el player s'encarrega de convertir a la
|
||||
// seua convenció (cantó superior-esquerra del sprite).
|
||||
const float CENTER_X = BOX.x + (BOX.w * 0.5F);
|
||||
const float CENTER_Y = BOX.y + (BOX.h * 0.5F);
|
||||
const float FLOOR_Y = BOX.y + BOX.h;
|
||||
player->enterHoleFall(CENTER_X, CENTER_Y, FLOOR_Y);
|
||||
}
|
||||
|
||||
REGISTER_ENTITY("hole", Hole)
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/hazard.hpp"
|
||||
|
||||
// Forat al sòl: hazard estàtic de 16×16 que NO bloqueja el pas però
|
||||
// detecta quan el jugador hi cau a sobre. Característiques:
|
||||
// - `collides_=false` + `triggers_on_overlap_=true`: rep `onPlayerContact`
|
||||
// pel bucle de `EntityManager::checkPlayerCollisions` sense bloquejar
|
||||
// el moviment del jugador.
|
||||
// - `height_=0`: el filtre per altura del bucle (`jump_z > height`) NO
|
||||
// s'activa amb height a zero, així que el bucle de contacte SÍ crida
|
||||
// `onPlayerContact` també per a jugadors AIRBORNE. Qui rebutja els
|
||||
// estats distints de GROUNDED (saltant, caient, HURT, DYING...) és el
|
||||
// guard explícit dins de `Hole::onPlayerContact`. El filtre d'altura
|
||||
// és una optimització ortogonal del bucle, no la defensa principal.
|
||||
// - Damage 0: el contacte no publica `player_hit`. En lloc d'això,
|
||||
// `onPlayerContact` invoca `Player::enterHoleFall`, que transiciona
|
||||
// el jugador a AIRBORNE amb subestat `falling_through_hole_`.
|
||||
// - Verificació amb hit-box interna: `onPlayerContact` re-verifica
|
||||
// l'overlap amb `Player::getHitBox()` (8×8 centrada al sprite),
|
||||
// així només es dispara quan el centre del jugador entra al tile.
|
||||
// - Z-order: `is_floor_decal_=true` (heretat) — `Entity::getFeetY()`
|
||||
// retorna sentinel mínim, així el forat es pinta abans que qualsevol
|
||||
// actor i les momies passen per davant. L'efecte "el sòl es traga el
|
||||
// jugador" no es fa amb Y-sort: el `Player::render` aplica un clip
|
||||
// rect mentre `falling_through_hole_=true` que retalla el sprite per
|
||||
// sota de la base del tile del forat.
|
||||
class Hole : public StaticHazard {
|
||||
public:
|
||||
explicit Hole(const fkyaml::node& yaml);
|
||||
|
||||
// No publica `player_hit`. En el seu lloc, dispara
|
||||
// `Player::enterHoleFall(center_x, center_y, floor_y)` perquè el
|
||||
// jugador armi el clip rect a la Y de la base del forat.
|
||||
void onPlayerContact(Player* player) override;
|
||||
};
|
||||
|
||||
// TODO(hole-feature): afegir reset de `falling_through_hole_` i `hole_clip_bottom_y_` a `Player::disable()` per simetria.
|
||||
// TODO(hole-feature): afegir tests dedicats del comportament de la mòmia amb forats (no requereixen Player real).
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/entity_manager.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
#include "game/entities/player.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
@@ -26,11 +28,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 +49,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 +81,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 {
|
||||
@@ -165,7 +199,15 @@ void Mummy::snapToPlayfieldEdge(float dx, float dy) {
|
||||
}
|
||||
|
||||
void Mummy::step(float delta_time) {
|
||||
// Desplaçament nominal en aquest frame segons la direcció actual.
|
||||
// Detecció reactiva al canvi de tile: si la mòmia ha entrat aquest
|
||||
// tick a un tile nou amb un decal trigger (Hole), dispara mitja volta
|
||||
// abans de calcular el desplaçament — així el frame ja consumeix la
|
||||
// nova direcció sense esperar al següent. La primera crida només
|
||||
// registra el tile de spawn (lazy init).
|
||||
reactToCurrentTile();
|
||||
|
||||
// Desplaçament nominal en aquest frame segons la direcció actual
|
||||
// (potser ja invertida per `reactToCurrentTile`).
|
||||
const float DX = dir_x_ * speed_ * delta_time;
|
||||
const float DY = dir_y_ * speed_ * delta_time;
|
||||
|
||||
@@ -179,6 +221,9 @@ void Mummy::step(float delta_time) {
|
||||
// centre; el remanent queda per al pròxim frame, ja amb la (potser
|
||||
// nova) direcció aplicada. Així la caixa 16×16 queda sempre alineada
|
||||
// a la graella de corredors i els girs no xoquen per drift sub-píxel.
|
||||
// L'evitació de forats en pasillos rectes la cobreix
|
||||
// `reactToCurrentTile` (al començament d'aquest mètode), no aquesta
|
||||
// branca — aquí només es decideix entre direccions geomètriques.
|
||||
if (const auto SNAP = detectLaneCrossing(DX, DY); SNAP.has_value()) {
|
||||
if (DX != 0.0F) {
|
||||
pixel_rect_.x += *SNAP;
|
||||
@@ -201,14 +246,86 @@ void Mummy::step(float delta_time) {
|
||||
// Bloqueig per la vora del playfield: amarrem la caja a la vora exacta.
|
||||
if (!Map::insidePlayfield(CANDIDATE)) { snapToPlayfieldEdge(DX, DY); }
|
||||
|
||||
// Cal girar. Preferim perpendiculars; només fem U-turn si no hi ha cap
|
||||
// altra opció (comportament típic d'Oh Mummy!: no es recula sense motiu).
|
||||
const auto POOLS = collectDirectionPools(speed_ * delta_time);
|
||||
const auto& pool = !POOLS.perp.empty() ? POOLS.perp : POOLS.uturn;
|
||||
applyChoice(chooseDirection(pool));
|
||||
// Cal girar. Cascada de tres nivells:
|
||||
// Nivell 1 — perpendiculars sense decal (preferides; el comportament
|
||||
// "natural" d'Oh Mummy!: girar abans que recular).
|
||||
// Nivell 2 — mitja volta sense decal (rescat: callejón sense salida o
|
||||
// tots els girs porten a forat). Preferim recular abans
|
||||
// que caure al forat — un forat al costat NO ha de fer
|
||||
// que la mòmia salti per damunt seu cap a una perpendicular
|
||||
// amb forat només perquè la mitja volta era l'única neta.
|
||||
// Nivell 3 — pool sense filtre de decals (cas degenerat: rodejada de
|
||||
// forats). Permet caure al forat com a últim recurs; les
|
||||
// mòmies no moren pels forats, així que és comportament
|
||||
// tolerable en sales mal dissenyades.
|
||||
// TODO(refactor): mateixa cascada a `decideAtIntersection` amb la
|
||||
// variant "afegir recta a Nivell 1". Factoritzable
|
||||
// quan tinga sentit (3+ callsites).
|
||||
auto pools = collectDirectionPools(speed_ * delta_time, /*avoid_decals=*/true);
|
||||
const std::vector<Direction>* chosen = nullptr;
|
||||
if (!pools.perp.empty()) {
|
||||
chosen = &pools.perp;
|
||||
} else if (!pools.uturn.empty()) {
|
||||
chosen = &pools.uturn;
|
||||
}
|
||||
DirectionPools pools_unfiltered;
|
||||
if (chosen == nullptr) {
|
||||
pools_unfiltered = collectDirectionPools(speed_ * delta_time, /*avoid_decals=*/false);
|
||||
if (!pools_unfiltered.perp.empty()) {
|
||||
chosen = &pools_unfiltered.perp;
|
||||
} else if (!pools_unfiltered.uturn.empty()) {
|
||||
chosen = &pools_unfiltered.uturn;
|
||||
}
|
||||
}
|
||||
applyChoice((chosen != nullptr) ? chooseDirection(*chosen) : Direction::NONE);
|
||||
// No avancem aquest frame: la direcció nova s'aplicarà al pròxim step.
|
||||
}
|
||||
|
||||
void Mummy::reactToCurrentTile() {
|
||||
// Calculem el tile que toca la **vora davantera** de la BOX en la
|
||||
// direcció de marxa, no el centre. Així la mitja volta es dispara
|
||||
// tan aviat com el primer píxel del cos de la mòmia entra al tile-
|
||||
// forat ("asoma el peu"), no quan ja hi ha mig cos a dins. Per a una
|
||||
// direcció positiva en un eix, la vora davantera és el costat major
|
||||
// de l'AABB (`x + w - 1`, exclusive→inclusive convention en píxels);
|
||||
// per a una direcció negativa, el menor (`x`); per a zero, qualsevol
|
||||
// (no hi ha avenç en aquest eix).
|
||||
const auto BOX = getCollisionRect();
|
||||
const float LEAD_X = (dir_x_ > 0.0F) ? (BOX.x + BOX.w - 1.0F) : BOX.x;
|
||||
const float LEAD_Y = (dir_y_ > 0.0F) ? (BOX.y + BOX.h - 1.0F) : BOX.y;
|
||||
const auto [CUR_COL, CUR_ROW] = Map::pixelToTile(LEAD_X, LEAD_Y);
|
||||
|
||||
// Sense canvi de tile no hi ha res a fer — això filtra també tots els
|
||||
// ticks intra-tile (la majoria, ~16 frames per tile a velocitats típiques).
|
||||
if (CUR_COL == last_tile_col_ && CUR_ROW == last_tile_row_) { return; }
|
||||
|
||||
// Primera crida després del spawn: registrem el tile sense reaccionar.
|
||||
// Així no disparem mitja volta espuria si la mòmia spawneja sobre un
|
||||
// decal (defensa contra mapes mal dissenyats) o si pixel_rect_ encara
|
||||
// no estava posat al constructor.
|
||||
const bool FIRST_TIME = (last_tile_col_ == INT_MIN || last_tile_row_ == INT_MIN);
|
||||
last_tile_col_ = CUR_COL;
|
||||
last_tile_row_ = CUR_ROW;
|
||||
if (FIRST_TIME) { return; }
|
||||
|
||||
// Sense manager injectat (smoke tests), no podem consultar decals.
|
||||
if (entity_manager_ == nullptr) { return; }
|
||||
|
||||
// Si el tile nou conté un decal trigger (Hole o futur pinchos plans,
|
||||
// plaques, etc.), mitja volta immediata. `changeDirection` també
|
||||
// selecciona l'animació cardinal corresponent.
|
||||
const auto TS = static_cast<float>(Map::tile_size);
|
||||
const SDL_FRect TILE_RECT{
|
||||
.x = (static_cast<float>(CUR_COL) * TS) + static_cast<float>(Map::tilemap_origin_x),
|
||||
.y = (static_cast<float>(CUR_ROW) * TS) + static_cast<float>(Map::tilemap_origin_y),
|
||||
.w = TS,
|
||||
.h = TS,
|
||||
};
|
||||
if (entity_manager_->findFloorTriggerAt(TILE_RECT) != nullptr) {
|
||||
changeDirection(-dir_x_, -dir_y_);
|
||||
}
|
||||
}
|
||||
|
||||
auto Mummy::detectLaneCrossing(float dx, float dy) const -> std::optional<float> {
|
||||
const auto BOX = getCollisionRect();
|
||||
if (dx != 0.0F) {
|
||||
@@ -218,44 +335,72 @@ 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;
|
||||
}
|
||||
|
||||
void Mummy::decideAtIntersection(float step_px) {
|
||||
// Pool base: perpendiculars lliures (collectDirectionPools ja exclou la
|
||||
// direcció actual i la mitja volta va al seu propi calaix, que ací
|
||||
// ignorem — mai fem U-turn a un creuament).
|
||||
auto pools = collectDirectionPools(step_px);
|
||||
std::vector<Direction> pool = std::move(pools.perp);
|
||||
// Cascada de tres nivells (paral·lela a la branca de bloqueig de
|
||||
// `step`). Aquí el Nivell 1 inclou la recta com a candidata addicional
|
||||
// (a un creuament la mòmia pot continuar recta si segueix lliure i no
|
||||
// apunta a un decal); el Nivell 2 admet la mitja volta — abans no es
|
||||
// permetia mai en creuaments, però és preferible recular que caure al
|
||||
// forat quan totes les altres surten a decal.
|
||||
// TODO(refactor): cascada paral·lela a la branca de bloqueig de `step`.
|
||||
auto pools = collectDirectionPools(step_px, /*avoid_decals=*/true);
|
||||
|
||||
// Afegim la recta si segueix essent lliure. A un creuament "net" ho és
|
||||
// sempre (la mòmia hi ha arribat per aquest corredor), però als creua-
|
||||
// ments de la vora del playfield la recta pot estar al límit i no
|
||||
// permetre un pas més — en aquest cas la descartem.
|
||||
// Nivell 1: perpendiculars (filtrades) + recta si lliure i sense decal.
|
||||
std::vector<Direction> level1 = std::move(pools.perp);
|
||||
const auto STRAIGHT_BOX = candidateBox(dir_x_ * step_px, dir_y_ * step_px);
|
||||
if (Map::insidePlayfield(STRAIGHT_BOX) && !Map::collides(STRAIGHT_BOX)) {
|
||||
pool.push_back(currentDirection());
|
||||
const bool STRAIGHT_OPEN = Map::insidePlayfield(STRAIGHT_BOX) && !Map::collides(STRAIGHT_BOX);
|
||||
if (STRAIGHT_OPEN && !isDecalAheadInDir(dir_x_, dir_y_)) {
|
||||
level1.push_back(currentDirection());
|
||||
}
|
||||
|
||||
if (pool.empty()) { return; }
|
||||
applyChoice(chooseDirection(pool));
|
||||
// Nivell 2: mitja volta sense decal — rescat quan Nivell 1 ja no té cap
|
||||
// opció neta. Un forat al final del passadís ha de fer recular la
|
||||
// mòmia, no convertir-se en l'opció triada per defecte.
|
||||
const std::vector<Direction>* chosen = nullptr;
|
||||
if (!level1.empty()) {
|
||||
chosen = &level1;
|
||||
} else if (!pools.uturn.empty()) {
|
||||
chosen = &pools.uturn;
|
||||
}
|
||||
|
||||
// Nivell 3: cas degenerat (rodejada de forats). Pool sense filtre amb
|
||||
// perpendiculars, recta si lliure i mitja volta. Permet caure al forat
|
||||
// com a últim recurs.
|
||||
DirectionPools pools_unfiltered;
|
||||
std::vector<Direction> level3;
|
||||
if (chosen == nullptr) {
|
||||
pools_unfiltered = collectDirectionPools(step_px, /*avoid_decals=*/false);
|
||||
level3 = std::move(pools_unfiltered.perp);
|
||||
if (STRAIGHT_OPEN) { level3.push_back(currentDirection()); }
|
||||
if (!level3.empty()) {
|
||||
chosen = &level3;
|
||||
} else if (!pools_unfiltered.uturn.empty()) {
|
||||
chosen = &pools_unfiltered.uturn;
|
||||
}
|
||||
}
|
||||
|
||||
if (chosen == nullptr) { return; }
|
||||
applyChoice(chooseDirection(*chosen));
|
||||
}
|
||||
|
||||
auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
||||
auto Mummy::collectDirectionPools(float step, bool avoid_decals) const -> DirectionPools {
|
||||
struct Candidate {
|
||||
float dx;
|
||||
float dy;
|
||||
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) {
|
||||
@@ -265,6 +410,11 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
||||
if (IS_CURRENT) { continue; }
|
||||
const auto TEST = candidateBox(c.dx, c.dy);
|
||||
if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; }
|
||||
// Filtre de decals (Hole i futurs decals trigger): comprova el
|
||||
// tile cardinal contigu; si conté un trigger, descartem la
|
||||
// direcció. El caller pot demanar `avoid_decals=false` com a
|
||||
// fallback quan totes les filtrades estan bloquejades.
|
||||
if (avoid_decals && isDecalAheadInDir(c.dx, c.dy)) { continue; }
|
||||
const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F);
|
||||
if (IS_UTURN) {
|
||||
pools.uturn.push_back(c.dir);
|
||||
@@ -275,18 +425,53 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
||||
return pools;
|
||||
}
|
||||
|
||||
auto Mummy::isDecalAheadInDir(float dx_signed, float dy_signed) const -> bool {
|
||||
// Sense manager injectat (smoke tests sense `EntityManager::add`), el
|
||||
// filtre és no-op: la mòmia es comporta exactament com abans del feature.
|
||||
if (entity_manager_ == nullptr) { return false; }
|
||||
if (dx_signed == 0.0F && dy_signed == 0.0F) { return false; }
|
||||
// El rect a consultar és el AABB del tile cardinal contigu (mida
|
||||
// `Map::tile_size`), ancorat al tile actual de la mòmia. Calculem el
|
||||
// tile a partir del **centre** de la BOX, no de la cantonada superior-
|
||||
// esquerra: amb una BOX de mida `tile_size`, la cantonada esquerra
|
||||
// entra al tile següent un sub-píxel abans que el cos de la mòmia hi
|
||||
// sigui majoritàriament. Si féssim servir `BOX.x` directament, una
|
||||
// mòmia a `BOX.x = 199.5` (cobrint [199.5, 215.5], 0.5 px al tile 12
|
||||
// i 15.5 px al tile 13) seria classificada al tile 12 i giraria un
|
||||
// tile abans del veritablement adjacent al decal.
|
||||
const auto BOX = getCollisionRect();
|
||||
const auto TS = static_cast<float>(Map::tile_size);
|
||||
const auto SIGN_OF = [](float v) -> float {
|
||||
if (v > 0.0F) { return 1.0F; }
|
||||
if (v < 0.0F) { return -1.0F; }
|
||||
return 0.0F;
|
||||
};
|
||||
const float CENTER_X = BOX.x + (BOX.w * 0.5F);
|
||||
const float CENTER_Y = BOX.y + (BOX.h * 0.5F);
|
||||
const auto [CUR_COL, CUR_ROW] = Map::pixelToTile(CENTER_X, CENTER_Y);
|
||||
const float TILE_X = (static_cast<float>(CUR_COL) * TS) + static_cast<float>(Map::tilemap_origin_x);
|
||||
const float TILE_Y = (static_cast<float>(CUR_ROW) * TS) + static_cast<float>(Map::tilemap_origin_y);
|
||||
const SDL_FRect NEXT{
|
||||
.x = TILE_X + (SIGN_OF(dx_signed) * TS),
|
||||
.y = TILE_Y + (SIGN_OF(dy_signed) * TS),
|
||||
.w = TS,
|
||||
.h = TS,
|
||||
};
|
||||
return entity_manager_->findFloorTriggerAt(NEXT) != nullptr;
|
||||
}
|
||||
|
||||
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 +481,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)
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
@@ -16,10 +17,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 +55,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;
|
||||
@@ -56,17 +86,31 @@ class Mummy : public DynamicHazard {
|
||||
// Recull les direccions alternatives viables des de la posició actual:
|
||||
// `perpendiculars` (girs) i `uturn` (inversa). La direcció actual s'exclou
|
||||
// perquè ja sabem que està bloquejada. Cada candidata es valida amb
|
||||
// `Map::insidePlayfield` + `Map::collides` sobre la caja resultant.
|
||||
// `Map::insidePlayfield` + `Map::collides` sobre la caja resultant. Si
|
||||
// `avoid_decals` és true i hi ha `EntityManager` injectat, també es
|
||||
// descarten les direccions que apunten a un tile amb un decal trigger
|
||||
// (Hole o futurs pinchos plans/charcos). El caller és responsable de
|
||||
// re-cridar amb `avoid_decals=false` com a fallback si totes les
|
||||
// direccions filtrades estan bloquejades.
|
||||
struct DirectionPools {
|
||||
std::vector<Direction> perp;
|
||||
std::vector<Direction> uturn;
|
||||
};
|
||||
[[nodiscard]] auto collectDirectionPools(float step) const -> DirectionPools;
|
||||
[[nodiscard]] auto collectDirectionPools(float step, bool avoid_decals = true) 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_).
|
||||
// True si el tile cardinal contigu en la direcció (signe de dx, signe
|
||||
// de dy) conté un decal trigger (`is_floor_decal_ && triggers_on_overlap_`)
|
||||
// segons `EntityManager::findFloorTriggerAt`. Captura `Hole` i qualsevol
|
||||
// futur decal trigger sense hardcodejar tipus. Si `entity_manager_` no
|
||||
// està injectat (smoke tests sense manager), retorna false silenciosament
|
||||
// — la mòmia es comporta com abans del feature.
|
||||
[[nodiscard]] auto isDecalAheadInDir(float dx_signed, float dy_signed) const -> bool;
|
||||
|
||||
// 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 +134,55 @@ 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;
|
||||
|
||||
// Detecció reactiva al canvi de tile (substitueix el lookahead sub-tile
|
||||
// entre creuaments). Es crida al principi de cada `step()`. Si el tile
|
||||
// que ocupa la mòmia (calculat des del centre de la BOX) ha canviat
|
||||
// respecte a l'última crida, comprova si el tile nou conté un decal
|
||||
// trigger; si el conté, dispara mitja volta immediata. La primera crida
|
||||
// (last_tile_col_ == INT_MIN) només registra el tile actual sense
|
||||
// reaccionar — evita un falç positiu si la mòmia spawneja sobre un
|
||||
// decal o si el setup encara no és estable.
|
||||
void reactToCurrentTile();
|
||||
|
||||
// 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};
|
||||
|
||||
// Tile que ocupava la mòmia a l'última crida a `reactToCurrentTile`.
|
||||
// Inicialitzats a `INT_MIN` perquè la primera crida (lazy init: no
|
||||
// podem fer-ho al constructor perquè `pixel_rect_` encara no té
|
||||
// posició; setPixelRect del manager arriba després) registra el tile
|
||||
// de spawn sense reaccionar.
|
||||
int last_tile_col_{INT_MIN};
|
||||
int last_tile_row_{INT_MIN};
|
||||
};
|
||||
@@ -10,6 +10,9 @@ WallTrap::WallTrap(const fkyaml::node& yaml) {
|
||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("wall_trap");
|
||||
collides_ = false;
|
||||
interactable_ = false;
|
||||
// Trigger d'àrea: igual que FloorTrap. Tanca el gap latent del filtre
|
||||
// de `checkPlayerCollisions` que ho deixava mut.
|
||||
triggers_on_overlap_ = true;
|
||||
|
||||
if (yaml.contains("cycle")) {
|
||||
const auto& c = yaml["cycle"];
|
||||
|
||||