Compare commits

...
55 Commits
Author SHA1 Message Date
JailDesigner 6f6aa182f7 Merge feat/control-remapping — redefinició de tecles i botons del mando 2026-04-29 19:58:25 +02:00
JailDesigner b65eeb7aaa chore: bump v0.30.0 — redefinició de controls 2026-04-29 19:58:20 +02:00
JailDesigner 37fdcb4751 feat(controls): redefinició de tecles i botons del mando des del menú de servei 2026-04-29 19:54:47 +02:00
JailDesigner 4739354875 feat(maps): tileset propi per piràmide + reload de tilemap a loadRoom 2026-04-29 13:30:39 +02:00
JailDesigner 2879cbdb17 merge feat/player-death-respawn — mort + respawn (DYING + death_spinning + cinemàtica aèria) 2026-04-29 12:16:48 +02:00
JailDesigner 38070b33aa feat(player): so de hurt al morir + respawn cinemàtic amb limbo invisible 2026-04-29 12:15:52 +02:00
JailDesigner 9f0a566ffb feat(player): mort i respawn (DYING + death_spinning + lives counter) 2026-04-29 11:40:05 +02:00
JailDesigner 6397f9d4eb Merge branch 'feat/animation-flip' — flip H/V al motor i cardinals N/S/E/W 2026-04-29 10:17:46 +02:00
JailDesigner 72384c3113 feat(anim): cardinal WEST i flip totalment governat pel motor d'animació 2026-04-29 10:11:27 +02:00
JailDesigner 9dc7ff0da3 refactor(anim): direccions cardinals (north/south/east) en YAML i codi 2026-04-29 08:59:01 +02:00
JailDesigner 1d43a77800 feat(anim): suport de flip H/V per frame i per animació 2026-04-29 08:30:08 +02:00
JailDesigner 39eda56c9d docs(player): neteja comentaris dels YAMLs (sense documentar el motor) 2026-04-28 20:31:35 +02:00
JailDesigner a351f920ba Merge branch 'chore/audio-reorg' — cicle de música per estat (INTRO/PLAYING/OUTRO) i sons de làtigo/HURT 2026-04-28 19:56:32 +02:00
JailDesigner 72642dc10e feat(player): so del swing del làtigo i de l'entrada a HURT 2026-04-28 19:55:53 +02:00
JailDesigner 404db74889 feat(audio): cicle de música per estat (INTRO/PLAYING/OUTRO) amb callback de fi i spawn escalonat 2026-04-28 19:41:38 +02:00
JailDesigner 91f664c4d5 chore: bump v0.29.0 — atac de làtigo (Player ATTACKING, Mummy HURT, hitbox amb retall) 2026-04-28 18:44:29 +02:00
JailDesigner ce55aeeadc Merge branch 'feat/whip-attack' — atac de làtigo (Player ATTACKING, Mummy HURT, hitbox amb retall) 2026-04-28 18:44:13 +02:00
JailDesigner 8f66961c7b feat(whip): separa draw_rect i damage_rect; sprites 80×32 / 16×96; feet_y delegat a l'owner; recover_* per al cooldown 2026-04-28 18:28:19 +02:00
JailDesigner 121558b1fd test(whip): smokes de hitbox, dany a mòmia i alreadyHit; tests de Mummy HURT 2026-04-28 16:20:22 +02:00
JailDesigner df60709e6d feat(whip): aplicació de dany amb retall de hitbox, alreadyHit i friendly fire 2026-04-28 16:20:22 +02:00
JailDesigner d208ba02df feat(player): estat ATTACKING amb edge-detect, animacions i spawn del whip via bus 2026-04-28 16:20:22 +02:00
JailDesigner 5db3bb0bd0 feat(mummy): estat HURT amb fugida en línia recta i recuperació al timer 2026-04-28 16:20:22 +02:00
JailDesigner 9ad21eb5bb feat(attack): esquelet WhipAttack + canal d'spawn via EventBus 2026-04-28 16:20:22 +02:00
JailDesigner 4752eb8ad7 build: silencia warning de sprintf deprecat a stb_image_write 2026-04-28 12:21:33 +02:00
JailDesigner 887b8f3aa3 build: evita crear fitxer 'NUL' a Windows quan Make corre dins MSYS/MinGW 2026-04-28 09:44:34 +02:00
JailDesigner 2f3643f09c build: silencia warnings de stb a source/external/ via set_source_files_properties 2026-04-28 09:38:52 +02:00
JailDesigner cc529afeb2 build: detecta Windows i tria generador CMake (Ninja o MinGW Makefiles) 2026-04-28 08:17:49 +02:00
JailDesigner 15a7c4e18a feat(audio): música només a PLAYING; so del rètol al final del fade-in d'INTRO 2026-04-27 08:48:01 +02:00
JailDesigner 7d725f99f4 Merge branch 'feat/sound-effects' — efectes d'àudio (eco/reverb) i reverb d'INTRO 2026-04-27 08:33:38 +02:00
JailDesigner e9ef6565a7 chore: bump v0.28.0 — efectes d'àudio (eco/reverb) i reverb a la cinemàtica d'INTRO 2026-04-27 08:33:34 +02:00
JailDesigner 47bb92144e feat(audio): reverb 'stone_hall' al primer LAND cinemàtic d'INTRO
Lligua el reverb al moment de la cinemàtica d'entrada de sala, no als
salts en general. La sala dóna "més detall" sonor a l'INTRO; un cop el
joc està en marxa, els salts (i els respawns post-mort) tornen al so
sec. Així el primer aterratge es percep com a part de l'animació
d'arribada, no com una propietat permanent del personatge.

  - RoomFsm::Config: nou camp `intro_land_reverb_preset` carregat des
    de `room_intro.land_reverb` a `data/scenes/game.yaml` (per defecte
    'stone_hall'). Buit = sense reverb a l'INTRO.
  - Player: membre `next_land_reverb_preset_` consumit per
    `integrateJump` al primer aterratge que el trobe armat. Setter
    públic `armNextLandReverb(preset)` per a GameScene. Cap altre punt
    (respawn, exit, disable) el toca: el reverb és estrictament
    one-shot lligat al spawn aeri d'INTRO.
  - GameScene::triggerPlayerSpawn: arma el preset al jugador just
    després de `spawnFromAir`, així cada slot rep el seu reverb un
    sol cop per cinemàtica.
  - JumpConfig perd `land_reverb_preset` i els YAMLs de Sam/Morcus
    perden `land_reverb`: la decisió ja no viu al personatge.
2026-04-27 08:31:51 +02:00
JailDesigner d3698a1504 feat(audio): suport d'efectes (eco i reverb) per a sons puntuals
Afig una capa d'efectes oberta a la unitat d'àudio. Els sons puntuals
es poden reproduir processats per un eco o un reverb (Freeverb 8C/4AP)
sense afectar la música ni canviar el camí ràpid existent (playSound
sense efecte segueix sent una sola crida sense cost extra).

  - data/sounds.yaml: catàleg de presets nominats (echo: cave/short,
    reverb: stone_hall/small_room).
  - core/audio/audio_effects: processadors purs que prenen un Ja::Sound
    (U8/S16, mono o estèreo) i tornen PCM S16 amb la cua de l'efecte
    (cap a 800 ms eco, 1500 ms reverb).
  - core/audio/sound_effects_config: Meyers singleton que parseja
    sounds.yaml a EchoParams/ReverbParams indexats per nom; l'API
    pública d'Audio fa el lookup per nom.
  - jail_audio::Engine: nou camí playSoundWith{Echo,Reverb} que empenta
    el buffer ja processat a un canal lliure marcat com a has_effect.
    El cap MAX_EFFECT_CHANNELS limita a 4 sons amb efecte simultanis;
    si està al límit, l'engine retorna -1 i Audio cau a la versió seca.
  - Audio::playSoundWith{Echo,Reverb}(name, preset, group) com a única
    via pública. Si el preset no existeix, fallback dry amb avís.
  - director: load("data/sounds.yaml") abans d'Audio::init.
2026-04-27 08:16:16 +02:00
JailDesigner 42062dca55 Merge branch 'chore/room-intro-polish' — polish d'intro/outro i spawn delay BOTH 2026-04-27 08:10:24 +02:00
JailDesigner 5502191725 chore: bump v0.27.0 — polish d'intro/outro, panell translúcid i spawn delay per a BOTH 2026-04-27 08:10:18 +02:00
JailDesigner 8e2865efbd feat(intro): delay de spawn independent per al segon jugador en mode BOTH 2026-04-26 18:55:30 +02:00
JailDesigner 99eb42b319 refactor(game-scene): extreu helpers de renderRoomText i elimina el NOLINT 2026-04-26 18:44:49 +02:00
JailDesigner 8243e95ef1 feat(intro/outro): panel translúcid sota el rètol amb easing del text 2026-04-26 18:41:17 +02:00
JailDesigner 0752df0f62 feat(intro/outro): rètol centrat sobre el playfield i ajustos de timings/colors 2026-04-26 18:08:02 +02:00
JailDesigner 98303f7ce8 feat(outro): pausa en negre opac entre sales (5a fase de OUTRO)
Nova fase 4 a ROOM_OUTRO: black_hold. Va just després del fade_out;
durant aquesta fase la pantalla queda completament en negre opac i
la FSM espera abans de transitar a INTRO. Permet donar respir entre
el final del rètol Fase/Completada i el rellançament del Piramide/Sala
de la sala següent, sense necessitat d'un estat nou.

Implementacio:
- RoomFsm::Config guanya outro_black_hold_ms (default 0 — backward
  compatible). YAML key room_outro.black_hold_ms al data/scenes/game.yaml
  (default visible 500 ms; lo afines sense recompilar).
- L'array DURS de l'OUTRO passa de 4 a 5 elements; el while-loop
  consumeix la fase exactament igual.
- renderRoomFade detecta fase 4 i dibuixa alpha=255 fix; sense aquesta
  branca, el fade s'apagaria a final de la fase 3 i el món tornaria
  a ser visible durant la pausa.
- renderRoomText no canvia: phases 3 i 4 ja queden fora del rang
  0..2 on dibuixa el rètol.

Backward compat: black_hold_ms=0 fa que el while-loop avance
immediatament la fase i recauen tots els tests existents (T7 i
companys) sense modificacions.
2026-04-26 17:39:02 +02:00
JailDesigner 7982df922b feat(outro): rètol "fase completada" en dues línies oposades
Mateixa tècnica que l'INTRO: la línia 1 ("Fase") entra per
l'esquerra, la línia 2 ("Completada") per la dreta, i surten cap
al costat oposat al d'entrada. Centrades verticalment al voltant
del CENTER_Y amb el gap configurable ( a
data/scenes/game.yaml; 0 per defecte, paral·lel a la del room_intro).

Locale: la clau game.room_complete es divideix en
game.room_complete_line1 / room_complete_line2 als ca.yaml i en.yaml
("Fase"/"Completada" i "Stage"/"Complete"). Cada línia es
tradueix per separat — útil si demà voleu paraules de longitud o
ordre distint en altres idiomes.
2026-04-26 17:28:07 +02:00
JailDesigner 8da18a6a86 feat(debug): tecla Keypad 1 per forçar l'obertura de la porta
Hook només Debug que crida EntityManager::forceOpenDoor (nou helper
públic que delega a Door::open). El flux posterior — creuar la porta,
player_exited, level_complete, transició a OUTRO — funciona idèntic
al del joc real, perquè depèn de la Door, no del comptador de
sarcòfags. Útil per saltar la mecànica de pintat/sarcs durant proves
de progressió de sales.

Tecla configurable a data/input/keys.yaml > id: open_door
(per defecte Keypad 1). Mateixos gates que els salts [/]: ignora la
pulsació si la pausa està activa o l'editor obert. En Release la
ENTRADA del YAML segueix carregant-se però no té consumidor.
2026-04-26 17:20:12 +02:00
JailDesigner 3b05704e97 feat(intro): colors del rètol independents + restitueix line_gap_px
Dues claus noves a data/ui.yaml > colors:
  room_text         (relleno, default 232)
  room_text_outline (contorn, default 255)
Abans el relleno reciclava notification_text (compartit amb les
notificacions del HUD) i el contorn estava hardcodejat a 255 dins
renderRoomText. Ara són canals separats: tocar el rètol no afecta
les notificacions i tot viu al YAML.

UiConfig guanya roomText() / roomTextOutline(); Defaults::UI hi posa
els fallbacks; renderRoomText els consumeix.

Drive-by: torna a posar la clau room_intro.line_gap_px: 8 al
data/scenes/game.yaml — s'havia perdut en una edició manual del
fitxer per afinar timings.
2026-04-26 17:02:26 +02:00
JailDesigner 00594d9eb2 feat(intro): línies del rètol travessen en direccions oposades
compute_x guanya un paràmetre from_right que decideix per quin costat
entra i per quin surt. La línia de la piràmide es queda amb el patró
clàssic (entra per l'esquerra, surt per la dreta); la de la sala s'hi
inverteix (entra per la dreta, surt per l'esquerra). El text de
l'OUTRO continua amb la direcció original.
2026-04-26 16:59:01 +02:00
JailDesigner af25a31639 feat(intro): font dedicada (CaesarDressing) i separació vertical entre línies
Nova font del rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO):
CaesarDressing-Regular, vista com a fonts.room_text al data/ui.yaml i
exposada per UiConfig::roomTextFont(). Director la preloda al costat
de la del HUD i la del placeholder; GameScene construeix
room_text_font_ paral·lel a pause_font_ i renderRoomText l'usa per a
les dues línies de l'INTRO i el text de l'OUTRO. La pausa segueix
amb la font placeholder antiga — no es barregen.

Espai vertical configurable: nova clau room_intro.line_gap_px al
data/scenes/game.yaml (default 8 px). El gap es suma al lineHeight
natural del font i es reparteix simètricament cap amunt i cap avall
del CENTER_Y, deixant el bloc de dues línies sempre centrat. Es
poden afinar des del YAML sense recompilar.
2026-04-26 15:40:09 +02:00
JailDesigner f52e7f4682 Merge branch 'feat/level-manager' — LevelManager i progressió sala a sala 2026-04-26 15:29:34 +02:00
JailDesigner e2f6d326aa feat(level-manager): HUD i text d'INTRO segueixen el LevelManager
HUD: la GameScene crida hud_->setPyramidRoom amb els valors del
LevelManager al ctor i a cada loadRoom — el marcador deixa de
mostrar 1/1 fix i s'actualitza en avançar de sala, en saltar amb
[/] i en arrencar amb starting_level del debug.yaml.

INTRO a dues línies: substitueix la clau game.room_intro per
game.room_intro_pyramid ("Piramide {p}") i game.room_intro_room
("Sala {r}") als locale ca/en. RoomFsm::formatRoomTitle(int)
desapareix i el reemplacen formatIntroPyramidLine(int) +
formatIntroRoomLine(int). renderRoomText es refactoritza per
dibuixar les dues línies (piràmide damunt, sala davall) amb
writeDX independents — comparteixen animació ara mateix però el
disseny ja les deixa preparades per ser desacoblades en el futur.

Drive-by: elimina el membre room_index_ de GameScene; les tres
ubicacions que el sincronitzaven amb el LevelManager (ctor,
advanceToNextRoom, salt debug) ja no fan falta — renderRoomText
i el HUD pregunten directament al singleton, evitant qualsevol
mirror local desincronitzable.

Test: T9/T10 actualitzats per a les dues noves formatadores.
2026-04-26 15:25:22 +02:00
JailDesigner b18bd4d021 chore: bump v0.26.0 — LevelManager i progressió sala a sala
Tanca la sèrie d'escalfament de la rama feat/level-manager:
- Levels::LevelManager com a font de veritat de la progressió
  (currentPath / advance / setIndex / setStartingPoint).
- 30 sales a data/maps/p<N>/r<M>.yaml + index a data/maps/levels.yaml.
- Avanç automàtic en creuar la porta + bucle al final de la llista.
- Hotkeys debug [ / ] per saltar entre sales.
- Clau opcional starting_level al debug.yaml.
- L'editor salva sobre la sala actualment carregada.

L'esquelet ja és funcional; queda pendent dissenyar les 30 sales,
canviar tilesets/música per piràmide i decidir la pantalla de fi de
joc (ara mateix fa bucle a la primera).
2026-04-26 15:09:22 +02:00
JailDesigner 738cde12bb feat(debug): starting_level a debug.yaml
Nova clau opcional al debug.yaml:
    starting_level: { pyramid: 2, room: 3 }
DebugConfig::resolveStartingLevel retorna optional<StartingLevel>; el
Director, abans de crear el GameScene, demana al LevelManager
setStartingPoint(pyramid, room) — que cerca l'entrada al levels.yaml
i hi posiciona l'índex. Si la clau no està (cas habitual) o
(pyramid, room) no existeix, la progressió arrenca a la primera sala
amb un avís a stderr. createDefaultIfMissing documenta la nova clau
sense escriure-la (la deixem opcional).

Drive-by: NOLINT puntual al capture de STARTING_PLAYERS al Director.
És pre-existent i s'ha activat ara perquè el commit toca director.cpp;
la captura és imprescindible en Debug (valor runtime), clang la veu
redundant en Release on és constexpr literal.
2026-04-26 15:07:34 +02:00
JailDesigner b147590a39 feat(level-manager): salts debug entre sales (només Debug)
Tecles [ i ] al keys.yaml (prev_room / next_room) que mouen
LevelManager::setIndex en ±1 i recarreguen la sala via loadRoom +
enterRoomIntro. Ignorades si la pausa està activa o l'editor obert
(qualsevol dels dos invalida l'estat editat).

Refactor de la FSM al mateix pas: l'avanç de sala (LevelManager +
loadRoom) ja no viu dins setState — el dispara updateRoomTransition
quan detecta el final de l'OUTRO via advanceToNextRoom. setState
torna a ser una transició pura, condició necessària perquè el salt
debug puga entrar a INTRO sense disparar un advance addicional.
Nou helper enterRoomIntro() reusable per la sortida de l'OUTRO i pels
salts; idempotent quan ja s'estava a INTRO.
2026-04-26 14:59:34 +02:00
JailDesigner d52cc1c509 feat(editor): el SAVE_PATH segueix la sala carregada
El writer del tilemap deixa de fer servir la constant
Defaults::Editor::SAVE_PATH (que apuntava a data/maps/pyramid.yaml) i
demana al LevelManager el path actual. Resultat: editar i guardar
desa la sala que tens davant — no l'antic mapa monolític — i salts
debug entre sales canviaran també el destí del save.
2026-04-26 14:54:54 +02:00
JailDesigner 9d4f92c98b feat(level-manager): avança a la sala següent al creuar la porta
Reemplaça resetRoom() per loadRoom(yaml_path) — la mateixa mecànica de
recàrrega però parametritzada amb el path de la nova sala. setState
de OUTRO → INTRO ara crida advanceToNextRoom(): demana al LevelManager
el següent nivell, sincronitza room_index_ amb el currentRoom() i
carrega via loadRoom. Si l'OUTRO arriba a l'última sala del levels.yaml,
fa loop a la primera (política temporal mentre no hi haja pantalla de
fi de joc). El comentari TODO de room_fsm.cpp queda actualitzat: la
FSM continua sent agnostica a quina sala ve, només marca el canvi
d'estat — l'itinerari el resol GameScene amb el LevelManager.
2026-04-26 14:53:07 +02:00
JailDesigner f51fe3d820 feat(level-manager): GameScene pren el mapa del LevelManager
defaultConfig() llig el path del nivell actual amb
Levels::LevelManager::get().currentPath() en lloc de la constant
Pyramid::MAP_YAML (que desapareix). El ctor sincronitza room_index_
amb LevelManager::currentRoom() per al text d'intro de sala. Encara
no hi ha progressió: cada arrencada continua a la primera sala del
levels.yaml; el comportament visible és idèntic, però la responsabilitat
de 'quin fitxer carreguem' ha passat al LevelManager.
2026-04-26 14:50:51 +02:00
JailDesigner fe17c35c67 feat(levels): LevelManager amb índex de sales
Nou singleton Levels::LevelManager que llig data/maps/levels.yaml a la
primera get() i exposa currentPath/currentPyramid/currentRoom/
currentLabel + advance/restart/setIndex. És pur estat 'on sóc' — no
orquestra cap reload; la GameScene i l'Editor li preguntaran al pas
següent. Afegida constant Defaults::Pyramid::LEVELS_YAML al costat de
la MAP_YAML antiga (la substitució completa va al Step 3).
2026-04-26 14:48:15 +02:00
JailDesigner 8fd06887d1 data: índex de nivells i camps pyramid/room a les 30 sales
Crea data/maps/levels.yaml com a font de veritat de la progressió (5
piràmides × 6 sales = 30 entrades ordenades) i afegeix els camps
pyramid/room a cada r<M>.yaml per a logs i text d'intro. El
LevelManager (a venir) consumirà aquesta llista; els fitxers de sala
encara són clons de pyramid.yaml — el contingut real el modificaré
després de tenir l'esquelet.
2026-04-26 14:45:36 +02:00
JailDesigner 580c13e854 feat: estat DISABLED i spawn aeri al començar la sala — v0.25.0 2026-04-26 14:17:17 +02:00
115 changed files with 10032 additions and 720 deletions

No files matched your search

+67 -2
View File
@@ -33,7 +33,10 @@ configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/versi
# Sources comuns del sistema d'entitats: s'afegeixen tant a l'executable
# principal com al smoke test. Mantenir-los en variable separada evita
# duplicació entre els dos targets.
set(ENTITY_SOURCES
# Entitats sense dependència directa amb Player.cpp: les enllacen tant
# l'executable principal com el test_entities_smoke (que evita arrossegar
# l'stack del jugador i les seues dependències pesades).
set(ENTITY_SOURCES_BASE
source/game/entities/entity.cpp
source/game/entities/animation_component.cpp
source/game/entities/event_bus.cpp
@@ -50,18 +53,29 @@ set(ENTITY_SOURCES
source/game/entities/concrete/dust.cpp
)
# Entitats que criden directament símbols de Player (com WhipAttack via
# `enterHurt`/`getBaseBox`): només van a l'executable principal.
set(ENTITY_SOURCES
${ENTITY_SOURCES_BASE}
source/game/entities/concrete/whip_attack.cpp
)
set(APP_SOURCES
source/main.cpp
source/core/system/director.cpp
source/core/audio/audio.cpp
source/core/audio/audio_adapter.cpp
source/core/audio/audio_effects.cpp
source/core/audio/jail_audio.cpp
source/core/audio/sound_effects_config.cpp
source/core/locale/locale.cpp
source/core/ui/ui_config.cpp
source/core/input/key_config.cpp
source/core/input/global_inputs.cpp
source/core/input/input_state.cpp
source/core/input/gamepad_manager.cpp
source/core/input/gamepad_bindings.cpp
source/core/input/define_buttons.cpp
source/core/input/mouse.cpp
source/core/rendering/screen.cpp
source/core/rendering/canvas.cpp
@@ -76,6 +90,7 @@ set(APP_SOURCES
source/core/rendering/sprite/sprite.cpp
source/core/rendering/sprite/moving_sprite.cpp
source/core/rendering/sprite/animated_sprite.cpp
source/core/rendering/sprite/animated_sprite_parser.cpp
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
source/core/rendering/sdl3gpu/viewport.cpp
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
@@ -101,6 +116,7 @@ set(APP_SOURCES
source/game/map/tilemap.cpp
source/game/map/tilemap_writer.cpp
source/game/map/paint_layer.cpp
source/game/levels/level_manager.cpp
# Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`).
# En Release el fitxer compila a un object buit i l'enllaçador no
# arrossega cap símbol del joc final.
@@ -163,6 +179,15 @@ endif()
add_executable(${PROJECT_NAME} ${APP_SOURCES})
# Silencia warnings de codi de tercers (mateixa filosofia que el
# .clang-tidy de source/external/). Només afecta aquests TUs concrets;
# la resta del codi continua compilant amb -Wall -Wextra -Wpedantic.
set_source_files_properties(
source/external/stb_image_write_impl.cpp
source/external/stb_vorbis_impl.cpp
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations"
)
if(NOT APPLE AND GLSLC_EXE)
add_dependencies(${PROJECT_NAME} shaders)
endif()
@@ -326,7 +351,7 @@ if(BUILD_TESTS)
source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp
source/game/map/map.cpp
${ENTITY_SOURCES}
${ENTITY_SOURCES_BASE}
)
target_include_directories(test_entities_smoke PRIVATE
"${CMAKE_SOURCE_DIR}/source"
@@ -339,6 +364,29 @@ if(BUILD_TESTS)
COMMAND test_entities_smoke
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
# Smoke del WhipAttack. Inclou tot l'stack d'entitats + whip_attack.cpp.
# `whip_attack_player_stubs.cpp` proveeix símbols mínims de Player per
# satisfer el linker sense arrossegar audio/gamepad/screen — els tests
# del whip s'enfoquen a hitbox + Mummy, no a friendly fire.
add_executable(test_whip_attack
tests/test_whip_attack.cpp
tests/whip_attack_player_stubs.cpp
source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp
source/game/map/map.cpp
${ENTITY_SOURCES_BASE}
source/game/entities/concrete/whip_attack.cpp
)
target_include_directories(test_whip_attack PRIVATE
"${CMAKE_SOURCE_DIR}/source"
"${CMAKE_BINARY_DIR}"
)
target_link_libraries(test_whip_attack PRIVATE SDL3::SDL3)
target_compile_options(test_whip_attack PRIVATE -Wall -Wextra -Wpedantic)
add_test(NAME whip_attack
COMMAND test_whip_attack
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
# Smoke del flux d'escenes i del parser de debug.yaml. Stand-alone:
# només enllaça els helpers purs (debug_config, placeholder_input,
# key_config) perquè la integració completa dels placeholders arrossegaria
@@ -399,4 +447,21 @@ if(BUILD_TESTS)
add_test(NAME game_fsm
COMMAND test_game_fsm
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
# Smoke del parser d'animacions amb suport de flip H/V. Stand-alone:
# només enllaça `animated_sprite_parser.cpp`, que és la part del motor
# que NO depèn de Resource::Cache ni de Surface — el parser és pure
# YAML-in / dades-out, així que no cal arrossegar tot l'stack.
add_executable(test_animated_sprite_flip
tests/test_animated_sprite_flip.cpp
source/core/rendering/sprite/animated_sprite_parser.cpp
)
target_include_directories(test_animated_sprite_flip PRIVATE
"${CMAKE_SOURCE_DIR}/source"
)
target_link_libraries(test_animated_sprite_flip PRIVATE SDL3::SDL3)
target_compile_options(test_animated_sprite_flip PRIVATE -Wall -Wextra -Wpedantic)
add_test(NAME animated_sprite_flip
COMMAND test_animated_sprite_flip
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
endif()
+42 -12
View File
@@ -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
# ==============================================================================
+26 -7
View File
@@ -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]
Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

+132
View File
@@ -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 # ·
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

+9
View File
@@ -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ó)"
+17 -4
View File
@@ -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"
+22 -5
View File
@@ -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"
+45
View File
@@ -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 }
+140
View File
@@ -0,0 +1,140 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 1
room: 1
tileset: data/tilesets/pyramid_1.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
# decideixen els límits del jugador i les seues caixes de col·lisió. Abans
# vivien com a `static constexpr` a map.hpp; ara cada piràmide pot definir
# la seua pròpia forma sense recompilar.
geometry:
grid_cols: 4
grid_rows: 5
sarc_w: 48
sarc_h: 16
pitch_x: 64
pitch_y: 32
hud_h: 24
back_wall_h: 24
side_wall_w: 24
# Columna central del tile transitable de la porta. La porta es renderitza
# com a sprite 48×32 (3 tiles d'amplada) centrat sobre aquesta columna; els
# 16 px centrals (just aquesta columna) són el "forat" del mur trasser per
# on el jugador surt de la sala. Les 2 columnes flanquejants segueixen sent
# mur visible. El spawn dels jugadors també s'alinea amb aquesta columna.
door_col: 10
data:
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
# Spawns d'entitats per a aquest nivell. Es llegeixen des de
# EntityManager::loadFromLevel al GameScene. Convencions:
# - sarcophagus → `sarc_position: [sarc_col, sarc_row]` en la graella de
# sarcòfags (4×5). EntityManager calcula el rect píxel via
# Map::getSarcRect i l'ancoratge de tile per al perímetre.
# - la resta (torch, door, mummy, rolling_rock, wall_trap, floor_trap)
# → `position: [tile_col, tile_row]` en coordenades del tilemap
# (origen `[-8, 0]` per defecte). Pixel = tile*16 + tilemap_origin.
#
# Dos sarcòfags en cantonades oposades → perímetres aïllats (cap tile en
# comú). La porta va al centre inferior, on el tilemap té els tiles
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
# superior esquerre per verificar render d'una entitat decorativa.
entities:
- type: torch
position: [4, 2]
- type: torch
position: [8, 2]
- type: torch
position: [12, 2]
- type: torch
position: [16, 2]
- type: sarcophagus
sarc_position: [0, 0]
- type: sarcophagus
sarc_position: [1, 0]
- type: sarcophagus
sarc_position: [2, 0]
- type: sarcophagus
sarc_position: [3, 0]
- type: sarcophagus
sarc_position: [0, 1]
- type: sarcophagus
sarc_position: [1, 1]
- type: sarcophagus
sarc_position: [2, 1]
- type: sarcophagus
sarc_position: [3, 1]
- type: sarcophagus
sarc_position: [0, 2]
- type: sarcophagus
sarc_position: [1, 2]
- type: sarcophagus
sarc_position: [2, 2]
- type: sarcophagus
sarc_position: [3, 2]
- type: sarcophagus
sarc_position: [0, 3]
- type: sarcophagus
sarc_position: [1, 3]
- type: sarcophagus
sarc_position: [2, 3]
- type: sarcophagus
sarc_position: [3, 3]
- type: sarcophagus
sarc_position: [0, 4]
- type: sarcophagus
sarc_position: [1, 4]
- type: sarcophagus
sarc_position: [2, 4]
- type: sarcophagus
sarc_position: [3, 4]
- type: door
# Sense `position`: la porta sempre viu al mur trasser. La columna
# transitable la dicta `geometry.door_col`; el render i la caixa
# de col·lisió es deriven d'aquesta dada al loadFromLevel.
- type: mummy
position: [10, 7]
- type: mummy
position: [7, 9]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+140
View File
@@ -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]
+1 -1
View File
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

+89 -92
View File
@@ -1,113 +1,97 @@
# player animation
sprite:
path: morcus.gif
size: [16, 32]
# Geometria de col·lisió del jugador (independent de l'animació):
# size → costat del quadrat de col·lisió (16×16)
# base_size → costat del quadrat visual inferior del sprite (els "peus");
# s'usa per centrar la caixa de col·lisió sobre ell.
# Geometria de col·lisió. `size` és la caixa contra murs/sarcs;
# `hit_size`, la caixa contra altres actors (més petita); `height`,
# l'alçada que un salt ha de superar per passar per damunt.
collision:
size: 16
base_size: 16
# Caixa de contacte actor-actor (jugador↔jugador, jugador↔momia/trampa).
# Més petita que `size` perquè els contactes d'acció siguin precisos;
# `size` es reserva per a la col·lisió amb murs/sarcòfags i per enfilar
# els passadissos. Centrada en el mateix punt que la caixa base.
hit_size: 8
# Alçada del cos en píxels. Funció DOBLE segons amb què col·lisione:
# · Contra entitats (mummy, rolling_rock, sarc): si `jump_z > height`
# d'aquella entitat, no hi ha col·lisió. Igual que als YAMLs
# d'entitats.
# · Contra altres jugadors: la col·lisió és binària per estat aeri.
# Si un està en l'aire i l'altre no → cap col·lisió. Si tots dos
# estan en l'aire, llavors `body_height` decideix si els rangs
# verticals solapen i s'aplica el rebot mid-aire. `0` = sense cos
# (ni rebot quan tots dos volen).
height: 12
# Animacions en mapa (clau = nom). Camps per animació:
# frames → llista d'índexs dins de l'spritesheet.
# frame_ms → durada de cada frame en ms. Escalar = uniforme; llista =
# per-frame (mateixa mida que `frames`).
# loop → true per defecte. `loop: false` = un sol tret (es queda al
# darrer frame i `animationIsCompleted()` passa a true).
# loop_from → rebota a aquest índex en passar del final (implica loop).
animations:
stand_down: { frames: [0] }
stand_up: { frames: [4] }
stand: { frames: [8] }
jump_down: { frames: [12] }
jump_up: { frames: [13] }
jump: { frames: [14] }
stun_down: { frames: [16] }
stun_up: { frames: [17] }
stun: { frames: [18] }
# Les variants `*_west` reaprofiten els frames de l'est i declaren
# `flip_h: true` perquè el motor d'animació apliqui el flip al sprite —
# el codi del joc no toca mai `flip_` directament.
stand_south: { frames: [0] }
stand_north: { frames: [4] }
stand_east: { frames: [8] }
stand_west: { frames: [8], flip_h: true }
walk_down:
frames: [0, 1, 2, 3]
frame_ms: 100
jump_south: { frames: [12] }
jump_north: { frames: [13] }
jump_east: { frames: [14] }
jump_west: { frames: [14], flip_h: true }
walk_up:
frames: [4, 5, 6, 7]
frame_ms: 100
stun_south: { frames: [16] }
stun_north: { frames: [17] }
stun_east: { frames: [18] }
stun_west: { frames: [18], flip_h: true }
walk:
frames: [8, 9, 10, 11]
frame_ms: 100
walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
walk_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
hurt_down:
frames: [0, 1, 2, 3]
frame_ms: 100
hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
hurt_up:
frames: [4, 5, 6, 7]
frame_ms: 100
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 }
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 }
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 }
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true }
hurt:
frames: [8, 9, 10, 11]
frame_ms: 100
recover_south: { frames: [23] }
recover_north: { frames: [27] }
recover_east: { frames: [31] }
recover_west: { frames: [31], flip_h: true }
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
# El `threshold_ms` filtra els tocs curts: si el temps acumulat de
# moviment continu no ha arribat al llindar, la parada és instantània
# (permet "passets" d'un tile sense rellisquera). L'acumulador es
# reseteja només quan no hi ha ni input ni velocitat residual, així
# els canvis de direcció enmig del coast no rearmen el llindar.
# `brake_time_ms: 0` o absència del bloc = inèrcia desactivada.
# Animació de mort: 3 voltes horàries començant pel sud (south →
# west → north → east, repetit 3×). Reaprofita els frames stand_*; el
# west és l'east amb `flip_h: true` declarat per-frame (mapping form,
# no hereta del nivell d'anim). `frame_ms` per-frame creix linealment
# per donar la sensació de desaccelerar fins a aturar-se. `loop: false`
# → es queda al darrer frame i marca completed.
death_spinning:
frames:
- 0
- { index: 8, flip_h: true }
- 4
- 8
- 0
- { index: 8, flip_h: true }
- 4
- 8
- 0
- { index: 8, flip_h: true }
- 4
- 8
frame_ms: [60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280]
loop: false
# Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra
# tocs curts (passets d'un tile sense rellisquera).
inertia:
brake_time_ms: 150
threshold_ms: 300
# Gir de 180°: mentre dura, el facing queda fixat a la direcció
# intermèdia (efecte purament visual; el moviment continua sense
# alterar-se). Els girs de 90° no passen per cap intermedi.
# Gir de 180°: durant `duration_ms` el facing es fixa a la direcció
# intermèdia indicada (purament visual).
turn:
duration_ms: 80
# Direcció intermèdia per a cada gir de 180° (sentit horari).
transitions:
west_to_east: down # passa per baix (stand_down)
east_to_west: up # passa per dalt (stand_up)
north_to_south: east # passa per la dreta (stand, sense flip)
south_to_north: west # passa per l'esquerra (stand, amb flip)
west_to_east: south
east_to_west: north
north_to_south: east
south_to_north: west
# Salt vertical (px verticals "visuals", no afecta la posició al món).
# impulse → velocitat vertical inicial (px/s) en deixar el sòl.
# gravity → acceleració descendent constant (px/s²).
# landing_stun_ms → temps en què el jugador queda paralitzat en aterrar
# (no es pot moure ni tornar a saltar).
# shadow_y_offset → desplaçament vertical de la sombra (px, positiu =
# puja). Compensa la franja transparent que els
# sprites del jugador tenen sota els peus reals
# perquè camine "pel mig del passadís" (3 px aquí).
# sound → fitxer .wav reproduït al despegue (relatiu a
# data/sounds/). Buit o absent = sense so.
# land_sound → fitxer .wav reproduït en aterrar.
# L'alçada del cos viu al bloc `collision:` (`height`), igual que la resta
# d'entitats: és una propietat de col·lisió, no de la física del salt.
# Amb impulse=240 i gravity=600: pic ≈ 48 px, temps total en l'aire ≈ 0.8 s.
# Salt vertical (px de z, no toca la posició al món).
# `shadow_y_offset`: puja la sombra per compensar la franja
# transparent sota els peus del sprite.
jump:
impulse: 200
gravity: 600
@@ -116,14 +100,27 @@ jump:
sound: jump.wav
land_sound: land.wav
# Estat HURT: el jugador queda projectat sense control per un impacte
# (algú que cau al damunt; en el futur també fuetada de l'altre jugador).
# `enterHurt` tria automàticament la direcció amb més recorregut lliure
# entre els 4 cardinals, posa la velocitat inicial a `speed` i la fa
# decaure linealment fins a 0 en `brake_time_ms`. Si pel camí xoca amb
# un mur, para en sec. En tornar a velocitat 0, recupera l'estat
# GROUNDED i el control. Mentre dura, no col·lisiona amb altres jugadors.
# Amb speed=120 i brake_time=500ms ⇒ recorregut ≈ 30 px (≈2 tiles).
# Projecció post-impacte: surt cap a la direcció amb més recorregut
# lliure i frena fins a 0 en `brake_time_ms`.
hurt:
speed: 120
brake_time_ms: 500
sound: hurt.wav
# Atac amb làtigo. `range_tiles` és la longitud abans de retallar
# contra murs/sarcs; `cooldown_ms` allarga ATTACKING amb una
# recuperació en què la hitbox ja no fa dany.
whip:
range_tiles: 3
swing_ms: 200
cooldown_ms: 200
damage: 1
sound: whip.wav
# Mort + respawn. `respawn_delay_ms` és la pausa entre la fi de
# `death_spinning` i la reaparició al spawn. `lives` és el comptador
# inicial d'intents (estil Mario): cada respawn el decrementa en 1, pot
# anar a negatius (no hi ha game over).
death:
respawn_delay_ms: 500
lives: 3
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

+89 -92
View File
@@ -1,113 +1,97 @@
# player animation
sprite:
path: sam.gif
size: [16, 32]
# Geometria de col·lisió del jugador (independent de l'animació):
# size → costat del quadrat de col·lisió (16×16)
# base_size → costat del quadrat visual inferior del sprite (els "peus");
# s'usa per centrar la caixa de col·lisió sobre ell.
# Geometria de col·lisió. `size` és la caixa contra murs/sarcs;
# `hit_size`, la caixa contra altres actors (més petita); `height`,
# l'alçada que un salt ha de superar per passar per damunt.
collision:
size: 16
base_size: 16
# Caixa de contacte actor-actor (jugador↔jugador, jugador↔momia/trampa).
# Més petita que `size` perquè els contactes d'acció siguin precisos;
# `size` es reserva per a la col·lisió amb murs/sarcòfags i per enfilar
# els passadissos. Centrada en el mateix punt que la caixa base.
hit_size: 8
# Alçada del cos en píxels. Funció DOBLE segons amb què col·lisione:
# · Contra entitats (mummy, rolling_rock, sarc): si `jump_z > height`
# d'aquella entitat, no hi ha col·lisió. Igual que als YAMLs
# d'entitats.
# · Contra altres jugadors: la col·lisió és binària per estat aeri.
# Si un està en l'aire i l'altre no → cap col·lisió. Si tots dos
# estan en l'aire, llavors `body_height` decideix si els rangs
# verticals solapen i s'aplica el rebot mid-aire. `0` = sense cos
# (ni rebot quan tots dos volen).
height: 12
# Animacions en mapa (clau = nom). Camps per animació:
# frames → llista d'índexs dins de l'spritesheet.
# frame_ms → durada de cada frame en ms. Escalar = uniforme; llista =
# per-frame (mateixa mida que `frames`).
# loop → true per defecte. `loop: false` = un sol tret (es queda al
# darrer frame i `animationIsCompleted()` passa a true).
# loop_from → rebota a aquest índex en passar del final (implica loop).
animations:
stand_down: { frames: [1] }
stand_up: { frames: [5] }
stand: { frames: [9] }
jump_down: { frames: [12] }
jump_up: { frames: [13] }
jump: { frames: [14] }
stun_down: { frames: [16] }
stun_up: { frames: [17] }
stun: { frames: [18] }
# Les variants `*_west` reaprofiten els frames de l'est i declaren
# `flip_h: true` perquè el motor d'animació apliqui el flip al sprite —
# el codi del joc no toca mai `flip_` directament.
stand_south: { frames: [0] }
stand_north: { frames: [4] }
stand_east: { frames: [8] }
stand_west: { frames: [8], flip_h: true }
walk_down:
frames: [0, 1, 2, 3]
frame_ms: 100
jump_south: { frames: [12] }
jump_north: { frames: [13] }
jump_east: { frames: [14] }
jump_west: { frames: [14], flip_h: true }
walk_up:
frames: [4, 5, 6, 7]
frame_ms: 100
stun_south: { frames: [16] }
stun_north: { frames: [17] }
stun_east: { frames: [18] }
stun_west: { frames: [18], flip_h: true }
walk:
frames: [8, 9, 10, 11]
frame_ms: 100
walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
walk_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
hurt_down:
frames: [0, 1, 2, 3]
frame_ms: 100
hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
hurt_up:
frames: [4, 5, 6, 7]
frame_ms: 100
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 }
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 }
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 }
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true }
hurt:
frames: [8, 9, 10, 11]
frame_ms: 100
recover_south: { frames: [23] }
recover_north: { frames: [27] }
recover_east: { frames: [31] }
recover_west: { frames: [31], flip_h: true }
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
# El `threshold_ms` filtra els tocs curts: si el temps acumulat de
# moviment continu no ha arribat al llindar, la parada és instantània
# (permet "passets" d'un tile sense rellisquera). L'acumulador es
# reseteja només quan no hi ha ni input ni velocitat residual, així
# els canvis de direcció enmig del coast no rearmen el llindar.
# `brake_time_ms: 0` o absència del bloc = inèrcia desactivada.
# Animació de mort: 3 voltes horàries començant pel sud (south →
# west → north → east, repetit 3×). Reaprofita els frames stand_*; el
# west és l'east amb `flip_h: true` declarat per-frame (mapping form,
# no hereta del nivell d'anim). `frame_ms` per-frame creix linealment
# per donar la sensació de desaccelerar fins a aturar-se. `loop: false`
# → es queda al darrer frame i marca completed.
death_spinning:
frames:
- 0
- { index: 8, flip_h: true }
- 4
- 8
- 0
- { index: 8, flip_h: true }
- 4
- 8
- 0
- { index: 8, flip_h: true }
- 4
- 8
frame_ms: [60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280]
loop: false
# Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra
# tocs curts (passets d'un tile sense rellisquera).
inertia:
brake_time_ms: 150
threshold_ms: 300
# Gir de 180°: mentre dura, el facing queda fixat a la direcció
# intermèdia (efecte purament visual; el moviment continua sense
# alterar-se). Els girs de 90° no passen per cap intermedi.
# Gir de 180°: durant `duration_ms` el facing es fixa a la direcció
# intermèdia indicada (purament visual).
turn:
duration_ms: 80
# Direcció intermèdia per a cada gir de 180° (sentit horari).
transitions:
west_to_east: down # passa per baix (stand_down)
east_to_west: up # passa per dalt (stand_up)
north_to_south: east # passa per la dreta (stand, sense flip)
south_to_north: west # passa per l'esquerra (stand, amb flip)
west_to_east: south
east_to_west: north
north_to_south: east
south_to_north: west
# Salt vertical (px verticals "visuals", no afecta la posició al món).
# impulse → velocitat vertical inicial (px/s) en deixar el sòl.
# gravity → acceleració descendent constant (px/s²).
# landing_stun_ms → temps en què el jugador queda paralitzat en aterrar
# (no es pot moure ni tornar a saltar).
# shadow_y_offset → desplaçament vertical de la sombra (px, positiu =
# puja). Compensa la franja transparent que els
# sprites del jugador tenen sota els peus reals
# perquè camine "pel mig del passadís" (3 px aquí).
# sound → fitxer .wav reproduït al despegue (relatiu a
# data/sounds/). Buit o absent = sense so.
# land_sound → fitxer .wav reproduït en aterrar.
# L'alçada del cos viu al bloc `collision:` (`height`), igual que la resta
# d'entitats: és una propietat de col·lisió, no de la física del salt.
# Amb impulse=240 i gravity=600: pic ≈ 48 px, temps total en l'aire ≈ 0.8 s.
# Salt vertical (px de z, no toca la posició al món).
# `shadow_y_offset`: puja la sombra per compensar la franja
# transparent sota els peus del sprite.
jump:
impulse: 200
gravity: 600
@@ -116,14 +100,27 @@ jump:
sound: jump.wav
land_sound: land.wav
# Estat HURT: el jugador queda projectat sense control per un impacte
# (algú que cau al damunt; en el futur també fuetada de l'altre jugador).
# `enterHurt` tria automàticament la direcció amb més recorregut lliure
# entre els 4 cardinals, posa la velocitat inicial a `speed` i la fa
# decaure linealment fins a 0 en `brake_time_ms`. Si pel camí xoca amb
# un mur, para en sec. En tornar a velocitat 0, recupera l'estat
# GROUNDED i el control. Mentre dura, no col·lisiona amb altres jugadors.
# Amb speed=120 i brake_time=500ms ⇒ recorregut ≈ 30 px (≈2 tiles).
# Projecció post-impacte: surt cap a la direcció amb més recorregut
# lliure i frena fins a 0 en `brake_time_ms`.
hurt:
speed: 120
brake_time_ms: 500
sound: hurt.wav
# Atac amb làtigo. `range_tiles` és la longitud abans de retallar
# contra murs/sarcs; `cooldown_ms` allarga ATTACKING amb una
# recuperació en què la hitbox ja no fa dany.
whip:
range_tiles: 3
swing_ms: 200
cooldown_ms: 200
damage: 1
sound: whip.wav
# Mort + respawn. `respawn_delay_ms` és la pausa entre la fi de
# `death_spinning` i la reaparició al spawn. `lives` és el comptador
# inicial d'intents (estil Mario): cada respawn el decrementa en 1, pot
# anar a negatius (no hi ha game over).
death:
respawn_delay_ms: 500
lives: 3
+30 -6
View File
@@ -7,18 +7,42 @@
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
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
+37
View File
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

+13 -1
View File
@@ -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
+42
View File
@@ -7,6 +7,7 @@
#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
@@ -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() {
+6
View File
@@ -80,6 +80,12 @@ 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)
// 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) ---
+250
View File
@@ -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
+38
View File
@@ -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
+58 -1
View File
@@ -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* {
+37
View File
@@ -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_;
};
+238
View File
@@ -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());
}
}
+126
View File
@@ -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};
};
+220
View File
@@ -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();
}
+91
View File
@@ -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
+63 -7
View File
@@ -12,6 +12,8 @@
#include <utility>
#include "core/audio/audio.hpp"
#include "core/audio/sound_effects_config.hpp"
#include "core/input/gamepad_bindings.hpp"
#include "core/input/gamepad_manager.hpp"
#include "core/input/key_config.hpp"
#include "core/input/mouse.hpp"
@@ -27,6 +29,7 @@
#include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp"
#include "game/debug_config.hpp"
#include "game/levels/level_manager.hpp"
#include "game/options.hpp"
#include "game/scenes/game_scene.hpp"
#include "game/scenes/intro_scene.hpp"
@@ -80,6 +83,11 @@ Director::Director() {
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n';
}
// Vinculacions de mando per dispositiu — fitxer aïllat de `config.yaml`
// perquè és per (name, path) del mando, no per perfil/usuari. Replica
// el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`).
GamepadBindings::get().load(system_folder_ + "gamepad_configs.yaml");
// Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix
// es crea amb presets per defecte (off, crt, ntsc).
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
@@ -121,10 +129,26 @@ Director::Director() {
const std::string PLACEHOLDER_FONT_GIF_KEY = UiConfig::get().placeholderFont().name + ".gif";
Resource::Cache::get().loadSurface(PLACEHOLDER_FONT_GIF_KEY, UiConfig::get().placeholderFont().gif);
Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
// Font del rètol de transició de sala (la consumeix `GameScene`
// tant per la línia "Piràmide N / Sala M" de l'INTRO com pel
// "Fase completada" de l'OUTRO). Es preloda ací per consistència
// amb la resta de fonts d'UI; sense això, la primera entrada a
// GAME aniria a disc.
const std::string ROOM_TEXT_FONT_GIF_KEY = UiConfig::get().roomTextFont().name + ".gif";
Resource::Cache::get().loadSurface(ROOM_TEXT_FONT_GIF_KEY, UiConfig::get().roomTextFont().gif);
Resource::Cache::get().getSurface(ROOM_TEXT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
// Música de la piràmide 1 (primer i únic track de moment). Carreguem ací
// perquè forma part del preload d'assets, com la font i la paleta.
Resource::Cache::get().loadMusic("pyramid_1.ogg", "data/music/pyramid_1.ogg");
// Pistes de música del cicle de sala (intro/playing/outro). Es preloden
// ací perquè formen part del preload d'assets, com la font i la paleta;
// la lògica de quina sona en cada estat viu a `GameScene`.
Resource::Cache::get().loadMusic("room_intro.ogg", "data/music/room_intro.ogg");
Resource::Cache::get().loadMusic("playing.ogg", "data/music/playing.ogg");
Resource::Cache::get().loadMusic("room_outro.ogg", "data/music/room_outro.ogg");
// Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio
// perquè els primers playSoundWith* (al començament del joc) ja
// troben els presets disponibles.
SoundEffectsConfig::get().load("data/sounds.yaml");
// Audio: singleton manual propietari d'un SDL audio device. Es destrueix
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els
@@ -182,6 +206,13 @@ Director::Director() {
DebugConfig::createDefaultIfMissing(DEBUG_YAML);
const auto START = DebugConfig::resolveStartScene(DEBUG_YAML);
const auto STARTING_PLAYERS = DebugConfig::resolveStartingPlayers(DEBUG_YAML);
// `starting_level` (opcional) sobreescriu l'índex inicial del
// LevelManager abans de crear cap GameScene. Si la clau no està o el
// (pyramid, room) no apareix al levels.yaml, el LevelManager
// continua a la primera entrada (P1-R1).
if (const auto SL = DebugConfig::resolveStartingLevel(DEBUG_YAML); SL.has_value()) {
Levels::LevelManager::get().setStartingPoint(SL->pyramid, SL->room);
}
#else
const auto START = DebugConfig::StartScene::LOGO;
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
@@ -189,6 +220,12 @@ Director::Director() {
std::cout << "Director: start_scene=" << DebugConfig::toString(START)
<< " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n';
// El branch Release inicialitza STARTING_PLAYERS amb una constant
// literal i clang considera la captura redundant; en Debug el valor és
// runtime i la captura sí és imprescindible. La lambda viu dins de
// IntroScene, més enllà del scope d'aquest ctor — capturar per
// referència seria UB.
// NOLINTNEXTLINE(clang-diagnostic-unused-lambda-capture)
auto make_game = [STARTING_PLAYERS]() -> std::unique_ptr<Scene> {
auto cfg = GameScene::defaultConfig();
cfg.starting_players = STARTING_PLAYERS;
@@ -265,12 +302,29 @@ void Director::processEvents() {
if (QUIT_REQUESTED) { current_scene_.reset(); }
}
namespace {
// Resol el botó SERVICE configurat per al mando origen d'aquest event.
// Retorna -1 si no en troba cap. La cerca passa per `GamepadManager`
// per obtenir el (name, path) i d'ahí a `GamepadBindings`.
auto serviceButtonForGamepadEvent(SDL_JoystickID jid) -> int {
for (const auto& info : GamepadManager::get().getAll()) {
if (info.pad == nullptr) { continue; }
if (SDL_GetJoystickID(SDL_GetGamepadJoystick(info.pad)) != jid) { continue; }
return GamepadBindings::get().getButton(info.name, info.path, GamepadBindings::Action::SERVICE);
}
return -1;
}
} // namespace
void Director::processGamepadEvents() {
// Dues responsabilitats: (1) passar ADDED/REMOVED al GamepadManager
// perquè obri/tanqui mandos i emeti les notificacions hot-plug; (2)
// tractar el botó BACK com a toggle del menú quan està tancat (el
// paral·lel de F9 per al teclat). Quan el menú està obert, el BACK
// passa pel pipeline del `ServiceMenu::handleEvent()` via
// tractar el botó SERVICE (per defecte BACK; remapejable per
// `GamepadBindings`) com a toggle del menú quan està tancat (el
// paral·lel de F9 per al teclat). Quan el menú està obert, el botó
// SERVICE passa pel pipeline del `ServiceMenu::handleEvent()` via
// `processMenuInput()` i allà es converteix en `close()`.
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
bool consumed = false;
@@ -278,11 +332,13 @@ 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()) {
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);
} else {
+6
View File
@@ -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_);
}
+6
View File
@@ -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_;
+31
View File
@@ -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";
+13
View File
@@ -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*;
+8 -5
View File
@@ -14,6 +14,7 @@
#include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/system/event_buffer.hpp"
#include "game/levels/level_manager.hpp"
#include "game/map/tilemap.hpp"
#include "utils/defaults.hpp"
#include "utils/defines.hpp"
@@ -76,11 +77,13 @@ void Editor::exit(Tilemap& tilemap) {
if (!active_) { return; }
active_ = false;
picker_open_ = false;
// Resolem la ruta via Resource::Helper perquè en Debug el root de
// dades és AEE_SOURCE_DIR (ruta absoluta al repo), no el CWD. Sense
// això, el writer obriria `data/maps/pyramid.yaml` relatiu al CWD
// i fallaria sempre excepte si el binari es llança des del repo root.
const auto ABS_PATH = Resource::Helper::resolve(Defaults::Editor::SAVE_PATH);
// El writer salva sempre sobre la sala actualment carregada (segons
// el LevelManager): així el "9" de l'editor desa el que tens davant,
// no una constant fixa. Resolem la ruta via Resource::Helper perquè
// en Debug el root de dades és AEE_SOURCE_DIR (ruta absoluta al repo),
// no el CWD; sense això, el writer obriria el path relatiu al CWD i
// fallaria si el binari no es llança des del repo root.
const auto ABS_PATH = Resource::Helper::resolve(Levels::LevelManager::get().currentPath());
const bool SAVED = tilemap.save(ABS_PATH);
Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error"));
}
+12 -1
View File
@@ -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};
+35
View File
@@ -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};
};
+202 -42
View File
@@ -26,11 +26,15 @@ namespace {
// Llegeix un bloc d'animació del YAML amb fallbacks tolerants (mateix
// patró que Door/Sarcophagus). Si la clau no apareix, retorna una
// animació d'1 frame loopant, neutra — la momia es podrà moure però
// visualment es quedarà estàtica.
// visualment es quedarà estàtica. Llegeix `flip_h`/`flip_v` (default
// false) i els passa al component perquè el motor sàpiga aplicar-los
// al sprite quan aquesta animació estigui activa.
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
std::vector<int> frames = {0};
float fps = 8.0F;
bool loop = true;
bool flip_h = false;
bool flip_v = false;
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
const auto& anim = yaml["animations"][key];
if (anim.contains("frames")) {
@@ -43,8 +47,10 @@ namespace {
}
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
if (anim.contains("flip_h")) { flip_h = anim["flip_h"].get_value<bool>(); }
if (anim.contains("flip_v")) { flip_v = anim["flip_v"].get_value<bool>(); }
}
return {frames, fps, loop};
return {frames, fps, loop, flip_h, flip_v};
}
// Busca si el segment [prev, prev+delta] travessa una lane del patró
@@ -73,57 +79,83 @@ namespace {
} // namespace
Mummy::Mummy(const fkyaml::node& yaml)
: anim_walk_down_(buildAnimation(yaml, "walk_down")),
anim_walk_up_(buildAnimation(yaml, "walk_up")),
anim_walk_lateral_(buildAnimation(yaml, "walk")) {
: anim_walk_south_(buildAnimation(yaml, "walk_south")),
anim_walk_north_(buildAnimation(yaml, "walk_north")),
anim_walk_east_(buildAnimation(yaml, "walk_east")),
anim_walk_west_(buildAnimation(yaml, "walk_west")) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
collides_ = true;
interactable_ = false;
// Velocitat i dany des del bloc `stats` (si existeix).
// Velocitat i dany des del bloc `stats` (si existeix). El flag
// `kills_outright` també hi viu: per defecte true (la mòmia mata
// d'un sol toc); posat a false al YAML, el contacte només publica
// `player_hit` sense disparar el cicle DYING.
if (yaml.contains("stats")) {
const auto& s = yaml["stats"];
if (s.contains("speed")) { setSpeed(s["speed"].get_value<float>()); }
if (s.contains("damage")) { setDamage(s["damage"].get_value<int>()); }
if (s.contains("kills_outright")) { kills_outright_ = s["kills_outright"].get_value<bool>(); }
}
// Direcció inicial llegida del bloc `ai:`. Per defecte dreta, per
// coherència amb el que fa RollingRock. Valors acceptats: right, left,
// up, down. Qualsevol altre s'interpreta com a "sense direcció" (la
// momia no es mou fins que step() no triï una direcció).
std::string initial = "right";
// Direcció inicial llegida del bloc `ai:`. Per defecte est, per
// coherència amb el que fa RollingRock. Valors acceptats:
// east, west, north, south. Qualsevol altre s'interpreta com a
// "sense direcció" (la momia no es mou fins que step() no triï una
// direcció).
std::string initial = "east";
if (yaml.contains("ai") && yaml["ai"].contains("initial_direction")) {
initial = yaml["ai"]["initial_direction"].get_value<std::string>();
}
if (initial == "right") {
if (initial == "east") {
changeDirection(1.0F, 0.0F);
} else if (initial == "left") {
} else if (initial == "west") {
changeDirection(-1.0F, 0.0F);
} else if (initial == "up") {
} else if (initial == "north") {
changeDirection(0.0F, -1.0F);
} else if (initial == "down") {
} else if (initial == "south") {
changeDirection(0.0F, 1.0F);
}
// Configuració de l'estat HURT: opcional. Si el bloc no apareix, la
// mòmia no entrarà mai a HURT efectiu (defensiu); el WhipAttack la
// pot ignorar amb `isWhippable() == false` si volem que no reacció.
if (yaml.contains("hurt")) {
const auto& h = yaml["hurt"];
if (h.contains("duration_ms")) {
hurt_config_.duration_s = static_cast<float>(h["duration_ms"].get_value<int>()) / 1000.0F;
}
if (h.contains("speed")) {
hurt_config_.speed_px_per_s = h["speed"].get_value<float>();
}
if (h.contains("whippable")) {
whippable_ = h["whippable"].get_value<bool>();
}
}
}
void Mummy::changeDirection(float dx, float dy) {
setDirection(dx, dy);
// Prevalença lateral: si hi ha component horitzontal s'usa l'anim
// lateral + flip segons el signe. Només direccions purament verticals
// disparen walk_up/walk_down. Quan la direcció queda (0,0) (cul-de-sac
// amb NONE) no toquem l'animació: la momia es quedarà al frame actual
// fins que step() li torni a assignar direcció.
if (dx != 0.0F) {
anim_ = anim_walk_lateral_;
flip_ = (dx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
// Una animació per cada cardinal — el flip viu al YAML
// (`walk_west: flip_h: true`) i el codi només selecciona quina
// animació mostrar. Prevalença lateral en diagonals; quan la direcció
// queda (0,0) (cul-de-sac amb NONE) no toquem l'animació: la mòmia es
// quedarà al frame actual fins que step() li torni a assignar direcció.
if (dx > 0.0F) {
anim_ = anim_walk_east_;
} else if (dx < 0.0F) {
anim_ = anim_walk_west_;
} else if (dy < 0.0F) {
anim_ = anim_walk_up_;
flip_ = SDL_FLIP_NONE;
anim_ = anim_walk_north_;
} else if (dy > 0.0F) {
anim_ = anim_walk_down_;
flip_ = SDL_FLIP_NONE;
anim_ = anim_walk_south_;
}
if (anim_.has_value()) {
anim_->reset();
// Propaguem el flip que l'animació declara al YAML al camp
// `flip_` que el render llegeix (entity_manager_loadlevel.cpp).
flip_ = anim_->flipMode();
}
if (anim_.has_value()) { anim_->reset(); }
}
auto Mummy::chooseDirection(const std::vector<Direction>& allowed) -> Direction {
@@ -218,10 +250,10 @@ auto Mummy::detectLaneCrossing(float dx, float dy) const -> std::optional<float>
}
auto Mummy::currentDirection() const -> Direction {
if (dir_x_ > 0.0F) { return Direction::RIGHT; }
if (dir_x_ < 0.0F) { return Direction::LEFT; }
if (dir_y_ > 0.0F) { return Direction::DOWN; }
if (dir_y_ < 0.0F) { return Direction::UP; }
if (dir_x_ > 0.0F) { return Direction::EAST; }
if (dir_x_ < 0.0F) { return Direction::WEST; }
if (dir_y_ > 0.0F) { return Direction::SOUTH; }
if (dir_y_ < 0.0F) { return Direction::NORTH; }
return Direction::NONE;
}
@@ -252,10 +284,10 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
Direction dir;
};
const std::array<Candidate, 4> ALL{{
{.dx = step, .dy = 0.0F, .dir = Direction::RIGHT},
{.dx = -step, .dy = 0.0F, .dir = Direction::LEFT},
{.dx = 0.0F, .dy = -step, .dir = Direction::UP},
{.dx = 0.0F, .dy = step, .dir = Direction::DOWN},
{.dx = step, .dy = 0.0F, .dir = Direction::EAST},
{.dx = -step, .dy = 0.0F, .dir = Direction::WEST},
{.dx = 0.0F, .dy = -step, .dir = Direction::NORTH},
{.dx = 0.0F, .dy = step, .dir = Direction::SOUTH},
}};
DirectionPools pools;
for (const auto& c : ALL) {
@@ -277,16 +309,16 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
void Mummy::applyChoice(Direction dir) {
switch (dir) {
case Direction::UP:
case Direction::NORTH:
changeDirection(0.0F, -1.0F);
break;
case Direction::DOWN:
case Direction::SOUTH:
changeDirection(0.0F, 1.0F);
break;
case Direction::LEFT:
case Direction::WEST:
changeDirection(-1.0F, 0.0F);
break;
case Direction::RIGHT:
case Direction::EAST:
changeDirection(1.0F, 0.0F);
break;
case Direction::NONE:
@@ -296,13 +328,141 @@ void Mummy::applyChoice(Direction dir) {
}
void Mummy::onPlayerContact(Player* player) {
if (bus_ != nullptr) {
if (bus_ == nullptr) { return; }
const int PID = (player != nullptr) ? player->getId() : -1;
bus_->publish(Event{
.name = "player_hit",
.payload = getDamage(),
.player_id = (player != nullptr) ? player->getId() : -1,
.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_killed",
.payload = getDamage(),
.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)
+71 -13
View File
@@ -16,10 +16,25 @@
class Mummy : public DynamicHazard {
public:
enum class Direction : std::uint8_t { NONE,
UP,
DOWN,
LEFT,
RIGHT };
NORTH,
SOUTH,
WEST,
EAST };
// Estat d'alt nivell. ROAMING = IA normal (corridor_wanderer); HURT =
// post-fuetada, fugint en línia recta durant `hurt_config_.duration_s`.
// Mentre dura el HURT no decideix a creuaments ni canvia de direcció;
// si topa amb mur o sarc, surt del HURT abans de l'expiració del timer.
enum class State : std::uint8_t { ROAMING,
HURT };
// Configuració de l'estat HURT, llegida del bloc `hurt:` del YAML.
// Si `duration_s == 0` la mòmia mai entra a HURT efectiu (defensiu).
// `speed_px_per_s == 0` ⇒ s'usa la velocitat normal de la mòmia.
struct HurtConfig {
float duration_s{0.0F};
float speed_px_per_s{0.0F};
};
explicit Mummy(const fkyaml::node& yaml);
@@ -39,9 +54,23 @@ class Mummy : public DynamicHazard {
// En ambdós casos el frame de gir no consumeix avenç extra.
void step(float delta_time) override;
// Update sencer: afegim la branca HURT (avanç del timer + step recta).
// En estat ROAMING delega a `step()` com sempre via la base.
void update(float delta_time) override;
// Contacte amb el jugador: publica `player_hit` amb el dany.
void onPlayerContact(Player* player) override;
// Activa l'estat HURT amb la direcció de fuga preferida `flee_dir`.
// Si està bloquejada, prova perpendiculars; si totes estan bloquejades,
// la mòmia queda quieta però en HURT durant tot el timer.
void enterHurt(Direction flee_dir);
[[nodiscard]] auto getState() const -> State { return state_; }
[[nodiscard]] auto isWhippable() const -> bool { return whippable_; }
[[nodiscard]] auto getHurtRemainingS() const -> float { return hurt_remaining_s_; }
[[nodiscard]] auto killsOutright() const -> bool { return kills_outright_; }
private:
// Caixa de col·lisió candidata si l'entitat es desplaçara (dx, dy).
[[nodiscard]] auto candidateBox(float dx, float dy) const -> SDL_FRect;
@@ -63,10 +92,11 @@ class Mummy : public DynamicHazard {
};
[[nodiscard]] auto collectDirectionPools(float step) const -> DirectionPools;
// Canvi de direcció + aplicació de l'animació i flip corresponents.
// Tots els camins que trien nova direcció (ctor amb `initial_direction`,
// gir després d'un bloqueig) passen per ací per mantenir anim_ i
// flip_ en sincronia amb (dir_x_, dir_y_).
// Canvi de direcció + selecció de l'animació corresponent (un cardinal
// per branch). Tots els camins que trien nova direcció (ctor amb
// `initial_direction`, gir després d'un bloqueig) passen per ací; el
// flip viu al YAML (`walk_west: flip_h: true`) i es propaga des de
// l'animació al `flip_` heretat.
void changeDirection(float dx, float dy);
// Aplica la direcció triada (wrapper del switch en una sola crida).
@@ -90,9 +120,37 @@ class Mummy : public DynamicHazard {
// S'usa al pool de creuament per afegir la recta com a candidata.
[[nodiscard]] auto currentDirection() const -> Direction;
// Animacions carregades del YAML, una per cada facing. Per a horitzontal
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta.
AnimationComponent anim_walk_down_;
AnimationComponent anim_walk_up_;
AnimationComponent anim_walk_lateral_;
// Avança recta a `hurt_speed_ * delta_time`. Si la caixa candidata
// sortiria del playfield o trepitjaria un sarc, marca la sortida del
// HURT abans de l'expiració del timer (la mòmia no rebota).
void hurtStep(float delta_time);
// Tria la direcció definitiva del HURT donada la preferida (la del
// whip). Si la preferida està bloquejada, prova perpendiculars; si
// totes ho estan, retorna `NONE` (la mòmia queda quieta en HURT).
[[nodiscard]] auto chooseHurtDirection(Direction preferred) const -> Direction;
// Animacions carregades del YAML, una per cada cardinal. La variant
// `walk_west` reaprofita els frames de l'est però amb `flip_h: true`
// declarat al YAML — el motor d'animació aplica el flip al sprite
// automàticament; aquesta classe no toca mai `flip_` directament.
AnimationComponent anim_walk_south_;
AnimationComponent anim_walk_north_;
AnimationComponent anim_walk_east_;
AnimationComponent anim_walk_west_;
// Estat HURT i la seua configuració. `hurt_speed_` és l'efectiva
// (resultat d'aplicar el `speed` del bloc `hurt:` o, si val 0, la
// velocitat normal de la mòmia).
State state_{State::ROAMING};
HurtConfig hurt_config_;
bool whippable_{true};
float hurt_remaining_s_{0.0F};
float hurt_speed_{0.0F};
// Si true, el contacte amb el jugador dispara `Player::enterDying`
// (mort d'un sol toc, bypass del flux `hurt`/knockback). Llegit del
// bloc `stats.kills_outright` del YAML; default true per coherència
// amb el comportament d'Oh Mummy! (les mòmies maten).
bool kills_outright_{true};
};
@@ -0,0 +1,342 @@
#include "game/entities/concrete/whip_attack.hpp"
#include <algorithm>
#include <filesystem>
#include <string>
#include <vector>
#include "game/entities/concrete/mummy.hpp"
#include "game/entities/entity_factory.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/entities/player.hpp"
#include "game/map/map.hpp"
namespace {
// Llig una entrada `sprites.<dir>: { path, size: [w, h] }` del YAML.
// Si manquen camps, retorna un DirSprite buit (queda invalidat al
// configure() per `surface_key.empty()`).
auto readDirSprite(const fkyaml::node& s) -> WhipAttack::DirSprite {
WhipAttack::DirSprite sprite;
if (s.contains("path")) {
const auto REL = s["path"].get_value<std::string>();
sprite.surface_key = std::filesystem::path(REL).filename().string();
}
if (s.contains("size")) {
const auto& sz = s["size"];
if (sz.is_sequence() && sz.size() == 2) {
sprite.frame_w = static_cast<float>(sz[0].get_value<int>());
sprite.frame_h = static_cast<float>(sz[1].get_value<int>());
}
}
return sprite;
}
// Construeix l'`AnimationComponent` del swing a partir del bloc
// `animations.swing`. Convenció igual que la resta d'entitats:
// - `frames` : llista d'índexs dins l'spritesheet (obligatori).
// - `frame_ms` : durada uniforme per frame (ms). Alternativament `fps`.
// - `loop` : per defecte true; al látigo el volem false (mor en
// completar).
auto buildSwingAnim(const fkyaml::node& yaml, std::vector<int>& active_out)
-> AnimationComponent {
std::vector<int> frames = {0};
float fps = 8.0F;
bool loop = false;
if (yaml.contains("animations") && yaml["animations"].contains("swing")) {
const auto& sw = yaml["animations"]["swing"];
if (sw.contains("frames")) {
frames.clear();
for (const auto& f : sw["frames"]) {
// cppcheck-suppress useStlAlgorithm
frames.push_back(f.get_value<int>());
}
if (frames.empty()) { frames.push_back(0); }
}
if (sw.contains("frame_ms")) {
const auto MS = static_cast<float>(sw["frame_ms"].get_value<int>());
if (MS > 0.0F) { fps = 1000.0F / MS; }
} else if (sw.contains("fps")) {
fps = sw["fps"].get_value<float>();
}
if (sw.contains("loop")) { loop = sw["loop"].get_value<bool>(); }
if (sw.contains("active_frames")) {
for (const auto& f : sw["active_frames"]) {
// cppcheck-suppress useStlAlgorithm
active_out.push_back(f.get_value<int>());
}
}
}
return {frames, fps, loop};
}
} // namespace
WhipAttack::WhipAttack(const fkyaml::node& yaml) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("whip_attack");
collides_ = false;
interactable_ = false;
if (yaml.contains("sprites")) {
const auto& s = yaml["sprites"];
if (s.contains("north")) { sprite_north_ = readDirSprite(s["north"]); }
if (s.contains("south")) { sprite_south_ = readDirSprite(s["south"]); }
if (s.contains("east")) { sprite_east_ = readDirSprite(s["east"]); }
if (s.contains("west")) { sprite_west_ = readDirSprite(s["west"]); }
}
anim_swing_ = buildSwingAnim(yaml, active_frames_);
anim_ = anim_swing_;
}
void WhipAttack::configure(Direction dir, const SDL_FRect& owner_box, float range_px) {
direction_ = dir;
range_px_ = range_px;
// Draw rect (= pixel_rect_): cobreix tot el sprite del látigo segons
// la taula d'offsets del disseny:
// E: (player.x - 16, player.y ), 80×32
// W: (player.x - 48, player.y ), 80×32
// N: (player.x, player.y - 48 ), 16×96
// S: (player.x, player.y - 16 ), 16×96
const auto& chosen = spriteFor(dir);
SDL_FRect draw{0.0F, 0.0F, chosen.frame_w, chosen.frame_h};
switch (dir) {
case Direction::EAST:
draw.x = owner_box.x - 16.0F;
draw.y = owner_box.y;
break;
case Direction::WEST:
draw.x = owner_box.x - 48.0F;
draw.y = owner_box.y;
break;
case Direction::NORTH:
draw.x = owner_box.x;
draw.y = owner_box.y - 48.0F;
break;
case Direction::SOUTH:
draw.x = owner_box.x;
draw.y = owner_box.y - 16.0F;
break;
}
setPixelRect(draw);
// Damage rect: 48×16 (E/W) o 16×48 (N/S) al nivell dels peus del
// jugador (els últims 16 px del seu pixel_rect — convenció estàndard
// del player AEE), pegat al borde en la direcció d'atac.
// computeDamageRect rep la cantonada del rect MÀXIM (sense retallar)
// i recorre internament la línia per topar amb el primer bloquejador.
const float COLLISION_TOP_Y = owner_box.y + owner_box.h - 16.0F;
const float OWNER_RIGHT = owner_box.x + owner_box.w;
const float OWNER_BOTTOM = owner_box.y + owner_box.h;
float spawn_x = 0.0F;
float spawn_y = 0.0F;
switch (dir) {
case Direction::EAST:
spawn_x = OWNER_RIGHT;
spawn_y = COLLISION_TOP_Y;
break;
case Direction::WEST:
spawn_x = owner_box.x - range_px;
spawn_y = COLLISION_TOP_Y;
break;
case Direction::NORTH:
spawn_x = owner_box.x;
spawn_y = COLLISION_TOP_Y - range_px;
break;
case Direction::SOUTH:
spawn_x = owner_box.x;
spawn_y = OWNER_BOTTOM;
break;
}
computeDamageRect(dir, spawn_x, spawn_y, range_px);
// Snapshot del feet_y del jugador. La convenció estàndard del Player
// AEE té el seu feet_y a la base inferior del pixel_rect (collision
// 16×16 als últims 16 px del sprite 16×32), així que owner_box.y +
// owner_box.h reprodueix `Player::getFeetY()` exactament.
owner_feet_y_ = owner_box.y + owner_box.h;
if (anim_.has_value()) { anim_->reset(); }
}
auto WhipAttack::getFeetY() const -> float {
return owner_feet_y_;
}
void WhipAttack::setWorldContext(EntityManager* manager, const std::vector<Player*>* players) {
entity_manager_ = manager;
players_ = players;
}
void WhipAttack::computeDamageRect(Direction dir, float spawn_x, float spawn_y, float range_px) {
// Caminem píxel a píxel des de la vora del jugador en la direcció
// d'atac, fins a topar amb un mur o sarc, o esgotar `range_px`.
// L'amplada perpendicular és `tile_size` (els passadissos del joc
// tenen exactament aquesta mida i l'atac és lateral).
const auto TILE = static_cast<float>(Map::tile_size);
const int MAX_PX = static_cast<int>(std::max(0.0F, range_px));
const auto MAX = static_cast<float>(MAX_PX);
float length = 0.0F;
auto try_probe = [&](float l) -> bool {
SDL_FRect probe{0.0F, 0.0F, 0.0F, 0.0F};
switch (dir) {
case Direction::EAST:
probe = {.x = spawn_x, .y = spawn_y, .w = l, .h = TILE};
break;
case Direction::WEST:
// Per a W, la cara "del jugador" és la dreta; com creix
// `l`, la vora esquerra retrocedeix. spawn_x apunta al
// límit esquerre amb l = MAX (cas no retallat).
probe = {.x = spawn_x + (MAX - l), .y = spawn_y, .w = l, .h = TILE};
break;
case Direction::SOUTH:
probe = {.x = spawn_x, .y = spawn_y, .w = TILE, .h = l};
break;
case Direction::NORTH:
probe = {.x = spawn_x, .y = spawn_y + (MAX - l), .w = TILE, .h = l};
break;
}
return Map::insidePlayfield(probe) && !Map::collides(probe);
};
for (int i = 1; i <= MAX_PX; ++i) {
const auto L = static_cast<float>(i);
if (try_probe(L)) {
length = L;
} else {
break;
}
}
switch (dir) {
case Direction::EAST:
damage_rect_ = {.x = spawn_x, .y = spawn_y, .w = length, .h = TILE};
return;
case Direction::WEST:
damage_rect_ = {.x = spawn_x + (MAX - length), .y = spawn_y, .w = length, .h = TILE};
return;
case Direction::SOUTH:
damage_rect_ = {.x = spawn_x, .y = spawn_y, .w = TILE, .h = length};
return;
case Direction::NORTH:
damage_rect_ = {.x = spawn_x, .y = spawn_y + (MAX - length), .w = TILE, .h = length};
return;
}
}
auto WhipAttack::getSurfaceKey() const -> const std::string& {
return spriteFor(direction_).surface_key;
}
auto WhipAttack::spriteFor(Direction dir) const -> const DirSprite& {
switch (dir) {
case Direction::NORTH:
return sprite_north_;
case Direction::SOUTH:
return sprite_south_;
case Direction::EAST:
return sprite_east_;
case Direction::WEST:
return sprite_west_;
}
return sprite_east_;
}
void WhipAttack::update(float delta_time) {
Entity::update(delta_time);
if (isFrameActive()) { applyDamage(); }
if (anim_.has_value() && anim_->completed()) { markDead(); }
}
namespace {
auto whipDirToMummyDir(WhipAttack::Direction d) -> Mummy::Direction {
switch (d) {
case WhipAttack::Direction::NORTH:
return Mummy::Direction::NORTH;
case WhipAttack::Direction::SOUTH:
return Mummy::Direction::SOUTH;
case WhipAttack::Direction::EAST:
return Mummy::Direction::EAST;
case WhipAttack::Direction::WEST:
return Mummy::Direction::WEST;
}
return Mummy::Direction::NONE;
}
auto whipDirToPlayerCardinal(WhipAttack::Direction d) -> Player::Cardinal {
switch (d) {
case WhipAttack::Direction::NORTH:
return Player::Cardinal::NORTH;
case WhipAttack::Direction::SOUTH:
return Player::Cardinal::SOUTH;
case WhipAttack::Direction::EAST:
return Player::Cardinal::EAST;
case WhipAttack::Direction::WEST:
return Player::Cardinal::WEST;
}
return Player::Cardinal::NONE;
}
auto playerIsTargetable(const Player* p) -> bool {
if (p == nullptr) { return false; }
const auto STATE = p->getState();
return STATE != Player::State::HURT && STATE != Player::State::EXITED && STATE != Player::State::DISABLED;
}
// Tracta una mòmia candidata: si és whippable i la seua caja solapa
// amb `damage_rect`, la posa a HURT i la marca al set. Extret per
// acotar la complexitat cognitiva d'`applyDamage` (clang-tidy llindar).
void hitMummyIfApplicable(
Entity* e,
const SDL_FRect& damage_rect,
WhipAttack::Direction dir,
std::unordered_set<void*>& already_hit) {
if (e == nullptr || e->isDead()) { return; }
if (already_hit.contains(static_cast<void*>(e))) { return; }
auto* mummy = dynamic_cast<Mummy*>(e);
if (mummy == nullptr || !mummy->isWhippable()) { return; }
const auto BOX = mummy->getCollisionRect();
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
mummy->enterHurt(whipDirToMummyDir(dir));
already_hit.insert(static_cast<void*>(e));
}
void hitPlayerIfApplicable(
Player* p,
const Player* owner,
const SDL_FRect& damage_rect,
WhipAttack::Direction dir,
std::unordered_set<void*>& already_hit) {
if (p == owner || !playerIsTargetable(p)) { return; }
if (already_hit.contains(static_cast<void*>(p))) { return; }
const auto BOX = p->getBaseBox();
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
p->enterHurt(whipDirToPlayerCardinal(dir));
already_hit.insert(static_cast<void*>(p));
}
} // namespace
void WhipAttack::applyDamage() {
if (entity_manager_ != nullptr) {
for (auto* e : entity_manager_->getEntities()) {
hitMummyIfApplicable(e, damage_rect_, direction_, already_hit_);
}
}
if (players_ != nullptr) {
for (auto* p : *players_) {
hitPlayerIfApplicable(p, owner_, damage_rect_, direction_, already_hit_);
}
}
}
auto WhipAttack::isFrameActive() const -> bool {
if (!anim_.has_value()) { return false; }
const int FRAME = anim_->currentFrame();
return std::ranges::any_of(active_frames_, [FRAME](int f) { return f == FRAME; });
}
REGISTER_ENTITY("whip_attack", WhipAttack)
@@ -0,0 +1,162 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string>
#include <unordered_set>
#include <vector>
#include "external/fkyaml_node.hpp"
#include "game/entities/animation_component.hpp"
#include "game/entities/attack.hpp"
class Entity;
class EntityManager;
class Player;
// Atac de látigo: spawnejat pel jugador en entrar a `ATTACKING`. Viu la
// durada de la seua animació (`swing`) i s'autoelimina al final.
//
// Sprites: 4 fitxers GIF distints, un per cardinal (sense flips). La
// mida del frame canvia entre direccions (E/W: 80×32, N/S: 16×96),
// cosa que el sistema d'animació actual no permet expressar amb una
// sola surface. Per això el YAML llista les 4 entrades sota `sprites:`
// i `configure()` tria la que toca segons direcció.
//
// Dos rectangles distints, intencionadament diferenciats:
// - `pixel_rect_` (heretat d'`Entity`) = `draw_rect`. Cobreix tot el
// sprite del látigo: 16 px de wind-up darrere del jugador, 16 px
// de solapament amb el jugador, i 48 px d'atac frontal (per E/W;
// proporcions equivalents per N/S amb 32 px de solapament). NO es
// retalla per bloquejadors — només render visual.
// - `damage_rect_` = caixa de dany al nivell dels peus del jugador
// (centrada sobre la base 16×16). 48×16 (E/W) o 16×48 (N/S),
// pegada al borde del jugador en la direcció d'atac. SÍ es retalla
// pel primer bloquejador (mur extern del playfield o sarcòfag).
//
// Z-order: `getFeetY()` delega al `owner_` per garantir que el látigo
// quede al mateix bucket d'y-sort que el jugador que l'ha llançat. Si
// no, l'altura física del sprite (esp. en N/S, que té 96 px) crearia
// solapaments raros amb mòmies a la trajectòria.
class WhipAttack : public Attack {
public:
// Direcció cardinal del swing. Coincideix valors amb
// `Player::Cardinal` (1=N, 2=S, 3=E, 4=W) per facilitar la
// conversió a la frontera de l'event `whip_spawn`.
enum class Direction : std::uint8_t {
NORTH = 1,
SOUTH = 2,
EAST = 3,
WEST = 4,
};
// Definició estàtica d'una direcció: clau al `Resource::Cache` i mida
// del frame en píxels. Llegida del YAML una vegada al ctor; usada per
// configure() per omplir surface_ i pixel_rect_. Pública perquè el
// helper d'anàlisi del YAML viu en un namespace anònim al .cpp.
struct DirSprite {
std::string surface_key;
float frame_w{0.0F};
float frame_h{0.0F};
};
explicit WhipAttack(const fkyaml::node& yaml);
// Configura l'estat runtime de la instància. Cal cridar-lo abans del
// primer `update`. A partir del `owner_box` (pixel_rect del jugador,
// típicament 16×32) i la direcció d'atac, deriva:
// - `pixel_rect_` (= draw_rect): cantonada i mida del sprite del
// látigo segons la taula d'offsets (E: -16, 0, 80×32; etc.).
// - `damage_rect_`: caixa de dany 48×16 (o 16×48) pegada al
// borde del jugador, retallada contra murs i sarcs.
// Convenció implícita: el jugador té collision rect 16×16 als
// últims 16 px del seu pixel_rect (els "peus"); aquesta funció
// n'usa la geometria per al damage_rect i el feet_y delegat.
void configure(Direction dir, const SDL_FRect& owner_box, float range_px);
// Injecta el context que necessita la fase de dany: punter a
// l'`EntityManager` (per iterar Hazards) i a la llista de jugadors
// (per al friendly fire). Si no es crida, els frames actius no
// apliquen dany — el whip queda merament visual. Cridar abans del
// primer `update` (típicament just després de `configure`).
void setWorldContext(EntityManager* manager, const std::vector<Player*>* players);
// Avança animació; durant frames actius, aplica dany; quan
// l'animació es completa, marca dead.
void update(float delta_time) override;
[[nodiscard]] auto getDirection() const -> Direction { return direction_; }
// True si el frame actual de l'animació és un dels `active_frames`
// declarats al YAML (els frames durant els quals la hitbox aplica
// dany). Wind-up i recovery retornen false.
[[nodiscard]] auto isFrameActive() const -> bool;
// Llistat dels frames actius (lectura del YAML, immutable per
// instància). Exposat per als smoke tests.
[[nodiscard]] auto getActiveFrames() const -> const std::vector<int>& { return active_frames_; }
// Llargada de la hitbox abans de retallar contra murs/sarcs (en píxels).
[[nodiscard]] auto getRangePx() const -> float { return range_px_; }
// Hitbox efectiva de dany (ja retallada pel primer bloquejador en la
// línia d'atac). Exposada per als smoke tests; en producció només la
// consumeix la pròpia `applyDamage`.
[[nodiscard]] auto getDamageRect() const -> SDL_FRect { return damage_rect_; }
// Y dels peus delegada al `owner_box` capturat a `configure()`: el
// látigo es z-ordena al mateix bucket que el jugador que l'ha
// llançat (= base inferior del seu pixel_rect, que coincideix amb
// el feet_y del jugador a la convenció estàndard del player AEE).
// Snapshot, no consulta dinàmica: el jugador queda quiet durant
// ATTACKING, així que snapshot ≡ live per a la vida del whip.
[[nodiscard]] auto getFeetY() const -> float override;
// Clau al `Resource::Cache` per a la surface de la direcció actual
// (la que ha triat `configure()`). El caller (típicament `GameScene`)
// resol la surface i la passa via `setSurface()`. Aquesta separació
// manté el codi de l'entitat sense dependència directa al
// `Resource::Cache`, igual que la resta d'entitats del joc.
[[nodiscard]] auto getSurfaceKey() const -> const std::string&;
private:
[[nodiscard]] auto spriteFor(Direction dir) const -> const DirSprite&;
// Calcula la `damage_rect_` final: retalla la longitud de l'atac
// contra el primer bloquejador (mur extern del playfield o sarc) en
// la línia d'atac. Implementació iterativa píxel a píxel — el rang
// típic és ~48 px, així que el cost és menyspreable.
void computeDamageRect(Direction dir, float spawn_x, float spawn_y, float range_px);
// Aplica dany aquest frame: itera Hazards (Mummy whippable) i
// jugadors (excepte `owner_`) que solapen amb `damage_rect_` i
// estan en estat "atacable", crida `enterHurt` amb la direcció
// adequada i els marca a `already_hit_` per garantir un sol
// impacte per swing per objectiu.
void applyDamage();
Direction direction_{Direction::EAST};
AnimationComponent anim_swing_;
std::vector<int> active_frames_;
// Set d'objectius ja colpejats aquest swing: usem `void*` perquè
// hi guardem tant `Entity*` (Mummy) com `Player*` (que NO hereta
// d'Entity) sense `reinterpret_cast` semànticament dubtós.
std::unordered_set<void*> already_hit_;
float range_px_{0.0F};
SDL_FRect damage_rect_{0.0F, 0.0F, 0.0F, 0.0F};
// Snapshot del feet_y de l'owner pres a `configure()` (= base
// inferior del seu pixel_rect, que coincideix amb el `getFeetY()`
// del Player en la convenció estàndard d'AEE). El retornem des
// del nostre `getFeetY()` per delegar el bucket d'y-sort al jugador.
float owner_feet_y_{0.0F};
EntityManager* entity_manager_{nullptr};
const std::vector<Player*>* players_{nullptr};
DirSprite sprite_north_;
DirSprite sprite_south_;
DirSprite sprite_east_;
DirSprite sprite_west_;
};
+9 -6
View File
@@ -140,8 +140,10 @@ class Entity {
// Y dels peus: la vora inferior de la caixa de col·lisió. La usarà el
// Z-order per ordenar entitats i jugador de menys a més profunditat
// (REND-01, quan s'implemente).
[[nodiscard]] auto getFeetY() const -> float;
// (REND-01, quan s'implemente). Virtual perquè entitats efímeres
// (com `WhipAttack`) poden delegar al seu owner per quedar al
// mateix bucket d'y-sort que el jugador que les ha llançat.
[[nodiscard]] virtual auto getFeetY() const -> float;
// El loader li assigna una caixa explícita llegida del YAML; sense
// aquesta crida, l'entitat queda sense `CollisionBox` i la caixa de
@@ -153,10 +155,11 @@ class Entity {
// el sprite) amb el tile. Nullopt si l'entitat no en té.
[[nodiscard]] auto getCollisionBox() const -> const std::optional<CollisionBox>& { return collision_box_; }
// Flip del sprite. Les entitats estàtiques mai no el toquen (queda a
// SDL_FLIP_NONE); les dinàmiques que miren cap als costats (p. ex.
// Mummy en direcció horitzontal) el canvien quan gira. El manager
// el passa a Surface::render.
// Flip del sprite, propagat des del motor d'animació
// (`AnimationComponent::flipMode()`): cap entitat el decideix per
// direcció — el YAML declara `flip_h: true` a l'animació simètrica
// (p. ex. `walk_west`) i el setter d'animació l'aplica al `flip_`.
// El manager el passa a `Surface::render`.
[[nodiscard]] auto getFlip() const -> SDL_FlipMode { return flip_; }
void setFlip(SDL_FlipMode flip) { flip_ = flip; }
+4
View File
@@ -65,6 +65,10 @@ void EntityManager::setExpectedPlayerCount(int n) {
level_complete_emitted_ = false;
}
void EntityManager::forceOpenDoor() {
if (door_ != nullptr) { door_->open(); }
}
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
if (entity == nullptr) { return nullptr; }
entity->setEventBus(&bus_);
+7
View File
@@ -112,6 +112,13 @@ class EntityManager {
[[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; }
[[nodiscard]] auto exitedPlayerCount() const -> std::size_t { return exited_players_; }
// Força l'obertura de la porta sense que els sarcòfags estiguin
// oberts. Pensat com a hook de debug per a la GameScene: el flux
// posterior (creuar la porta → `player_exited` → `level_complete`
// → OUTRO) segueix exactament igual perquè depèn de Door, no de
// sarcs. No-op si la porta ja està oberta o si el nivell no en té.
void forceOpenDoor();
private:
EventBus bus_;
std::vector<std::unique_ptr<Entity>> entities_;
@@ -65,7 +65,10 @@ void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
const auto BOX = entity->getCollisionRect();
const int E_HEIGHT = entity->getHeight();
for (auto* p : players) {
if (p == nullptr || p->isExited()) { continue; }
// Filtrem també DYING: el jugador en cicle de mort no rep
// contacte amb hazards (un segon toc de mòmia mentre la roda
// ja és no-op; cap altra entitat l'ha de fer transicionar).
if (p == nullptr || p->isOutOfPlay() || p->isDying()) { continue; }
// Filtre per altura: si el jugador està en l'aire i està per
// damunt de l'alçada del cos de l'entitat, no hi ha contacte.
// És el mecanisme del salt per esquivar momies, roques, etc.
+29 -1
View File
@@ -1,5 +1,7 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstddef>
#include <cstdint>
#include <functional>
@@ -12,6 +14,7 @@
// Forward: els events poden dur un punter a `Entity` (p. ex. quin sarcòfag
// s'ha obert). No cal incloure el header complet aquí.
class Entity;
class Player;
// Event pub/sub minimalista. Hi ha un únic `EventBus` per mundus (propietat
// de l'`EntityManager`); les entitats que el necessiten en reben un punter
@@ -49,9 +52,31 @@ struct WorldPos {
float y{0.0F};
};
// Payload de `whip_spawn`: el `Player` el publica quan entra a ATTACKING
// i el `GameScene` el consumeix per crear un `WhipAttack` via
// `EntityManager::add`. Així el Player no depèn ni de `EntityManager` ni de
// `WhipAttack`, i el contracte queda dins del bus.
// - `owner` : Player que ha llançat l'atac (no rep dany propi).
// - `direction` : Player::Cardinal castat a int (1=N, 2=S, 3=E, 4=W) per
// evitar dependència cíclica al header.
// - `owner_box` : pixel_rect del jugador (típicament 16×32 a AEE). El
// WhipAttack en deriva tant la `draw_rect_` (sprite del
// látigo, més gran que el jugador per cobrir wind-up i
// solapament) com la `damage_rect_` (caixa de dany al
// nivell dels peus, retallada per murs/sarcs).
// - `range_px` : longitud nominal de la hitbox de dany abans de
// retallar contra murs i sarcòfags. Llegida del YAML
// del jugador.
struct WhipSpawnPayload {
Player* owner{nullptr};
int direction{0};
SDL_FRect owner_box{0.0F, 0.0F, 0.0F, 0.0F};
float range_px{0.0F};
};
struct Event {
std::string name;
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile, WorldPos>;
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile, WorldPos, WhipSpawnPayload>;
Payload payload;
// Jugador que ha originat (o provocat) l'event. -1 = sense jugador
// associat (events del món, com `sarcophagus_opened` des de la pròpia
@@ -101,6 +126,9 @@ class EventBus {
void publish(const std::string& name, WorldPos pos) {
publish(Event{.name = name, .payload = pos});
}
void publish(const std::string& name, const WhipSpawnPayload& whip) {
publish(Event{.name = name, .payload = whip});
}
private:
struct Subscription {
+464 -90
View File
@@ -9,6 +9,7 @@
#include <utility>
#include "core/audio/audio.hpp"
#include "core/input/gamepad_bindings.hpp"
#include "core/input/gamepad_manager.hpp"
#include "core/input/input_state.hpp"
#include "core/rendering/screen.hpp"
@@ -16,6 +17,7 @@
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "game/entities/event_bus.hpp"
#include "game/options.hpp"
#include "utils/defaults.hpp"
namespace {
@@ -68,6 +70,8 @@ Player::TurnConfig Player::turn_config;
Player::InertiaConfig Player::inertia_config;
Player::JumpConfig Player::jump_config;
Player::HurtConfig Player::hurt_config;
Player::WhipConfig Player::whip_config;
Player::DeathConfig Player::death_config;
void Player::loadFrom(const std::string& yaml_path) {
auto bytes = Resource::Helper::loadFile(yaml_path);
@@ -114,6 +118,16 @@ void Player::loadFrom(const std::string& yaml_path) {
// Bloc opcional `hurt:` — estat post-impacte. Si manca o el speed
// és 0, l'estat HURT mai s'activa (cas degenerat).
readHurtBlock(yaml);
// Bloc opcional `whip:` — paràmetres del látigo. Si manca, el
// jugador no atacarà efectivament (ATTACKING dura 0 ms i el flanc
// d'input es perd).
readWhipBlock(yaml);
// Bloc opcional `death:` — pausa de respawn i comptador inicial de
// vides. Si manca, el respawn és immediat i les vides queden a 3
// per defecte (no és un valor crític: avui no hi ha game over).
readDeathBlock(yaml);
}
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
@@ -173,25 +187,53 @@ void Player::readHurtBlock(const fkyaml::node& yaml) {
const auto& h = yaml["hurt"];
if (h.contains("speed")) { hurt_config.initial_speed_px_per_s = h["speed"].get_value<float>(); }
if (h.contains("brake_time_ms")) { hurt_config.brake_time_ms = h["brake_time_ms"].get_value<int>(); }
// So d'entrada a HURT — mateix patró que `jump.sound` i `whip.sound`.
if (h.contains("sound")) {
hurt_config.sound_key = h["sound"].get_value<std::string>();
Resource::Cache::get().loadSound(hurt_config.sound_key, "data/sounds/" + hurt_config.sound_key);
}
}
void Player::readDeathBlock(const fkyaml::node& yaml) {
death_config = DeathConfig{};
if (!yaml.contains("death")) { return; }
const auto& d = yaml["death"];
if (d.contains("respawn_delay_ms")) { death_config.respawn_delay_ms = d["respawn_delay_ms"].get_value<int>(); }
if (d.contains("lives")) { death_config.initial_lives = d["lives"].get_value<int>(); }
}
void Player::readWhipBlock(const fkyaml::node& yaml) {
whip_config = WhipConfig{};
if (!yaml.contains("whip")) { return; }
const auto& w = yaml["whip"];
if (w.contains("range_tiles")) { whip_config.range_tiles = w["range_tiles"].get_value<float>(); }
if (w.contains("swing_ms")) { whip_config.swing_ms = w["swing_ms"].get_value<int>(); }
if (w.contains("cooldown_ms")) { whip_config.cooldown_ms = w["cooldown_ms"].get_value<int>(); }
if (w.contains("damage")) { whip_config.damage = w["damage"].get_value<int>(); }
// So del swing: mateix patró que `jump.sound` — basename val de
// clau al cache i de path. Si manca, queda buit i no es dispara.
if (w.contains("sound")) {
whip_config.sound_key = w["sound"].get_value<std::string>();
Resource::Cache::get().loadSound(whip_config.sound_key, "data/sounds/" + whip_config.sound_key);
}
}
void Player::applyCardinal(Cardinal c) {
// El flip del sprite el governa l'animació al motor: només cal posar
// el `facing_` que tocaria; `animationName()` ja resol el sufix
// (`_west` per Cardinal::WEST) i el YAML hi declara `flip_h: true`.
switch (c) {
case Cardinal::NORTH:
facing_ = Facing::UP;
setFlip(SDL_FLIP_NONE);
facing_ = Facing::NORTH;
break;
case Cardinal::SOUTH:
facing_ = Facing::DOWN;
setFlip(SDL_FLIP_NONE);
facing_ = Facing::SOUTH;
break;
case Cardinal::EAST:
facing_ = Facing::SIDE;
setFlip(SDL_FLIP_NONE);
facing_ = Facing::EAST;
break;
case Cardinal::WEST:
facing_ = Facing::SIDE;
setFlip(SDL_FLIP_HORIZONTAL);
facing_ = Facing::WEST;
break;
case Cardinal::NONE:
break;
@@ -209,10 +251,10 @@ auto Player::cardinalFromInput(float vx, float vy) -> Cardinal {
}
auto Player::cardinalFromString(const std::string& s) -> Cardinal {
if (s == "north" || s == "up") { return Cardinal::NORTH; }
if (s == "south" || s == "down") { return Cardinal::SOUTH; }
if (s == "east" || s == "right") { return Cardinal::EAST; }
if (s == "west" || s == "left") { return Cardinal::WEST; }
if (s == "north") { return Cardinal::NORTH; }
if (s == "south") { return Cardinal::SOUTH; }
if (s == "east") { return Cardinal::EAST; }
if (s == "west") { return Cardinal::WEST; }
throw std::runtime_error("Player: cardinal desconegut '" + s + "' al bloc turn.transitions");
}
@@ -286,17 +328,14 @@ void Player::updateFacingAndTurn(float delta_time, float vx, float vy) {
turning_remaining_ms_ = static_cast<float>(turn_config.duration_ms);
applyCardinal(intermediate_cardinal_);
} else if (vx != 0.0F) {
// Tria de facing original: prevalença lateral, flip segons el
// signe de vx; verticals purs van a UP/DOWN amb flip NONE; en
// aturar-se conservem `facing_` per a l'animació stand.
facing_ = Facing::SIDE;
setFlip(vx < 0.0F ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
// Tria de facing: prevalença lateral, EAST/WEST segons el signe
// de vx; verticals purs van a NORTH/SOUTH. En aturar-se
// conservem `facing_` per a l'animació stand.
facing_ = (vx < 0.0F) ? Facing::WEST : Facing::EAST;
} else if (vy < 0.0F) {
facing_ = Facing::UP;
setFlip(SDL_FLIP_NONE);
facing_ = Facing::NORTH;
} else if (vy > 0.0F) {
facing_ = Facing::DOWN;
setFlip(SDL_FLIP_NONE);
facing_ = Facing::SOUTH;
}
}
@@ -315,11 +354,18 @@ Player::Player(const std::string& anim_key, int id, float x, float y, const Cont
paint_tile_index_(paint_tile_index) {
setPos(x, y);
// Postura inicial mirant cap al jugador (cara avall), no de costat:
// forcem facing_ a DOWN i l'animació `stand_down` abans del primer
// forcem facing_ a SOUTH i l'animació `stand_south` abans del primer
// frame d'input — el primer `update` la preservarà si l'usuari no
// prem cap tecla.
facing_ = Facing::DOWN;
setCurrentAnimation("stand_down");
facing_ = Facing::SOUTH;
setCurrentAnimation("stand_south");
// Comptador d'intents inicialitzat amb el valor del YAML (default 3
// si el bloc `death:` manca). Fora del cicle DYING no toca a ningú.
lives_remaining_ = death_config.initial_lives;
// Spawn point inicial: la posició del ctor val per defecte; el
// GameScene el reescriu via `setSpawnPoint` quan canvia de sala.
spawn_x_ = x;
spawn_y_ = y;
}
void Player::update(float delta_time) {
@@ -328,10 +374,10 @@ void Player::update(float delta_time) {
}
auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
// EXITED: jugador despawnat. No xoca amb res ni res xoca amb ell.
// També salta jugadors EXITED del check actor↔actor més avall via
// el filtre de `isExited` (vegeu el predicat any_of).
if (state_ == State::EXITED) { return false; }
// EXITED/DISABLED: jugador fora del joc. No xoca amb res ni res
// xoca amb ell. També es filtren els altres jugadors fora de joc
// al check actor↔actor més avall via el predicat `isOutOfPlay`.
if (isOutOfPlay()) { return false; }
if (map_.collides(box)) { return true; }
if (all_players_ == nullptr) { return false; }
// Per al contacte actor-actor usem la hit-box petita a banda i banda:
@@ -355,9 +401,13 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
const bool MY_AIR = (state_ == State::AIRBORNE);
const bool MY_HURT = (state_ == State::HURT);
const float MY_Z = jump_z_;
return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT, MY_AIR, MY_HURT, MY_Z](const Player* p) {
const bool MY_DYING = (state_ == State::DYING);
return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT, MY_AIR, MY_HURT, MY_DYING, MY_Z](const Player* p) {
if (p == nullptr || p == this) { return false; }
if (p->isExited()) { return false; }
if (p->isOutOfPlay()) { return false; }
// DYING és intangible per a PvP: el jugador mort no actua de
// mur, i un altre jugador en DYING no pot empentar tampoc.
if (MY_DYING || p->isDying()) { return false; }
if (MY_HURT || p->isHurt()) { return false; }
if (MY_AIR != p->isAirborne()) { return false; }
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
@@ -407,10 +457,30 @@ void Player::integrateAxis(float& phys, float input, float delta_time, float mot
// llegibilitat. La complexitat puja 1 per la guarda EXITED.
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
void Player::tick(float delta_time) {
// EXITED: jugador despawnat, no fa res. Cap input, cap física, cap
// bus event. Continua sent al vector de jugadors per simplicitat
// (no s'esborra) però queda completament inert fins al respawn.
if (state_ == State::EXITED) { return; }
// EXITED/DISABLED: jugador fora del joc, no fa res. Cap input, cap
// física, cap bus event. Continua sent al vector de jugadors per
// simplicitat (no s'esborra) però queda completament inert fins
// que GameScene el reactiva (respawn / spawnFromAir).
if (isOutOfPlay()) { return; }
// DYING: animació `death_spinning` + pausa de respawn. Ignorem tot
// l'input (ni moviment, ni salt, ni atac). El moviment queda
// congelat al lloc de la mort; `applyMovement` avança l'animació i
// detecta la fi per disparar el respawn. Mantenim els flancs de
// botó actualitzats perquè una tecla sostinguda no es comporte com
// a flanc nou en sortir del cicle.
if (state_ == State::DYING) {
(void)consumeJumpEdge();
(void)consumeAttackEdge();
input_vx_ = 0.0F;
input_vy_ = 0.0F;
intended_vx_ = 0.0F;
intended_vy_ = 0.0F;
motion_time_ms_ = 0.0F;
setVelX(0.0F);
setVelY(0.0F);
return;
}
// Reset per frame: el resolver del GameScene el tornarà a posar a
// true si aquest frame hi ha push actiu. Així quan el push acaba
@@ -439,6 +509,7 @@ void Player::tick(float delta_time) {
// durant el stun: així si l'usuari l'ha estat prement abans, no
// se'l consumeix com a salt nou en sortir del stun.
(void)consumeJumpEdge();
(void)consumeAttackEdge();
return;
}
@@ -448,11 +519,36 @@ void Player::tick(float delta_time) {
// pulsació quan torne a terra es detecte com a flanc nou.
if (state_ == State::AIRBORNE) {
(void)consumeJumpEdge();
(void)consumeAttackEdge();
setVelX(intended_vx_);
setVelY(intended_vy_);
return;
}
// En ATTACKING el jugador queda bloquejat tot el `swing_ms +
// cooldown_ms`: ni input direccional, ni salt, ni atac, ni canvi
// de facing. El timer descompta amb `delta_time * 1000` per
// mantenir la convenció de delta_ms de la resta d'estats temporals.
// Mantenim els flancs dels botons actualitzats perquè una
// tecla mantinguda no es comporte com a flanc nou en sortir.
if (state_ == State::ATTACKING) {
(void)consumeJumpEdge();
(void)consumeAttackEdge();
attacking_remaining_ms_ -= delta_time * 1000.0F;
if (attacking_remaining_ms_ <= 0.0F) {
attacking_remaining_ms_ = 0.0F;
state_ = State::GROUNDED;
}
input_vx_ = 0.0F;
input_vy_ = 0.0F;
intended_vx_ = 0.0F;
intended_vy_ = 0.0F;
motion_time_ms_ = 0.0F;
setVelX(0.0F);
setVelY(0.0F);
return;
}
// En HURT el jugador està projectat sense control: descomptem
// velocitat per inèrcia (decaiment lineal fins a 0). Quan arriba a
// zero, tornem a GROUNDED i el frame següent ja torna a llegir input.
@@ -460,6 +556,7 @@ void Player::tick(float delta_time) {
// estats), que també gestiona el xoc contra murs.
if (state_ == State::HURT) {
(void)consumeJumpEdge();
(void)consumeAttackEdge();
const float DECEL = (hurt_config.brake_time_ms > 0)
? hurt_config.initial_speed_px_per_s / (static_cast<float>(hurt_config.brake_time_ms) / 1000.0F)
: 0.0F;
@@ -483,6 +580,12 @@ void Player::tick(float delta_time) {
return;
}
// Refresca les vinculacions des d'Options en calent: així un remap fet
// al menú de servei té efecte immediatament, sense esperar al pròxim
// GameScene::onEnter. Replica el comportament d'AEE
// (`KeyRemap::update` que llig `Options::keys_game` cada frame).
refreshControlsFromOptions();
// Via `InputState` en lloc de `SDL_GetKeyboardState` directe: quan el
// service menu (o qualsevol UI modal) està obert, el wrapper retorna
// un buffer tot-a-zero perquè el jugador no es mogui mentre l'usuari
@@ -534,6 +637,18 @@ void Player::tick(float delta_time) {
// només disparen amb intenció explícita del jugador.
updateFacingAndTurn(delta_time, input_vx_, input_vy_);
// Detecció de flanc del botó d'atac. Es processa ABANS del salt
// perquè ATTACKING i AIRBORNE són mútuament excloents i, davant un
// mateix-frame, l'atac té prioritat (millor castigar la pulsació
// doble per excés que perdre l'atac per un toc accidental al salt).
// En entrar a ATTACKING, descartem el flanc del salt per no que
// queda penjat per a quan es desbloque.
if (consumeAttackEdge() && whip_config.swing_ms > 0) {
enterAttacking(currentFacingCardinal());
(void)consumeJumpEdge();
return;
}
// Detecció de flanc del botó de salt. Es processa DESPRÉS d'aplicar
// el facing perquè el sprite del salt usa el facing del moment del
// despegue. `being_pushed_` no ho pot bloquejar ací (encara no s'ha
@@ -546,6 +661,18 @@ void Player::tick(float delta_time) {
}
}
void Player::refreshControlsFromOptions() {
if (id_ != 0 && id_ != 1) { return; }
const auto& kb = (id_ == 0) ? Options::controls().keyboard_p1
: Options::controls().keyboard_p2;
controls_.up = kb.up;
controls_.down = kb.down;
controls_.left = kb.left;
controls_.right = kb.right;
controls_.jump = kb.jump;
controls_.attack = kb.attack;
}
auto Player::consumeJumpEdge() -> bool {
// Edge-detection independent per teclat i mando: si qualsevol dels
// dos passa de no-premut a premut aquest frame, comptem com a salt.
@@ -557,13 +684,103 @@ auto Player::consumeJumpEdge() -> bool {
jump_held_keyboard_ = KEY_NOW;
SDL_Gamepad* pad = GamepadManager::get().getForPlayer(id_);
const bool PAD_NOW = (pad != nullptr) && InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_SOUTH);
const auto& slot = (id_ == 0) ? Options::controls().gamepad_p1
: Options::controls().gamepad_p2;
const int JUMP_BTN = GamepadBindings::get().getButton(slot.name, slot.path, GamepadBindings::Action::JUMP);
const bool PAD_NOW = (pad != nullptr) && !InputState::isBlocked() &&
GamepadBindings::isPressed(pad, JUMP_BTN);
if (PAD_NOW && !jump_held_gamepad_) { edge = true; }
jump_held_gamepad_ = PAD_NOW;
return edge;
}
auto Player::consumeAttackEdge() -> bool {
// Mateix patró que `consumeJumpEdge` però per al botó d'atac. El botó
// físic ve d'`GamepadBindings`, persistit per dispositiu — l'usuari
// l'ha pogut remapejar des del menú de servei.
const bool* keys = InputState::getKeyboardState();
const bool KEY_NOW = keys[controls_.attack];
bool edge = (KEY_NOW && !attack_held_keyboard_);
attack_held_keyboard_ = KEY_NOW;
SDL_Gamepad* pad = GamepadManager::get().getForPlayer(id_);
const auto& slot = (id_ == 0) ? Options::controls().gamepad_p1
: Options::controls().gamepad_p2;
const int ATK_BTN = GamepadBindings::get().getButton(slot.name, slot.path, GamepadBindings::Action::ATTACK);
const bool PAD_NOW = (pad != nullptr) && !InputState::isBlocked() &&
GamepadBindings::isPressed(pad, ATK_BTN);
if (PAD_NOW && !attack_held_gamepad_) { edge = true; }
attack_held_gamepad_ = PAD_NOW;
return edge;
}
auto Player::currentFacingCardinal() const -> Cardinal {
switch (facing_) {
case Facing::NORTH:
return Cardinal::NORTH;
case Facing::SOUTH:
return Cardinal::SOUTH;
case Facing::EAST:
return Cardinal::EAST;
case Facing::WEST:
return Cardinal::WEST;
}
return Cardinal::EAST;
}
void Player::enterAttacking(Cardinal facing) {
state_ = State::ATTACKING;
attacking_remaining_ms_ = static_cast<float>(whip_config.swing_ms + whip_config.cooldown_ms);
// Garantim que facing/flip i cardinal queden coherents amb la
// direcció triada (per si l'atac s'ha disparat amb facing residual,
// p.ex. just després d'un canvi d'input).
applyCardinal(facing);
if (facing == Cardinal::NONE) { return; }
// So del swing — disparat ací (no des del WhipAttack) perquè és el
// jugador qui executa l'acció; si demà el spawn falla per qualsevol
// motiu, el so del gest segueix sonant.
if (!whip_config.sound_key.empty() && Audio::get() != nullptr) {
Audio::get()->playSound(whip_config.sound_key, Audio::Group::GAME);
}
// Mapa de Cardinal → int al payload (vegeu `WhipSpawnPayload`).
int dir_int = 0;
switch (facing) {
case Cardinal::NORTH:
dir_int = 1;
break;
case Cardinal::SOUTH:
dir_int = 2;
break;
case Cardinal::EAST:
dir_int = 3;
break;
case Cardinal::WEST:
dir_int = 4;
break;
case Cardinal::NONE:
return;
}
// Passem el pixel_rect (16×32 a sam/morcus) al payload; el WhipAttack
// en deriva tant la draw_rect (sprite del látigo, més gran) com la
// damage_rect (al nivell dels peus, retallada per bloquejadors).
if (bus_ != nullptr) {
const float RANGE_PX = whip_config.range_tiles * static_cast<float>(Map::tile_size);
bus_->publish("whip_spawn", WhipSpawnPayload{
.owner = this,
.direction = dir_int,
.owner_box = getPosition(),
.range_px = RANGE_PX,
});
}
}
void Player::startJump() {
// No saltem si l'estat no ho permet. `being_pushed_` no és segur de
// consultar ací (es pot haver marcat o no, segons l'ordre del frame),
@@ -576,7 +793,7 @@ void Player::startJump() {
// Un gir de 180° en curs es compromet immediatament: si l'usuari
// ha disparat un giro (oest→est, p.ex.) i salta abans que s'esgote
// el timer, l'animació del salt ha de mostrar la direcció final
// (jump lateral), no la intermèdia (jump_down). Aplem el pending
// (jump lateral), no la intermèdia (jump_south). Aplem el pending
// ara mateix i resetem l'estat del gir. Si no hi havia gir en
// curs, és no-op.
if (turning_remaining_ms_ > 0.0F) {
@@ -671,6 +888,9 @@ void Player::enterHurt(Cardinal preferred) {
// Activem l'estat i fixem la velocitat segons la direcció triada.
state_ = State::HURT;
if (!hurt_config.sound_key.empty() && Audio::get() != nullptr) {
Audio::get()->playSound(hurt_config.sound_key, Audio::Group::GAME);
}
applyCardinal(CHOSEN);
last_input_cardinal_ = CHOSEN;
turning_remaining_ms_ = 0.0F;
@@ -696,7 +916,15 @@ auto Player::integrateJump(float delta_time) -> bool {
state_ = State::LANDING_STUN;
landing_stun_remaining_ms_ = static_cast<float>(jump_config.landing_stun_ms);
if (!jump_config.land_sound_key.empty() && Audio::get() != nullptr) {
// Consumeix el preset armat per GameScene al spawn aeri
// d'INTRO (single-shot): el primer LAND el sona reverberat,
// els salts següents cauen al camí sec.
if (next_land_reverb_preset_.empty()) {
Audio::get()->playSound(jump_config.land_sound_key, Audio::Group::GAME);
} else {
Audio::get()->playSoundWithReverb(jump_config.land_sound_key, next_land_reverb_preset_, Audio::Group::GAME);
next_land_reverb_preset_.clear();
}
}
if (bus_ != nullptr) {
bus_->publish(Event{
@@ -719,6 +947,11 @@ void Player::resolveLandingOverlap() {
if (all_players_ == nullptr) { return; }
for (Player* other : *all_players_) {
if (other == nullptr || other == this) { continue; }
// Salta jugadors fora del joc: un caigut sobre un slot DISABLED
// (o sobre un EXITED encara al vector) no els ha de projectar.
// També DYING — un jugador en cicle de mort no rep cap més
// efecte fins que respawneja.
if (other->isOutOfPlay() || other->isDying()) { continue; }
// Test amb la hit-box (8×8 de contacte precís), no la base-box.
// Coherent amb la resta de col·lisions actor-actor del joc:
// momies, fuetades futures, resolver PvP… tots usen hit-box. Un
@@ -753,8 +986,8 @@ void Player::resolveLandingOverlap() {
}
void Player::applyMovement(float delta_time) {
// EXITED: cap moviment ni col·lisió. Idèntic al gating de `tick`.
if (state_ == State::EXITED) { return; }
// EXITED/DISABLED: cap moviment ni col·lisió. Idèntic al gating de `tick`.
if (isOutOfPlay()) { return; }
// Integració del salt: si estem AIRBORNE, avança jump_z_/jump_vz_.
// El facing i intended_v* es mantenen del moment del despegue (no es
@@ -766,6 +999,11 @@ void Player::applyMovement(float delta_time) {
resolveLandingOverlap();
}
if (state_ == State::DYING) {
applyDyingMovement(delta_time);
return;
}
if (state_ == State::HURT) {
applyHurtMovement(delta_time);
return;
@@ -776,6 +1014,15 @@ void Player::applyMovement(float delta_time) {
return;
}
if (state_ == State::ATTACKING) {
// Cap moviment durant ATTACKING. Animem (perquè avance la
// seqüència `attack_*`) i actualitzem la clau d'animació segons
// la sub-fase (swing vs cooldown).
animate(delta_time);
setCurrentAnimation(animationName(false));
return;
}
// El resolver del GameScene pot haver escalat `intended_vx_/vy_`
// (cas push: velocitat * push_factor) o haver-les anul·lat (cas
// head-on). Consumim-les aquí com a velocitat definitiva del frame.
@@ -828,6 +1075,75 @@ void Player::applyMovement(float delta_time) {
if (state_ == State::GROUNDED) { publishTileEnteredIfChanged(); }
}
void Player::enterDying() {
// Idempotent. Un segon contacte de momia mentre el jugador ja està
// en DYING no reinicia l'animació ni el comptador. Tampoc disparem
// el cicle si el jugador està fora de joc (EXITED al passar la
// porta, DISABLED durant les transicions de sala).
if (state_ == State::DYING || isOutOfPlay()) { return; }
state_ = State::DYING;
intended_vx_ = 0.0F;
intended_vy_ = 0.0F;
input_vx_ = 0.0F;
input_vy_ = 0.0F;
motion_time_ms_ = 0.0F;
being_pushed_ = false;
jump_z_ = 0.0F;
jump_vz_ = 0.0F;
landing_stun_remaining_ms_ = 0.0F;
attacking_remaining_ms_ = 0.0F;
turning_remaining_ms_ = 0.0F;
pending_cardinal_ = Cardinal::NONE;
intermediate_cardinal_ = Cardinal::NONE;
last_input_cardinal_ = Cardinal::NONE;
setVelX(0.0F);
setVelY(0.0F);
death_animation_complete_ = false;
respawn_remaining_ms_ = 0.0F;
setCurrentAnimation("death_spinning");
// So del cop letal: reaprofitem `hurt.wav` (ja precarregat per
// `readHurtBlock`). El bloc `death:` no té camp de so propi —
// semànticament l'impacte mortal és la versió forta de `hurt`.
if (!hurt_config.sound_key.empty() && Audio::get() != nullptr) {
Audio::get()->playSound(hurt_config.sound_key, Audio::Group::GAME);
}
}
void Player::applyDyingMovement(float delta_time) {
// Avancem l'animació; el sprite no es mou (la posició queda
// congelada al punt de la mort). `animationIsCompleted()` flanqueja
// un cop la roda arriba al darrer frame (loop:false). Llavors
// descomptem `respawn_delay_ms` mantenint el sprite al darrer frame
// i, en arribar a 0, disparem el respawn.
animate(delta_time);
if (!death_animation_complete_ && animationIsCompleted()) {
death_animation_complete_ = true;
respawn_remaining_ms_ = static_cast<float>(death_config.respawn_delay_ms);
}
if (death_animation_complete_) {
respawn_remaining_ms_ -= delta_time * 1000.0F;
if (respawn_remaining_ms_ <= 0.0F) {
respawnFromDying();
}
}
}
void Player::respawnFromDying() {
// Vides: decreix sempre. Pot anar a negatius — no hi ha game over.
--lives_remaining_;
death_animation_complete_ = false;
respawn_remaining_ms_ = 0.0F;
// Cinemàtica idèntica a la d'INTRO de sala: el jugador cau des de
// dalt fins al passadís d'spawn (`spawnFromAir` reseteja primer i
// després posa AIRBORNE amb l'altura derivada de la geometria).
// Així el respawn post-mort comparteix tota la sensació visual i
// física del primer spawn — incloent el LANDING_STUN final.
spawnFromAir(spawn_x_, spawn_y_);
}
void Player::applyHurtMovement(float delta_time) {
// En HURT el jugador es desplaça segons `intended_v*` (escrit a
// `enterHurt` i decremental a `tick`). Si `tryMove` no avança
@@ -875,9 +1191,17 @@ void Player::publishTileEnteredIfChanged() {
}
void Player::render() {
// EXITED: invisible. La crida al render base es salta sencera; el
// jugador no apareix al z-sort tot i seguir vivint al vector.
if (state_ == State::EXITED) { return; }
// EXITED/DISABLED: invisible. La crida al render base es salta
// sencera; el jugador no apareix al z-sort tot i seguir vivint al
// vector.
if (isOutOfPlay()) { return; }
// Limbo post-mort: durant `respawn_delay_ms` entre la fi de
// `death_spinning` i el `spawnFromAir`, el jugador queda invisible.
// Així el cicle complet és: animació rodant → desapareix → ressuscita
// caient des de dalt. Sense aquest gat, el sprite quedaria pintat al
// darrer frame de la roda mentre passa el delay.
if (state_ == State::DYING && death_animation_complete_) { return; }
// Mentre el jugador està enlaire, el sprite es desplaça visualment
// cap amunt `jump_z_` píxels. Modifiquem `pos_.y` (la posició entera
@@ -933,60 +1257,56 @@ void Player::renderShadow() const {
Screen::get().blendRect(SX, SY, SHADOW_W, SHADOW_H, SHADOW_ARGB, SHADOW_ALPHA);
}
auto Player::animationName(bool moving) const -> const char* {
// En l'aire forcem la variant `jump_*` (per direcció), independent de
// si hi ha velocitat horitzontal: per al jugador, l'estat visual del
// salt és per direcció, no per velocitat.
if (state_ == State::AIRBORNE) {
switch (facing_) {
case Facing::UP:
return "jump_up";
case Facing::DOWN:
return "jump_down";
case Facing::SIDE:
break;
// Sufix d'animació per cardinal — empat 1:1 amb l'enum `Facing` i el
// sufix declarat als YAMLs de jugador (sam.yaml, morcus.yaml).
auto Player::facingSuffix(Facing facing) -> const char* {
switch (facing) {
case Facing::NORTH:
return "_north";
case Facing::SOUTH:
return "_south";
case Facing::EAST:
return "_east";
case Facing::WEST:
return "_west";
}
return "_east";
}
// Prefix d'animació per estat. ATTACKING té dues sub-fases: durant
// `swing_ms` és `attack_*` (animació del braç llançant el látigo); durant
// `cooldown_ms` posterior, `recover_*`. A AIRBORNE/LANDING_STUN/HURT, la
// variant és per direcció, no per velocitat: l'estat bloqueja el `moving`
// argument i no fa servir walk/stand.
auto Player::statePrefix(State state, bool moving, bool in_attack_swing) -> const char* {
switch (state) {
case State::AIRBORNE:
return "jump";
}
// En LANDING_STUN, `stun_*` (per direcció): mostrem el frame
// d'aterratge fins que s'esgota el timer. La direcció ve fixada del
// moment del despegue (no es canvia mentre estem en l'aire).
if (state_ == State::LANDING_STUN) {
switch (facing_) {
case Facing::UP:
return "stun_up";
case Facing::DOWN:
return "stun_down";
case Facing::SIDE:
break;
}
case State::LANDING_STUN:
return "stun";
}
// En HURT, `hurt_*` (per direcció): el jugador llisca sense
// control en la direcció triada per `enterHurt`. Mateixa estructura
// que walk per facilitat (ara mateix els frames són clons).
if (state_ == State::HURT) {
switch (facing_) {
case Facing::UP:
return "hurt_up";
case Facing::DOWN:
return "hurt_down";
case Facing::SIDE:
break;
}
case State::HURT:
return "hurt";
}
switch (facing_) {
case Facing::UP:
return moving ? "walk_up" : "stand_up";
case Facing::DOWN:
return moving ? "walk_down" : "stand_down";
case Facing::SIDE:
case State::ATTACKING:
return in_attack_swing ? "attack" : "recover";
case State::DYING:
// L'animació concreta (`death_spinning`) la fixa `enterDying`;
// `applyDyingMovement` no torna a cridar `setCurrentAnimation`,
// així que aquest valor no s'usa mai. El retornem només per
// exhaustivitat del switch.
return "death";
case State::GROUNDED:
case State::EXITED:
case State::DISABLED:
break;
}
return moving ? "walk" : "stand";
}
auto Player::animationName(bool moving) const -> std::string {
const bool IN_ATTACK_SWING = (attacking_remaining_ms_ > static_cast<float>(whip_config.cooldown_ms));
return std::string(statePrefix(state_, moving, IN_ATTACK_SWING)) + facingSuffix(facing_);
}
auto Player::getCurrentTile() const -> std::pair<int, int> {
// El grid del tilemap és de `tile_size` píxels i està 16-alineat
// respecte a l'origen del tilemap (pot ser negatiu per overscan). El
@@ -1040,20 +1360,73 @@ void Player::respawn(float x, float y) {
jump_z_ = 0.0F;
jump_vz_ = 0.0F;
landing_stun_remaining_ms_ = 0.0F;
attacking_remaining_ms_ = 0.0F;
turning_remaining_ms_ = 0.0F;
pending_cardinal_ = Cardinal::NONE;
intermediate_cardinal_ = Cardinal::NONE;
last_input_cardinal_ = Cardinal::NONE;
// Flags del cicle DYING (que `respawnFromDying` també neteja, però
// si algú crida `respawn` directament des d'un altre estat, han de
// quedar coherents per al pròxim cicle de mort).
death_animation_complete_ = false;
respawn_remaining_ms_ = 0.0F;
// Reseteig del facing al d'spawn original (mirant cap a la càmera):
// sense això, un EXITED que entrava per la porta amb facing UP
// sense això, un EXITED que entrava per la porta amb facing NORTH
// re-aparix d'esquena al pròxim cicle. Mateixa lògica que el ctor.
facing_ = Facing::DOWN;
setFlip(SDL_FLIP_NONE);
setCurrentAnimation("stand_down");
facing_ = Facing::SOUTH;
setCurrentAnimation("stand_south");
setPosX(x);
setPosY(y);
}
void Player::disable() {
if (state_ == State::DISABLED) { return; }
state_ = State::DISABLED;
intended_vx_ = 0.0F;
intended_vy_ = 0.0F;
input_vx_ = 0.0F;
input_vy_ = 0.0F;
being_pushed_ = false;
jump_z_ = 0.0F;
jump_vz_ = 0.0F;
landing_stun_remaining_ms_ = 0.0F;
turning_remaining_ms_ = 0.0F;
pending_cardinal_ = Cardinal::NONE;
intermediate_cardinal_ = Cardinal::NONE;
last_input_cardinal_ = Cardinal::NONE;
}
void Player::spawnFromAir(float x, float y) {
// Net total: equival a un `respawn` seguit d'una entrada en l'aire.
// Així el flag de gir, l'animació, els flancs del salt i les
// velocitats residuals queden a un estat conegut abans de promocionar
// a AIRBORNE. Si hi havia un cardinal d'input penjat de la sessió
// anterior, no contamina la pròxima caiguda.
respawn(x, y);
// Altura derivada de la geometria: prou per arrencar amb el sprite
// sencer per damunt del bord superior del canvas (recordem que
// visualment es pinta a `pos_.y - jump_z_`). Afegim un xicotet marge
// perquè el primer frame ja siga clarament off-screen, no just al ras.
constexpr float OFFSCREEN_MARGIN_PX = 8.0F;
const float SPAWN_HEIGHT = y + static_cast<float>(sprite_h) + OFFSCREEN_MARGIN_PX;
state_ = State::AIRBORNE;
jump_z_ = SPAWN_HEIGHT;
// Caiguda recta: sense impuls inicial, deixem que la gravetat (la
// mateixa que la del salt normal) faça la feina. Velocitat horitzontal
// a zero perquè no es desviï del passadís d'spawn.
jump_vz_ = 0.0F;
intended_vx_ = 0.0F;
intended_vy_ = 0.0F;
// Mirant cap avall durant la caiguda — `animationName(AIRBORNE)` amb
// facing SOUTH ja resol a `jump_south`. `respawn` ja ha posat facing a
// SOUTH; ho deixem explícit ací per claredat de la cinemàtica.
facing_ = Facing::SOUTH;
setCurrentAnimation("jump_south");
}
auto Player::getBaseBox() const -> SDL_FRect {
return {
.x = getPosX() + static_cast<float>(collision_x_offset),
@@ -1144,6 +1517,7 @@ auto Player::tryPushSnapBlockers(
const SDL_FRect MY_HIT = getHitBox();
for (Player* p : *all_players_) {
if (p == nullptr || p == this) { continue; }
if (p->isOutOfPlay()) { continue; }
const SDL_FRect OTHER_HIT = p->getHitBox();
if (SDL_HasRectIntersectionFloat(&MY_HIT, &OTHER_HIT)) {
blockers.push_back(p);
+231 -15
View File
@@ -24,6 +24,7 @@ class Player : public AnimatedSprite {
SDL_Scancode left;
SDL_Scancode right;
SDL_Scancode jump;
SDL_Scancode attack;
};
// Estat d'alt nivell del jugador. Ortogonal a `Facing`/`Cardinal`
@@ -44,12 +45,36 @@ class Player : public AnimatedSprite {
AIRBORNE,
LANDING_STUN,
HURT,
// Atacant amb látigo. Estat mútuament exclusiu amb AIRBORNE: no
// es pot atacar saltant, ni saltar atacant. Cobreix swing +
// cooldown internament: durant `whip_swing_ms` es spawneja
// l'entitat WhipAttack i corre l'animació `attack_*`; durant
// `whip_cooldown_ms` posterior, el jugador segueix bloquejat
// (no input, no salt, no atac) però visualment torna a
// `stand_*`. En total, ATTACKING dura `swing_ms + cooldown_ms`;
// en acabar, torna a GROUNDED.
ATTACKING,
// Jugador que ha eixit per la porta de la sala. Despawn lògic:
// no es renderitza, no processa input, no es mou, no col·lisiona
// amb res ni rep contacte d'entitats. Idempotent: les crides
// posteriors a `exit()` són no-op. El reset el porta `respawn()`
// (cridada per GameScene en transició OUTRO → INTRO).
EXITED,
// Slot de jugador inactiu: instanciat però fora de partida. Mateix
// perfil físic que EXITED (invisible, sense input, sense col·lisió,
// ignorat pel resolver PvP), però amb semàntica diferent: no
// compta com a "ha completat la sala" ni dispara `level_complete`.
// Usat per (a) el slot del segon jugador en single-player, (b)
// tots els slots durant les transicions de sala abans del spawn
// aeri, i (c) en el futur, l'estat post-mort abans del respawn.
DISABLED,
// Mort en curs: animació `death_spinning` (3 voltes horàries) +
// pausa `respawn_delay_ms` abans del respawn. Bloqueja input i
// moviment; no rep contacte amb hazards (cf. `isDying`). Quan el
// timer s'esgota, `respawn(spawn_x_, spawn_y_)` torna a GROUNDED
// i decrementa el comptador de vides (pot anar a negatius — no
// hi ha game over).
DYING,
};
// Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`).
@@ -184,6 +209,16 @@ class Player : public AnimatedSprite {
[[nodiscard]] auto isAirborne() const -> bool { return state_ == State::AIRBORNE; }
[[nodiscard]] auto isHurt() const -> bool { return state_ == State::HURT; }
[[nodiscard]] auto isExited() const -> bool { return state_ == State::EXITED; }
[[nodiscard]] auto isDisabled() const -> bool { return state_ == State::DISABLED; }
[[nodiscard]] auto isDying() const -> bool { return state_ == State::DYING; }
// Resum dels estats "fora del món": EXITED i DISABLED comparteixen el
// mateix perfil físic (no es pinten, no llegeixen input, no
// col·lisionen, no participen al resolver PvP). Tots dos gates de
// l'update i el render comproven aquesta combinació amb una sola
// crida per evitar oblits si demà afegim un tercer estat-fora-del-món.
[[nodiscard]] auto isOutOfPlay() const -> bool {
return state_ == State::EXITED || state_ == State::DISABLED;
}
// Tangible per a contactes jugador↔jugador: només els estats a terra
// amb cos sòlid. AIRBORNE i HURT són intangibles (es travessen).
// EXITED tampoc — el jugador ha eixit lògicament i ja no és al món.
@@ -213,12 +248,66 @@ class Player : public AnimatedSprite {
being_pushed_ = false;
}
// Mata el jugador d'un sol toc: transició a DYING, animació
// `death_spinning`, posició congelada, input ignorat. Idempotent —
// si ja està DYING, EXITED o DISABLED, és no-op (un segon toc de
// momia mentre la roda no la reinicia). Quan l'animació acaba i
// `respawn_delay_ms` expira, `respawn(spawn_x_, spawn_y_)` torna a
// GROUNDED i `lives_remaining_` decreix en 1 (pot anar a negatius).
void enterDying();
// Coordenades del spawn de respawn (cantó superior-esquerra del
// sprite, mateixa convenció que `respawn(x, y)`). El GameScene les
// assigna després de `computeSpawn` al ctor i a cada `loadRoom`. Sense
// això, el respawn enviaria el jugador a (0, 0).
void setSpawnPoint(float x, float y) {
spawn_x_ = x;
spawn_y_ = y;
}
// Comptador d'intents (estil Mario, NO HP). Decreix a cada respawn;
// no es clampa a 0 — permet anar a negatius perquè avui no hi ha
// game over. Públic per a HUD i tests.
[[nodiscard]] auto getLivesRemaining() const -> int { return lives_remaining_; }
// Reset complet a l'estat inicial: posició a (x, y), velocitats a 0,
// estat GROUNDED, qualsevol flag de control reinicialitzat. Cridat
// per GameScene a la transició ROOM_OUTRO → ROOM_INTRO. Idempotent
// (es pot cridar des de qualsevol estat).
// (es pot cridar des de qualsevol estat). Es manté disponible per a
// qualsevol cas futur on calgui aparèixer el jugador ja a terra
// sense la cinemàtica de caiguda; el flux estàndard d'arrencada de
// sala usa `disable()` + `spawnFromAir()`.
void respawn(float x, float y);
// Posa el jugador en el slot inactiu (DISABLED). Idempotent: la
// segona crida és no-op. Mateix patró que `exit()` per simetria.
// No publica res al bus — el GameScene decideix quan disparar el
// `spawnFromAir` posterior, i la transició DISABLED↔jugant és
// ortogonal a `level_complete`. Útil per:
// - el slot del segon jugador en single-player,
// - el període entre setState(ROOM_INTRO) i el trigger del spawn,
// - el futur estat post-mort (abans del respawn temporitzat).
void disable();
// Cinemàtica d'aparició: posiciona el jugador al seu tile final
// (x, y al sistema de coordenades del sprite) i el deixa en
// AIRBORNE a una altura derivada de la geometria del playfield —
// prou per arrencar fora del marc i caure recte fins al sòl. La
// velocitat horitzontal queda a zero (caiguda perfectament
// vertical). En tocar terra, el flux normal d'AIRBORNE→
// LANDING_STUN→GROUNDED el reactiva amb input. Reutilitzable per
// al respawn post-mort: mateixa entrada cinemàtica.
void spawnFromAir(float x, float y);
// Arma un preset de reverb (catàleg a `data/sounds.yaml`) per al
// pròxim aterratge d'aquest jugador. Pensat per a la cinemàtica
// d'INTRO de sala: GameScene crida aquesta funció just després de
// `spawnFromAir`, i el primer LAND consumeix el preset (queda buit
// immediatament). Els salts posteriors del joc, els respawns
// post-mort i el reset general no rearmen res. `preset` buit
// desactiva el reverb del proper LAND (no afecta cap LAND posterior).
void armNextLandReverb(std::string preset) { next_land_reverb_preset_ = std::move(preset); }
// Alçada actual del salt (píxels visuals per damunt del sòl).
// 0 quan està a terra. L'usa el render i el sistema de col·lisions
// per altura.
@@ -292,9 +381,12 @@ class Player : public AnimatedSprite {
void snapToHorizontalLane(float delta_time);
void snapToVerticalLane(float delta_time);
// Nom d'animació per al `facing_` actual. Si `moving` és cert s'usa
// la variant `walk*`, altrament la variant `stand*`.
[[nodiscard]] auto animationName(bool moving) const -> const char*;
// Nom d'animació per al `facing_` actual. Composat com a `prefix +
// suffix` via els dos helpers estàtics (declarats junt amb `Facing`
// més avall) — el prefix depèn de l'estat (walk/stand/jump/stun/hurt/
// attack/recover), el sufix del cardinal (`_north`/`_south`/`_east`/
// `_west`).
[[nodiscard]] auto animationName(bool moving) const -> std::string;
void snapToLane(
float delta_time,
float base,
@@ -315,6 +407,12 @@ class Player : public AnimatedSprite {
float (Player::*get_pos)() const,
void (Player::*set_pos)(float)) -> bool;
// Refresca `controls_` des d'`Options::controls()` cada frame. Replica
// el patró d'AEE on `KeyRemap::update` reflecteix Options::keys_game al
// buffer del teclat cada tick: així un remap fet al menú de servei té
// efecte immediat sense haver de tornar a entrar a la GameScene.
void refreshControlsFromOptions();
int id_{-1};
Controls controls_;
@@ -350,6 +448,14 @@ class Player : public AnimatedSprite {
// gestiona `intended_vx_/vy_` escalats pel resolver.
bool being_pushed_{false};
// Preset de reverb (catàleg de `data/sounds.yaml`) que el pròxim
// LAND ha de fer servir; buit = so sec. L'arma `armNextLandReverb`
// (cridada per GameScene al spawn aeri d'INTRO) i el consumeix
// `integrateJump` quan el jugador toca terra. Cap altre punt
// (respawn, exit, disable) el toca: així només sona reverberat el
// primer LAND de la cinemàtica d'entrada.
std::string next_land_reverb_preset_;
// Escala d'empenta usada per `snapToLane` quan descobreix un blocker
// (mateixa del resolver). 1.0 = sense escalar (comportament si
// GameScene no ha cridat setPushFactor).
@@ -361,25 +467,34 @@ class Player : public AnimatedSprite {
// reinstanciar-lo.
Map::Info map_;
// Direcció "cap on mira" el jugador. Governa quin conjunt d'animacions
// s'aplica (lateral, amunt, avall) i si el sprite es flipeja o no. Els
// moviments purament horitzontals o diagonals prenen `SIDE` (prevalença
// lateral), els purament verticals fan `UP`/`DOWN`. En aturar-se es
// preserva l'última orientació perquè la versió stand coincideixi.
enum class Facing : std::uint8_t { SIDE,
UP,
DOWN };
Facing facing_{Facing::SIDE};
// Direcció "cap on mira" el jugador. Una entrada per cada cardinal: el
// codi del joc no toca mai el flip — l'oest reprodueix `*_west`, que al
// YAML declara `flip_h: true`, i és el motor d'animació qui aplica el
// flip al sprite. Els moviments diagonals prenen el component lateral
// (EAST/WEST); en aturar-se es preserva l'última orientació perquè la
// versió stand coincideixi.
enum class Facing : std::uint8_t { EAST,
WEST,
NORTH,
SOUTH };
Facing facing_{Facing::EAST};
// Helpers de composició de `animationName`. Han de viure ací (després
// de la declaració de `Facing`) perquè la signatura el referencia.
static auto facingSuffix(Facing facing) -> const char*;
static auto statePrefix(State state, bool moving, bool in_attack_swing) -> const char*;
// Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°.
// El parell (Facing, flip) només té 3+1 estats; aquesta enum n'afegeix
// la 4a sense reorganitzar el sistema d'animacions.
// Pública perquè el `WhipAttack` la passa a `enterHurt` quan colpeja
// un altre jugador (friendly fire).
public:
enum class Cardinal : std::uint8_t { NONE,
NORTH,
SOUTH,
EAST,
WEST };
private:
// Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb
// `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i
// el comportament és instantani: soltar input para el jugador en sec.
@@ -440,12 +555,66 @@ class Player : public AnimatedSprite {
struct HurtConfig {
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
// Fitxer .wav reproduït en entrar a HURT (relatiu a `data/sounds/`).
// Mateixa convenció que la resta de sons del jugador: el basename val
// de clau al Resource::Cache i de path. Buit o absent = entrada
// silenciosa.
std::string sound_key;
};
static HurtConfig hurt_config;
// Llig el bloc `hurt:` del YAML i n'omple `hurt_config`.
static void readHurtBlock(const fkyaml::node& yaml);
// Configuració del látigo del jugador llegida del bloc `whip:` del
// YAML. `range_tiles` defineix la longitud de la hitbox abans de
// retallar; `swing_ms` és la durada de l'animació d'atac (des
// d'`enterAttacking` fins a desbloquejar el cooldown); `cooldown_ms`
// afegeix una fase posterior durant la qual el jugador segueix
// bloquejat però la hitbox ja no fa dany. `damage` queda llegit per
// a futur; per ara els objectius reben sempre `enterHurt`.
struct WhipConfig {
float range_tiles{0.0F};
int swing_ms{0};
int cooldown_ms{0};
int damage{0};
// Fitxer .wav reproduït al moment de disparar el swing (relatiu a
// `data/sounds/`). Convenció: el basename val tant de clau al
// Resource::Cache com de path. Buit o absent = swing silenciós.
std::string sound_key;
};
static WhipConfig whip_config;
// Llig el bloc `whip:` del YAML i n'omple `whip_config`. Si manca el
// bloc, els camps queden a 0 i el jugador no pot atacar (el flanc
// d'input es perd silenciosament).
static void readWhipBlock(const fkyaml::node& yaml);
// Configuració de mort/respawn llegida del bloc `death:` del YAML.
// `respawn_delay_ms` és la pausa entre la fi de `death_spinning` i la
// reaparició al spawn; `initial_lives` és el comptador d'intents amb
// què naix cada Player. Si el bloc manca, defaults segurs (0/3).
struct DeathConfig {
int respawn_delay_ms{0};
int initial_lives{3};
};
static DeathConfig death_config;
// Llig el bloc `death:` del YAML i n'omple `death_config`. Si manca,
// `respawn_delay_ms` queda a 0 (el respawn és immediat al final de
// l'animació) i `initial_lives` es manté al default (3).
static void readDeathBlock(const fkyaml::node& yaml);
// Activa l'estat ATTACKING en la direcció cardinal indicada (la
// direcció cap on mira el jugador) i publica `whip_spawn` al bus
// perquè el GameScene cree el `WhipAttack` corresponent. Posa el
// timer `attacking_remaining_ms_` a `swing_ms + cooldown_ms`.
void enterAttacking(Cardinal facing);
// Cardinal corresponent al `facing_` actual del sprite — la direcció
// cap on mira el jugador. Mapeig 1:1 amb `Facing` (mai NONE).
[[nodiscard]] auto currentFacingCardinal() const -> Cardinal;
// Activa l'estat HURT: el jugador queda projectat amb velocitat
// inicial `hurt_config.initial_speed_px_per_s`. La direcció es tria
// així:
@@ -456,8 +625,12 @@ class Player : public AnimatedSprite {
// i tria el que dóna més recorregut lliure.
// Decelera linealment fins a parar i, en eixe instant, torna a
// GROUNDED. Sense col·lisió amb altres jugadors mentre dura.
// Pública: la dispara tant la cinemàtica de "caure damunt" com el
// `WhipAttack` quan colpeja un altre jugador (friendly fire).
public:
void enterHurt(Cardinal preferred = Cardinal::NONE);
private:
// Tria la direcció definitiva del HURT. Si `preferred` cabe el
// recorregut natural complet, la retorna; d'altra manera tria la
// direcció amb més recorregut lliure entre els 4 cardinals.
@@ -492,10 +665,53 @@ class Player : public AnimatedSprite {
bool jump_held_keyboard_{false};
bool jump_held_gamepad_{false};
// Mateixa idea per al botó d'atac: edge-detection independent
// teclat/mando, perquè mantenir la tecla no genere atacs en cadena.
bool attack_held_keyboard_{false};
bool attack_held_gamepad_{false};
// Temps que queda dins de l'estat ATTACKING (swing + cooldown). Quan
// arriba a 0, transició a GROUNDED. Mentre dura, el jugador queda
// bloquejat i no rep input direccional ni de salt ni d'atac.
float attacking_remaining_ms_{0.0F};
// Estat de DYING. `death_animation_complete_` es marca quan
// `animationIsCompleted()` retorna true per primera vegada al cicle
// actual; només llavors comença a descomptar `respawn_remaining_ms_`.
// Quan el delay arriba a 0, `respawnFromDying()` aplica `respawn(...)`
// i decreix el comptador de vides.
bool death_animation_complete_{false};
float respawn_remaining_ms_{0.0F};
// Comptador d'intents (estil Mario). Inicialitzat a `death_config.
// initial_lives` al ctor; decreix una vegada per cada respawn
// post-mort (no per respawns de canvi de sala). Pot anar a negatius:
// avui no hi ha game over.
int lives_remaining_{0};
// Coordenades del spawn de respawn (cantó superior-esquerra del
// sprite). Les assigna el GameScene via `setSpawnPoint`. 0/0 fins que
// el GameScene n'invoque el setter al ctor / `loadRoom`.
float spawn_x_{0.0F};
float spawn_y_{0.0F};
// Aplica el respawn al final del cicle DYING: decreix vides (sense
// clamp), neteja els flags interns del cicle i delega a `respawn`
// amb les coordenades del spawn.
void respawnFromDying();
// Branca de `applyMovement` per a DYING. Avança l'animació, detecta
// la fi i descompta el delay; al final, dispara el respawn.
void applyDyingMovement(float delta_time);
// Llig el botó de salt aquest frame (teclat + mando assignat) i,
// amb edge-detection, retorna true si s'ha de despegar el salt.
[[nodiscard]] auto consumeJumpEdge() -> bool;
// Anàleg per a l'atac: edge-detection del botó d'atac (teclat
// `controls_.attack` + `SDL_GAMEPAD_BUTTON_WEST`).
[[nodiscard]] auto consumeAttackEdge() -> bool;
// Inicia un salt: copia `intended_v*` com a velocitat fixada del salt,
// posa `jump_vz_ = impulse`, transiciona a AIRBORNE.
void startJump();
+104
View File
@@ -0,0 +1,104 @@
#include "game/levels/level_manager.hpp"
#include <algorithm>
#include <iostream>
#include <stdexcept>
#include <utility>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/defaults.hpp"
namespace Levels {
auto LevelManager::get() -> LevelManager& {
static LevelManager instance_;
return instance_;
}
LevelManager::LevelManager() { loadIndex(); }
void LevelManager::loadIndex() {
const std::string PATH = Defaults::Pyramid::LEVELS_YAML;
auto bytes = Resource::Helper::loadFile(PATH);
if (bytes.empty()) {
throw std::runtime_error("LevelManager: no s'ha pogut obrir " + PATH);
}
const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
if (!yaml.contains("sequence") || !yaml["sequence"].is_sequence()) {
throw std::runtime_error("LevelManager: manca o és invàlid `sequence:` a " + PATH);
}
const auto& seq = yaml["sequence"];
entries_.clear();
entries_.reserve(seq.size());
for (const auto& e : seq) {
Entry entry{
.pyramid = e["pyramid"].get_value<int>(),
.room = e["room"].get_value<int>(),
.file = e["file"].get_value<std::string>(),
};
entries_.push_back(std::move(entry));
}
if (entries_.empty()) {
throw std::runtime_error("LevelManager: la seqüència de " + PATH + " és buida");
}
index_ = 0;
std::cout << "LevelManager: " << entries_.size() << " sales carregades de "
<< PATH << " (començant a " << currentLabel() << ")\n";
}
auto LevelManager::currentPath() const -> const std::string& {
return entries_[index_].file;
}
auto LevelManager::currentPyramid() const -> int { return entries_[index_].pyramid; }
auto LevelManager::currentRoom() const -> int { return entries_[index_].room; }
auto LevelManager::currentLabel() const -> std::string {
return "P" + std::to_string(currentPyramid()) + "-R" + std::to_string(currentRoom());
}
auto LevelManager::index() const -> int { return index_; }
auto LevelManager::count() const -> int { return static_cast<int>(entries_.size()); }
auto LevelManager::advance() -> bool {
if (index_ + 1 >= count()) { return false; }
++index_;
std::cout << "LevelManager: avança a " << currentLabel()
<< " (" << currentPath() << ")\n";
return true;
}
void LevelManager::restart() {
index_ = 0;
std::cout << "LevelManager: restart a " << currentLabel() << '\n';
}
void LevelManager::setIndex(int new_index) {
const int CLAMPED = std::clamp(new_index, 0, count() - 1);
if (CLAMPED != index_) {
index_ = CLAMPED;
std::cout << "LevelManager: setIndex(" << new_index << ") → "
<< currentLabel() << " (" << currentPath() << ")\n";
}
}
auto LevelManager::setStartingPoint(int pyramid, int room) -> bool {
for (std::size_t i = 0; i < entries_.size(); ++i) {
if (entries_[i].pyramid == pyramid && entries_[i].room == room) {
index_ = static_cast<int>(i);
std::cout << "LevelManager: starting_point P" << pyramid << "-R" << room
<< " trobat a l'índex " << i << " (" << currentPath() << ")\n";
return true;
}
}
std::cerr << "LevelManager: starting_point P" << pyramid << "-R" << room
<< " no trobat a levels.yaml — es manté l'índex actual ("
<< currentLabel() << ")\n";
return false;
}
} // namespace Levels
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include <string>
#include <vector>
namespace Levels {
// Font de veritat de la progressió de sales del joc. Carrega
// `data/maps/levels.yaml` a la primera crida a `get()` i exposa l'estat
// "on sóc ara" + les operacions per moure'm. La GameScene, l'Editor i
// els salts de debug consulten aquest singleton; ningú no manté el seu
// propi índex de progressió.
//
// Meyers singleton: la destrucció estàtica només toca std::string i
// std::vector, així que és segura sense ordenació explícita.
class LevelManager {
public:
static auto get() -> LevelManager&;
LevelManager(const LevelManager&) = delete;
LevelManager(LevelManager&&) = delete;
auto operator=(const LevelManager&) -> LevelManager& = delete;
auto operator=(LevelManager&&) -> LevelManager& = delete;
// Una entrada del `levels.yaml`. `file` és la ruta relativa al data
// root (mateixa convenció que la resta de YAML del joc); pyramid i
// room són purament informatius (logs i text d'intro).
struct Entry {
int pyramid{0};
int room{0};
std::string file;
};
// Ruta del nivell actualment apuntat. Sempre vàlida un cop carregat
// l'índex (entries_ no pot estar buit). Pensada per a `Map::loadFrom`,
// `EntityManager::reload` i l'Editor.
[[nodiscard]] auto currentPath() const -> const std::string&;
[[nodiscard]] auto currentPyramid() const -> int;
[[nodiscard]] auto currentRoom() const -> int;
// "P1-R3" — útil per a logs i el text d'intro de sala.
[[nodiscard]] auto currentLabel() const -> std::string;
// Posició dins de la seqüència. `count()` és el total de sales
// carregades del YAML (no necessàriament 30: si afegeixes/treus
// entrades de `levels.yaml`, el manager s'adapta).
[[nodiscard]] auto index() const -> int;
[[nodiscard]] auto count() const -> int;
// Avança al següent nivell. Retorna true si avança, false si ja
// estava a l'últim. NO fa loop — la GameScene decideix la política
// (loop a la primera, fi de joc, etc.) segons el context.
auto advance() -> bool;
// Torna al primer nivell.
void restart();
// Salta a un índex concret. Clamp als bordes [0, count()-1].
// Pensat per als hotkeys debug.
void setIndex(int new_index);
// Posiciona l'índex a la primera entrada que coincideix amb
// (pyramid, room). Retorna true si trobada; si no, l'índex no
// es modifica i el caller decideix què fer (típicament ignorar i
// arrencar des de la primera sala). Pensat per al
// `debug.yaml.starting_level` i per a futures lookups per coords.
auto setStartingPoint(int pyramid, int room) -> bool;
private:
LevelManager();
~LevelManager() = default;
// Carrega `data/maps/levels.yaml` via `Resource::Helper::loadFile`.
// Si manca, és invàlid o té la seqüència buida, llança — no té sentit
// continuar sense progressió de sales.
void loadIndex();
std::vector<Entry> entries_;
int index_{0};
};
} // namespace Levels
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more