Compare commits

..
87 Commits
Author SHA1 Message Date
JailDesigner 11a909b58d Merge feat/hazard-hole — hazard Hole, evitació per mòmies i broadcast de decals al load 2026-04-30 19:29:57 +02:00
JailDesigner 17cf6e0636 chore: bump v0.31.0 — hazard Hole, IA d'evitació de mòmies i broadcast de decals 2026-04-30 19:29:53 +02:00
JailDesigner 7d0aca506a feat(decals): tiles de decal de sòl compten com a pintats des del load 2026-04-30 19:16:21 +02:00
JailDesigner 7cd7b4dc4f chore(hole): neteja de comentaris obsolets i notes sobre riscos latents 2026-04-30 18:52:49 +02:00
JailDesigner 6d4f606eeb feat(mummy): evita forats per detecció reactiva al canvi de tile 2026-04-30 18:30:03 +02:00
JailDesigner 1ccd444aad fix(hazard): el jugador només cau al Hole si la hit-box hi cap del tot 2026-04-30 16:56:56 +02:00
JailDesigner 3bfca35f3a feat(mummy): evita tiles amb decal trigger (Hole) en decidir direcció 2026-04-30 16:50:26 +02:00
JailDesigner 3e5217066f fix(hazard): aterrar damunt d'un Hole salta el LANDING_STUN 2026-04-30 13:53:26 +02:00
JailDesigner 4da50571eb feat(hazard): nou hazard Hole — caiguda per forat amb clip rect 2026-04-30 13:32:17 +02:00
JailDesigner d4593d0686 Merge feat/tilepicker-layout — tilepicker amb editor.yaml 2026-04-30 09:52:51 +02:00
JailDesigner a53cf4b797 feat(editor): spacing, border i centrat vertical del tilepicker via editor.yaml 2026-04-30 09:51:30 +02:00
JailDesigner bcbf990a5f feat(editor): tilepicker respecta l'amplada del tileset si hi cap 2026-04-30 09:36:24 +02:00
JailDesigner 27cf2f032d chore(audio): normalitzat el volum dels efectes 2026-04-29 20:00:07 +02:00
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
JailDesigner 645db217fa Merge branch 'feat/game-states' — FSM de sala, EXITED, bucle complet 2026-04-26 10:51:45 +02:00
JailDesigner cd961cf718 chore: bump v0.24.0 — FSM de sala, EXITED i bucle complet 2026-04-26 10:51:40 +02:00
JailDesigner c0214b233b fix(game-scene): text d'INTRO/OUTRO surt cap a la dreta després del hold 2026-04-26 10:47:41 +02:00
JailDesigner d054d1b246 fix(player): respawn reseteja facing a DOWN i input/cardinal pendents 2026-04-26 10:47:13 +02:00
JailDesigner cd5bb94223 fix(player): EXITED no participa al resolver ni a checkPlayerCollisions 2026-04-26 10:41:05 +02:00
JailDesigner 178ce486c2 test(entity-manager): smoke del comptador player_exited i level_complete agregat 2026-04-26 10:32:20 +02:00
JailDesigner 811c6c482d feat(game-scene): spawn lligat a door_col i respawn a OUTRO->INTRO 2026-04-26 10:30:31 +02:00
JailDesigner 59cd9b524c feat(entity-manager): reload del nivell + PaintLayer::clear 2026-04-26 10:26:07 +02:00
JailDesigner 28e55a0615 feat(entity-manager): comptador player_exited agrega a level_complete 2026-04-26 10:22:42 +02:00
JailDesigner 5c5cd1d5da feat(player): estat EXITED i porta oberta despawna jugadors via player_exited 2026-04-26 10:20:13 +02:00
JailDesigner 377e5d0b97 feat(player): clamp permet pujar al passadís + porta tancada bloqueja via Map::collides 2026-04-26 10:09:04 +02:00
JailDesigner 648a5932f8 feat(door): col·locació derivada de Map::door_col i caixa de col·lisió neta 2026-04-26 10:07:37 +02:00
JailDesigner b54f3fc622 feat(map): door_col a la geometria + passadís de sortida a Map 2026-04-26 10:05:32 +02:00
JailDesigner 63bb41425a test(game-scene): extracció RoomFsm i smoke test de la FSM de sala 2026-04-26 09:10:54 +02:00
JailDesigner 9a2c677153 feat(locale): claus game.room_intro / game.room_complete (ca + en) 2026-04-26 09:03:37 +02:00
JailDesigner 4a9a037cd8 feat(game-scene): ROOM_OUTRO — text de sortida i fade out del món 2026-04-26 09:02:30 +02:00
JailDesigner 67b06c0a10 feat(game-scene): ROOM_INTRO — fade in del món i text d'entrada de sala 2026-04-26 09:00:15 +02:00
JailDesigner 368f9d2d6a feat(scenes): data/scenes/game.yaml amb timings i corbes de transició 2026-04-26 08:54:45 +02:00
JailDesigner 651db50a93 feat(game-scene): FSM mínima ROOM_INTRO/PLAYING/ROOM_OUTRO sense efectes 2026-04-26 08:51:56 +02:00
135 changed files with 12069 additions and 689 deletions

No files matched your search

+97 -3
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
@@ -47,21 +50,33 @@ set(ENTITY_SOURCES
source/game/entities/concrete/rolling_rock.cpp
source/game/entities/concrete/wall_trap.cpp
source/game/entities/concrete/floor_trap.cpp
source/game/entities/concrete/hole.cpp
source/game/entities/concrete/dust.cpp
)
# Entitats que criden directament símbols de Player (com WhipAttack via
# `enterHurt`/`getBaseBox`): només van a l'executable principal.
set(ENTITY_SOURCES
${ENTITY_SOURCES_BASE}
source/game/entities/concrete/whip_attack.cpp
)
set(APP_SOURCES
source/main.cpp
source/core/system/director.cpp
source/core/audio/audio.cpp
source/core/audio/audio_adapter.cpp
source/core/audio/audio_effects.cpp
source/core/audio/jail_audio.cpp
source/core/audio/sound_effects_config.cpp
source/core/locale/locale.cpp
source/core/ui/ui_config.cpp
source/core/input/key_config.cpp
source/core/input/global_inputs.cpp
source/core/input/input_state.cpp
source/core/input/gamepad_manager.cpp
source/core/input/gamepad_bindings.cpp
source/core/input/define_buttons.cpp
source/core/input/mouse.cpp
source/core/rendering/screen.cpp
source/core/rendering/canvas.cpp
@@ -76,6 +91,7 @@ set(APP_SOURCES
source/core/rendering/sprite/sprite.cpp
source/core/rendering/sprite/moving_sprite.cpp
source/core/rendering/sprite/animated_sprite.cpp
source/core/rendering/sprite/animated_sprite_parser.cpp
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
source/core/rendering/sdl3gpu/viewport.cpp
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
@@ -95,15 +111,18 @@ set(APP_SOURCES
source/game/scenes/title_scene.cpp
source/game/scenes/intro_scene.cpp
source/game/scenes/game_scene.cpp
source/game/scenes/room_fsm.cpp
source/game/entities/player.cpp
source/game/map/map.cpp
source/game/map/tilemap.cpp
source/game/map/tilemap_writer.cpp
source/game/map/paint_layer.cpp
source/game/levels/level_manager.cpp
# Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`).
# En Release el fitxer compila a un object buit i l'enllaçador no
# En Release els fitxers compilen a object buit i l'enllaçador no
# arrossega cap símbol del joc final.
source/game/editor/editor.cpp
source/game/editor/editor_config.cpp
${ENTITY_SOURCES}
# Fitxer només del binari principal: `loadFromLevel` depèn de
# Resource::Cache (→ Surface → GIF → àudio). Separat per no arrossegar
@@ -162,6 +181,15 @@ endif()
add_executable(${PROJECT_NAME} ${APP_SOURCES})
# Silencia warnings de codi de tercers (mateixa filosofia que el
# .clang-tidy de source/external/). Només afecta aquests TUs concrets;
# la resta del codi continua compilant amb -Wall -Wextra -Wpedantic.
set_source_files_properties(
source/external/stb_image_write_impl.cpp
source/external/stb_vorbis_impl.cpp
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations"
)
if(NOT APPLE AND GLSLC_EXE)
add_dependencies(${PROJECT_NAME} shaders)
endif()
@@ -322,10 +350,11 @@ option(BUILD_TESTS "Build smoke tests" ON)
if(BUILD_TESTS)
add_executable(test_entities_smoke
tests/test_entities_smoke.cpp
tests/hole_player_stubs.cpp
source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp
source/game/map/map.cpp
${ENTITY_SOURCES}
${ENTITY_SOURCES_BASE}
)
target_include_directories(test_entities_smoke PRIVATE
"${CMAKE_SOURCE_DIR}/source"
@@ -338,6 +367,30 @@ if(BUILD_TESTS)
COMMAND test_entities_smoke
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
# Smoke del WhipAttack. Inclou tot l'stack d'entitats + whip_attack.cpp.
# `whip_attack_player_stubs.cpp` proveeix símbols mínims de Player per
# satisfer el linker sense arrossegar audio/gamepad/screen — els tests
# del whip s'enfoquen a hitbox + Mummy, no a friendly fire.
add_executable(test_whip_attack
tests/test_whip_attack.cpp
tests/whip_attack_player_stubs.cpp
tests/hole_player_stubs.cpp
source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp
source/game/map/map.cpp
${ENTITY_SOURCES_BASE}
source/game/entities/concrete/whip_attack.cpp
)
target_include_directories(test_whip_attack PRIVATE
"${CMAKE_SOURCE_DIR}/source"
"${CMAKE_BINARY_DIR}"
)
target_link_libraries(test_whip_attack PRIVATE SDL3::SDL3)
target_compile_options(test_whip_attack PRIVATE -Wall -Wextra -Wpedantic)
add_test(NAME whip_attack
COMMAND test_whip_attack
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
# Smoke del flux d'escenes i del parser de debug.yaml. Stand-alone:
# només enllaça els helpers purs (debug_config, placeholder_input,
# key_config) perquè la integració completa dels placeholders arrossegaria
@@ -374,4 +427,45 @@ if(BUILD_TESTS)
add_test(NAME tilemap_writer
COMMAND test_tilemap_writer
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
# Smoke de la FSM de sala (RoomFsm). Cobreix loadConfig (timings i
# corbes des de YAML), advance (transicions entre estats), i
# formatRoomTitle (locale + substitució `{n}`). Stand-alone: no
# arrossega Audio/Screen/Player/EntityManager — només els helpers
# purs de la FSM, Locale i UiConfig.
add_executable(test_game_fsm
tests/test_game_fsm.cpp
source/game/scenes/room_fsm.cpp
source/core/locale/locale.cpp
source/core/ui/ui_config.cpp
source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp
source/utils/easing.cpp
)
target_include_directories(test_game_fsm PRIVATE
"${CMAKE_SOURCE_DIR}/source"
"${CMAKE_BINARY_DIR}"
)
target_link_libraries(test_game_fsm PRIVATE SDL3::SDL3)
target_compile_options(test_game_fsm PRIVATE -Wall -Wextra -Wpedantic)
add_test(NAME game_fsm
COMMAND test_game_fsm
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
# 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
# ==============================================================================
+13
View File
@@ -0,0 +1,13 @@
# Tema visual de l'editor de tilemap (només Debug). Carregat per
# EditorConfig a l'arrencada; les claus absents cauen als valors compilats
# a Defaults::Editor (veure source/utils/defaults.hpp).
picker:
margin: 8 # px entre canvas i caixa del picker
spacing: 1 # px de separació entre tiles dins del picker
# Colors ARGB en hex. Accepta enter (0xFF2244FF) o cadena ("#FF2244FF").
colors:
border: "0xFF2244FF" # vora 1 px que envolta canvas i picker
picker_bg: "0xFF101824" # fons del picker
picker_hover: "0xFFFFFFFF" # vora 1 px del tile sota el cursor
+8
View File
@@ -5,6 +5,14 @@ sprite:
size: [48, 32]
tile_size: [3, 2]
collision:
# Caixa de col·lisió: 16×24 centrada al sprite 48×32. Coincideix amb el
# tile central transitable (els 16 px del passadís de sortida) i es
# restringeix a la franja del mur trasser (24 px d'alçada). Així
# `checkPlayerCollisions` només dispara `onPlayerContact` quan el
# jugador entra al forat real, no als marcs flanquejants ni als 8 px
# superiors que queden tapats pel HUD.
size: [16, 24]
offset: [16, 8]
# Porta = mur infranquejable. Igual que el sarc, alçada efectiva
# infinita per impedir que un jugador la salte per damunt.
height: 999
Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

+26
View File
@@ -0,0 +1,26 @@
name: hole
type: static_hazard
sprite:
path: hole.gif
size: [16, 16]
tile_size: [1, 1]
collision:
# Forat: ocupa el tile sencer (16×16). `height: 0` deixa que un salt
# AIRBORNE el creui sense disparar el contacte (el filtre d'altura de
# `checkPlayerCollisions` el descarta quan `jump_z_ > height`). El
# bucle de contacte admet l'entitat per `flags.triggers_on_overlap`,
# encara que `flags.collides` sigui false.
size: [16, 16]
height: 0
flags:
collides: false
interactable: false
# Avui el loader no llig el bloc `flags:` (és declaratiu); el flag
# real es fixa al constructor C++ de `Hole`. Mantenim el camp per
# coherència amb la resta de plantilles i per quan es cablege la
# lectura genèrica de flags.
triggers_on_overlap: true
animations:
idle: { frames: [0], fps: 1, loop: true }
stats:
damage: 0
+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ó)"
+20
View File
@@ -36,6 +36,17 @@ hud:
pyramid: "Piramide"
room: "Sala"
# Textos de la transició d'entrada/sortida de sala (FSM de GameScene).
# Tant l'INTRO com l'OUTRO es renderitzen com a dues línies separades
# (cadascuna amb la seua animació). L'INTRO accepta els placeholders
# `{p}` (piràmide) i `{r}` (sala); l'OUTRO no en té cap — són les
# dues paraules del rètol "fase completada".
game:
room_intro_pyramid: "Piramide {p}"
room_intro_room: "Sala {r}"
room_complete_line1: "Fase"
room_complete_line2: "Completada"
menu:
titles:
root: "Menú de servei"
@@ -60,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"
@@ -93,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"
+24
View File
@@ -36,6 +36,16 @@ hud:
pyramid: "Pyramid"
room: "Room"
# Room transition texts (GameScene FSM intro/outro). Both INTRO and
# OUTRO render as two independent lines with their own animation.
# INTRO uses `{p}` (pyramid) and `{r}` (room) placeholders; OUTRO has
# no placeholders — it's the two words of the "stage complete" label.
game:
room_intro_pyramid: "Pyramid {p}"
room_intro_room: "Room {r}"
room_complete_line1: "Stage"
room_complete_line2: "Complete"
menu:
titles:
root: "Service menu"
@@ -47,6 +57,7 @@ menu:
system: "System"
labels:
player_1: "Player 1"
player_2: "Player 2"
gamepad_unavailable: "Not available"
items:
video_submenu: "Video"
@@ -59,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"
@@ -88,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 }
+152
View File
@@ -0,0 +1,152 @@
# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre pyramid_1.gif.
# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles
# en el tilemap però només 1.5 visibles al canvas, així que la textura es
# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240.
#
# Format del tilemap:
# `data` és una llista de files; cada fila és una llista d'índexs del
# tileset (lineal esq→dre, dalt→baix). Amb un tileset de 512×256 i tiles
# de 16×16 hi ha 32 tiles per fila. Un valor negatiu (p.ex. -1) deixa
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 1
room: 1
tileset: data/tilesets/pyramid_1.gif
tile_size: 16
origin: [-8, 0]
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
# pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
# per desactivar el pintat per a aquest jugador.
player1_paint_tile: 0
player2_paint_tile: 1
# Física del contacte entre jugadors. Quan un jugador està quiet i l'altre
# el topa, el segon l'empenta a aquesta fracció de la velocitat normal (i
# ell mateix es ralentitza per mantenir el grup). En head-on, ambdós
# s'anul·len (sense push, sense moviment).
push_factor: 0.30
# Geometria lògica de la piràmide (separada del render visual del tileset).
# La graella de sarcòfags, el pitch entre ells i les amplades dels murs
# decideixen els límits del jugador i les seues caixes de col·lisió. Abans
# vivien com a `static constexpr` a map.hpp; ara cada piràmide pot definir
# la seua pròpia forma sense recompilar.
geometry:
grid_cols: 4
grid_rows: 5
sarc_w: 48
sarc_h: 16
pitch_x: 64
pitch_y: 32
hud_h: 24
back_wall_h: 24
side_wall_w: 24
# Columna central del tile transitable de la porta. La porta es renderitza
# com a sprite 48×32 (3 tiles d'amplada) centrat sobre aquesta columna; els
# 16 px centrals (just aquesta columna) són el "forat" del mur trasser per
# on el jugador surt de la sala. Les 2 columnes flanquejants segueixen sent
# mur visible. El spawn dels jugadors també s'alinea amb aquesta columna.
door_col: 10
data:
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
- [11,30,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,31,13]
- [11,13,27,28,15,22,28,28,28,28,28,28,28,28,28,28,15,28, 6,11,13]
- [11,13, 8, 8,16, 8,17, 8,10, 8,17, 8, 8, 8,17, 8, 8, 8, 8,11,13]
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
- [11,13,17, 8, 9, 2, 8,10, 8, 8, 8, 8,10, 8, 8,26, 8, 8,17,11,13]
- [11,13, 8, 2, 2, 2, 2, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
- [11,13,17, 8, 8,26, 8, 8, 9, 8, 8, 8, 8, 8,10, 8,16, 8,17,11,13]
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
- [11,13,17, 8,10, 8, 8, 8, 2, 8, 8, 8, 9, 8, 8, 8,10, 8,17,11,13]
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2,16, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
- [11,13,17, 8,10, 8, 8,10, 2, 8, 8,26, 8, 8, 8, 8,10, 8,17,11,13]
- [11,13, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8, 2, 2, 2, 8,11,13]
- [11,13, 8,10,16,16,17, 8, 8, 8,17, 8, 9, 8,17, 8, 2, 8, 8,11,13]
- [11,38, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,39,13]
# Spawns d'entitats per a aquest nivell. Es llegeixen des de
# EntityManager::loadFromLevel al GameScene. Convencions:
# - sarcophagus → `sarc_position: [sarc_col, sarc_row]` en la graella de
# sarcòfags (4×5). EntityManager calcula el rect píxel via
# Map::getSarcRect i l'ancoratge de tile per al perímetre.
# - la resta (torch, door, mummy, rolling_rock, wall_trap, floor_trap)
# → `position: [tile_col, tile_row]` en coordenades del tilemap
# (origen `[-8, 0]` per defecte). Pixel = tile*16 + tilemap_origin.
#
# Dos sarcòfags en cantonades oposades → perímetres aïllats (cap tile en
# comú). La porta va al centre inferior, on el tilemap té els tiles
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
# superior esquerre per verificar render d'una entitat decorativa.
entities:
- type: torch
position: [4, 2]
- type: torch
position: [8, 2]
- type: torch
position: [12, 2]
- type: torch
position: [16, 2]
- type: sarcophagus
sarc_position: [0, 0]
- type: sarcophagus
sarc_position: [1, 0]
- type: sarcophagus
sarc_position: [2, 0]
- type: sarcophagus
sarc_position: [3, 0]
- type: sarcophagus
sarc_position: [0, 1]
- type: sarcophagus
sarc_position: [1, 1]
- type: sarcophagus
sarc_position: [2, 1]
- type: sarcophagus
sarc_position: [3, 1]
- type: sarcophagus
sarc_position: [0, 2]
- type: sarcophagus
sarc_position: [1, 2]
- type: sarcophagus
sarc_position: [2, 2]
- type: sarcophagus
sarc_position: [3, 2]
- type: sarcophagus
sarc_position: [0, 3]
- type: sarcophagus
sarc_position: [1, 3]
- type: sarcophagus
sarc_position: [2, 3]
- type: sarcophagus
sarc_position: [3, 3]
- type: sarcophagus
sarc_position: [0, 4]
- type: sarcophagus
sarc_position: [1, 4]
- type: sarcophagus
sarc_position: [2, 4]
- type: sarcophagus
sarc_position: [3, 4]
- type: door
- type: mummy
position: [10, 7]
- type: mummy
position: [7, 9]
- type: hole
position: [11, 5]
- type: hole
position: [4, 7]
- type: hole
position: [8, 11]
- type: hole
position: [15, 11]
- type: hole
position: [15, 5]
+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]
+10 -2
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
@@ -42,6 +42,12 @@ geometry:
hud_h: 24
back_wall_h: 24
side_wall_w: 24
# Columna central del tile transitable de la porta. La porta es renderitza
# com a sprite 48×32 (3 tiles d'amplada) centrat sobre aquesta columna; els
# 16 px centrals (just aquesta columna) són el "forat" del mur trasser per
# on el jugador surt de la sala. Les 2 columnes flanquejants segueixen sent
# mur visible. El spawn dels jugadors també s'alinea amb aquesta columna.
door_col: 10
data:
- [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
@@ -123,7 +129,9 @@ entities:
- type: sarcophagus
sarc_position: [3, 4]
- type: door
position: [9, 1]
# Sense `position`: la porta sempre viu al mur trasser. La columna
# transitable la dicta `geometry.door_col`; el render i la caixa
# de col·lisió es deriven d'aquesta dada al loadFromLevel.
- type: mummy
position: [10, 7]
- type: mummy
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

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

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

+96 -92
View File
@@ -1,113 +1,104 @@
# player animation
sprite:
path: sam.gif
size: [16, 32]
# Geometria de col·lisió del jugador (independent de l'animació):
# size → costat del quadrat de col·lisió (16×16)
# base_size → costat del quadrat visual inferior del sprite (els "peus");
# s'usa per centrar la caixa de col·lisió sobre ell.
# Geometria de col·lisió. `size` és la caixa contra murs/sarcs;
# `hit_size`, la caixa contra altres actors (més petita); `height`,
# l'alçada que un salt ha de superar per passar per damunt.
collision:
size: 16
base_size: 16
# Caixa de contacte actor-actor (jugador↔jugador, jugador↔momia/trampa).
# Més petita que `size` perquè els contactes d'acció siguin precisos;
# `size` es reserva per a la col·lisió amb murs/sarcòfags i per enfilar
# els passadissos. Centrada en el mateix punt que la caixa base.
hit_size: 8
# Alçada del cos en píxels. Funció DOBLE segons amb què col·lisione:
# · Contra entitats (mummy, rolling_rock, sarc): si `jump_z > height`
# d'aquella entitat, no hi ha col·lisió. Igual que als YAMLs
# d'entitats.
# · Contra altres jugadors: la col·lisió és binària per estat aeri.
# Si un està en l'aire i l'altre no → cap col·lisió. Si tots dos
# estan en l'aire, llavors `body_height` decideix si els rangs
# verticals solapen i s'aplica el rebot mid-aire. `0` = sense cos
# (ni rebot quan tots dos volen).
height: 12
# Animacions en mapa (clau = nom). Camps per animació:
# frames → llista d'índexs dins de l'spritesheet.
# frame_ms → durada de cada frame en ms. Escalar = uniforme; llista =
# per-frame (mateixa mida que `frames`).
# loop → true per defecte. `loop: false` = un sol tret (es queda al
# darrer frame i `animationIsCompleted()` passa a true).
# loop_from → rebota a aquest índex en passar del final (implica loop).
animations:
stand_down: { frames: [1] }
stand_up: { frames: [5] }
stand: { frames: [9] }
jump_down: { frames: [12] }
jump_up: { frames: [13] }
jump: { frames: [14] }
stun_down: { frames: [16] }
stun_up: { frames: [17] }
stun: { frames: [18] }
# Les variants `*_west` reaprofiten els frames de l'est i declaren
# `flip_h: true` perquè el motor d'animació apliqui el flip al sprite —
# el codi del joc no toca mai `flip_` directament.
stand_south: { frames: [0] }
stand_north: { frames: [4] }
stand_east: { frames: [8] }
stand_west: { frames: [8], flip_h: true }
walk_down:
frames: [0, 1, 2, 3]
frame_ms: 100
jump_south: { frames: [12] }
jump_north: { frames: [13] }
jump_east: { frames: [14] }
jump_west: { frames: [14], flip_h: true }
walk_up:
frames: [4, 5, 6, 7]
frame_ms: 100
stun_south: { frames: [16] }
stun_north: { frames: [17] }
stun_east: { frames: [18] }
stun_west: { frames: [18], flip_h: true }
walk:
frames: [8, 9, 10, 11]
frame_ms: 100
walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
walk_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
hurt_down:
frames: [0, 1, 2, 3]
frame_ms: 100
hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
hurt_up:
frames: [4, 5, 6, 7]
frame_ms: 100
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 }
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 }
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 }
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true }
hurt:
frames: [8, 9, 10, 11]
frame_ms: 100
recover_south: { frames: [23] }
recover_north: { frames: [27] }
recover_east: { frames: [31] }
recover_west: { frames: [31], flip_h: true }
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
# El `threshold_ms` filtra els tocs curts: si el temps acumulat de
# moviment continu no ha arribat al llindar, la parada és instantània
# (permet "passets" d'un tile sense rellisquera). L'acumulador es
# reseteja només quan no hi ha ni input ni velocitat residual, així
# els canvis de direcció enmig del coast no rearmen el llindar.
# `brake_time_ms: 0` o absència del bloc = inèrcia desactivada.
# Animació de mort: 3 voltes horàries començant pel sud (south →
# west → north → east, repetit 3×). Reaprofita els frames stand_*; el
# west és l'east amb `flip_h: true` declarat per-frame (mapping form,
# no hereta del nivell d'anim). `frame_ms` per-frame creix linealment
# per donar la sensació de desaccelerar fins a aturar-se. `loop: false`
# → es queda al darrer frame i marca completed.
death_spinning:
frames:
- 0
- { index: 8, flip_h: true }
- 4
- 8
- 0
- { index: 8, flip_h: true }
- 4
- 8
- 0
- { index: 8, flip_h: true }
- 4
- 8
frame_ms: [60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280]
loop: false
# Mort per caiguda a un agujero (DeathCause::HOLE). `loop: false` perquè
# `animationIsCompleted()` dispari el respawn timer determinísticament.
death_hole:
frames: [15]
frame_ms: 60
loop: false
# Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra
# tocs curts (passets d'un tile sense rellisquera).
inertia:
brake_time_ms: 150
threshold_ms: 300
# Gir de 180°: mentre dura, el facing queda fixat a la direcció
# intermèdia (efecte purament visual; el moviment continua sense
# alterar-se). Els girs de 90° no passen per cap intermedi.
# Gir de 180°: durant `duration_ms` el facing es fixa a la direcció
# intermèdia indicada (purament visual).
turn:
duration_ms: 80
# Direcció intermèdia per a cada gir de 180° (sentit horari).
transitions:
west_to_east: down # passa per baix (stand_down)
east_to_west: up # passa per dalt (stand_up)
north_to_south: east # passa per la dreta (stand, sense flip)
south_to_north: west # passa per l'esquerra (stand, amb flip)
west_to_east: south
east_to_west: north
north_to_south: east
south_to_north: west
# Salt vertical (px verticals "visuals", no afecta la posició al món).
# impulse → velocitat vertical inicial (px/s) en deixar el sòl.
# gravity → acceleració descendent constant (px/s²).
# landing_stun_ms → temps en què el jugador queda paralitzat en aterrar
# (no es pot moure ni tornar a saltar).
# shadow_y_offset → desplaçament vertical de la sombra (px, positiu =
# puja). Compensa la franja transparent que els
# sprites del jugador tenen sota els peus reals
# perquè camine "pel mig del passadís" (3 px aquí).
# sound → fitxer .wav reproduït al despegue (relatiu a
# data/sounds/). Buit o absent = sense so.
# land_sound → fitxer .wav reproduït en aterrar.
# L'alçada del cos viu al bloc `collision:` (`height`), igual que la resta
# d'entitats: és una propietat de col·lisió, no de la física del salt.
# Amb impulse=240 i gravity=600: pic ≈ 48 px, temps total en l'aire ≈ 0.8 s.
# Salt vertical (px de z, no toca la posició al món).
# `shadow_y_offset`: puja la sombra per compensar la franja
# transparent sota els peus del sprite.
jump:
impulse: 200
gravity: 600
@@ -116,14 +107,27 @@ jump:
sound: jump.wav
land_sound: land.wav
# Estat HURT: el jugador queda projectat sense control per un impacte
# (algú que cau al damunt; en el futur també fuetada de l'altre jugador).
# `enterHurt` tria automàticament la direcció amb més recorregut lliure
# entre els 4 cardinals, posa la velocitat inicial a `speed` i la fa
# decaure linealment fins a 0 en `brake_time_ms`. Si pel camí xoca amb
# un mur, para en sec. En tornar a velocitat 0, recupera l'estat
# GROUNDED i el control. Mentre dura, no col·lisiona amb altres jugadors.
# Amb speed=120 i brake_time=500ms ⇒ recorregut ≈ 30 px (≈2 tiles).
# Projecció post-impacte: surt cap a la direcció amb més recorregut
# lliure i frena fins a 0 en `brake_time_ms`.
hurt:
speed: 120
brake_time_ms: 500
sound: hurt.wav
# Atac amb làtigo. `range_tiles` és la longitud abans de retallar
# contra murs/sarcs; `cooldown_ms` allarga ATTACKING amb una
# recuperació en què la hitbox ja no fa dany.
whip:
range_tiles: 3
swing_ms: 200
cooldown_ms: 200
damage: 1
sound: whip.wav
# Mort + respawn. `respawn_delay_ms` és la pausa entre la fi de
# `death_spinning` i la reaparició al spawn. `lives` és el comptador
# inicial d'intents (estil Mario): cada respawn el decrementa en 1, pot
# anar a negatius (no hi ha game over).
death:
respawn_delay_ms: 500
lives: 3
+48
View File
@@ -0,0 +1,48 @@
# Configuració de la GameScene: timings i corbes de la FSM de sala
# (ROOM_INTRO i ROOM_OUTRO). Tots els valors són ms i tota la geometria
# i font del text es reutilitzen del tema UI (data/ui.yaml > pause +
# notification). Si manca el fitxer o alguna clau, GameScene cau als
# fallbacks compilats; veure `readSceneConfig` a game_scene.cpp.
room_intro:
fade_in_ms: 500 # negre opac → món visible
text_delay_ms: 500 # espera amb el món net abans de sortir el text
text_enter_ms: 400 # text esquerra → centre
text_hold_ms: 2000 # text quiet al centre
text_exit_ms: 400 # text centre → fora per l'esquerra
line_gap_px: -4 # px extra entre les dues línies (piràmide / sala)
# 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: 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.
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
+45 -3
View File
@@ -5,9 +5,10 @@
#include <algorithm> // Per a clamp
#include <cstdio> // Per a std::fprintf
#include "core/audio/audio_adapter.hpp" // Per a AudioResource::getMusic/getSound
#include "core/audio/jail_audio.hpp" // Per a Ja::* (motor jailgames)
#include "utils/defaults.hpp" // Per a Defaults::Audio::FREQUENCY
#include "core/audio/audio_adapter.hpp" // Per a AudioResource::getMusic/getSound
#include "core/audio/jail_audio.hpp" // Per a Ja::* (motor jailgames)
#include "core/audio/sound_effects_config.hpp" // Per a SoundEffectsConfig
#include "utils/defaults.hpp" // Per a Defaults::Audio::FREQUENCY
// Invariant compile-time: tots els valors d'Audio::Group han de cabre als slots
// de volum per grup que manté l'engine. Si s'afegeix una nova entrada a Group
@@ -128,6 +129,47 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
}
}
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
// existeix o l'engine retorna -1 (cap de canals d'efecte plé), cau a playSound
// sec — l'usuari sent el so encara que la cua no s'apliqui.
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; }
const auto* params = SoundEffectsConfig::get().findEcho(preset_name);
if (params == nullptr) {
std::fprintf(stderr, "Audio: preset d'eco '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
engine_->playSound(sound, 0, static_cast<int>(group));
return;
}
if (engine_->playSoundWithEcho(sound, *params, static_cast<int>(group)) < 0) {
engine_->playSound(sound, 0, static_cast<int>(group));
}
}
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
// fallback que playSoundWithEcho.
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; }
const auto* params = SoundEffectsConfig::get().findReverb(preset_name);
if (params == nullptr) {
std::fprintf(stderr, "Audio: preset de reverb '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
engine_->playSound(sound, 0, static_cast<int>(group));
return;
}
if (engine_->playSoundWithReverb(sound, *params, static_cast<int>(group)) < 0) {
engine_->playSound(sound, 0, static_cast<int>(group));
}
}
// Atura tots els sons
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::stopAllSounds() {
+7 -1
View File
@@ -80,7 +80,13 @@ class Audio {
// --- Control de sons ---
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
void stopAllSounds(); // Aturar tots els sons (muta globals de Ja)
// Reprodueix un so processat per un efecte definit a data/sounds.yaml
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix
// o el motor està al cap de canals amb efecte, fa fallback a playSound
// sec — l'usuari sent el so igualment, sense la cua.
void playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
void playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
void stopAllSounds(); // Aturar tots els sons (muta globals de Ja)
// --- Control de volum (API interna: float 0.0..1.0) ---
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
+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
+74 -9
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,10 @@
#include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp"
#include "game/debug_config.hpp"
#ifdef _DEBUG
#include "game/editor/editor_config.hpp"
#endif
#include "game/levels/level_manager.hpp"
#include "game/options.hpp"
#include "game/scenes/game_scene.hpp"
#include "game/scenes/intro_scene.hpp"
@@ -80,6 +86,11 @@ Director::Director() {
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n';
}
// Vinculacions de mando per dispositiu — fitxer aïllat de `config.yaml`
// perquè és per (name, path) del mando, no per perfil/usuari. Replica
// el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`).
GamepadBindings::get().load(system_folder_ + "gamepad_configs.yaml");
// Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix
// es crea amb presets per defecte (off, crt, ntsc).
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
@@ -111,6 +122,12 @@ Director::Director() {
// data/ui.yaml cau al valor compilat a Defaults::UI.
UiConfig::get().load("data/ui.yaml");
#ifdef _DEBUG
// Tema visual de l'editor de tilemap (Debug-only). Claus absents
// cauen als valors compilats a Defaults::Editor.
EditorConfig::get().load("data/editor/editor.yaml");
#endif
// Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els
// overlays perquè tots els reaprofiten sense duplicar la lògica. Els
// noms de font viuen al fitxer data/ui.yaml via UiConfig.
@@ -121,10 +138,26 @@ Director::Director() {
const std::string PLACEHOLDER_FONT_GIF_KEY = UiConfig::get().placeholderFont().name + ".gif";
Resource::Cache::get().loadSurface(PLACEHOLDER_FONT_GIF_KEY, UiConfig::get().placeholderFont().gif);
Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
// Font del rètol de transició de sala (la consumeix `GameScene`
// tant per la línia "Piràmide N / Sala M" de l'INTRO com pel
// "Fase completada" de l'OUTRO). Es preloda ací per consistència
// amb la resta de fonts d'UI; sense això, la primera entrada a
// GAME aniria a disc.
const std::string ROOM_TEXT_FONT_GIF_KEY = UiConfig::get().roomTextFont().name + ".gif";
Resource::Cache::get().loadSurface(ROOM_TEXT_FONT_GIF_KEY, UiConfig::get().roomTextFont().gif);
Resource::Cache::get().getSurface(ROOM_TEXT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
// Música de la piràmide 1 (primer i únic track de moment). Carreguem ací
// perquè forma part del preload d'assets, com la font i la paleta.
Resource::Cache::get().loadMusic("pyramid_1.ogg", "data/music/pyramid_1.ogg");
// Pistes de música del cicle de sala (intro/playing/outro). Es preloden
// ací perquè formen part del preload d'assets, com la font i la paleta;
// la lògica de quina sona en cada estat viu a `GameScene`.
Resource::Cache::get().loadMusic("room_intro.ogg", "data/music/room_intro.ogg");
Resource::Cache::get().loadMusic("playing.ogg", "data/music/playing.ogg");
Resource::Cache::get().loadMusic("room_outro.ogg", "data/music/room_outro.ogg");
// Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio
// perquè els primers playSoundWith* (al començament del joc) ja
// troben els presets disponibles.
SoundEffectsConfig::get().load("data/sounds.yaml");
// Audio: singleton manual propietari d'un SDL audio device. Es destrueix
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els
@@ -182,6 +215,13 @@ Director::Director() {
DebugConfig::createDefaultIfMissing(DEBUG_YAML);
const auto START = DebugConfig::resolveStartScene(DEBUG_YAML);
const auto STARTING_PLAYERS = DebugConfig::resolveStartingPlayers(DEBUG_YAML);
// `starting_level` (opcional) sobreescriu l'índex inicial del
// LevelManager abans de crear cap GameScene. Si la clau no està o el
// (pyramid, room) no apareix al levels.yaml, el LevelManager
// continua a la primera entrada (P1-R1).
if (const auto SL = DebugConfig::resolveStartingLevel(DEBUG_YAML); SL.has_value()) {
Levels::LevelManager::get().setStartingPoint(SL->pyramid, SL->room);
}
#else
const auto START = DebugConfig::StartScene::LOGO;
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
@@ -189,6 +229,12 @@ Director::Director() {
std::cout << "Director: start_scene=" << DebugConfig::toString(START)
<< " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n';
// El branch Release inicialitza STARTING_PLAYERS amb una constant
// literal i clang considera la captura redundant; en Debug el valor és
// runtime i la captura sí és imprescindible. La lambda viu dins de
// IntroScene, més enllà del scope d'aquest ctor — capturar per
// referència seria UB.
// NOLINTNEXTLINE(clang-diagnostic-unused-lambda-capture)
auto make_game = [STARTING_PLAYERS]() -> std::unique_ptr<Scene> {
auto cfg = GameScene::defaultConfig();
cfg.starting_players = STARTING_PLAYERS;
@@ -265,12 +311,29 @@ void Director::processEvents() {
if (QUIT_REQUESTED) { current_scene_.reset(); }
}
namespace {
// Resol el botó SERVICE configurat per al mando origen d'aquest event.
// Retorna -1 si no en troba cap. La cerca passa per `GamepadManager`
// per obtenir el (name, path) i d'ahí a `GamepadBindings`.
auto serviceButtonForGamepadEvent(SDL_JoystickID jid) -> int {
for (const auto& info : GamepadManager::get().getAll()) {
if (info.pad == nullptr) { continue; }
if (SDL_GetJoystickID(SDL_GetGamepadJoystick(info.pad)) != jid) { continue; }
return GamepadBindings::get().getButton(info.name, info.path, GamepadBindings::Action::SERVICE);
}
return -1;
}
} // namespace
void Director::processGamepadEvents() {
// Dues responsabilitats: (1) passar ADDED/REMOVED al GamepadManager
// perquè obri/tanqui mandos i emeti les notificacions hot-plug; (2)
// tractar el botó BACK com a toggle del menú quan està tancat (el
// paral·lel de F9 per al teclat). Quan el menú està obert, el BACK
// passa pel pipeline del `ServiceMenu::handleEvent()` via
// tractar el botó SERVICE (per defecte BACK; remapejable per
// `GamepadBindings`) com a toggle del menú quan està tancat (el
// paral·lel de F9 per al teclat). Quan el menú està obert, el botó
// SERVICE passa pel pipeline del `ServiceMenu::handleEvent()` via
// `processMenuInput()` i allà es converteix en `close()`.
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
bool consumed = false;
@@ -278,10 +341,12 @@ void Director::processGamepadEvents() {
GamepadManager::get().handleEvent(*it);
consumed = true;
} else if (it->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN &&
it->gbutton.button == SDL_GAMEPAD_BUTTON_BACK &&
!ServiceMenu::get().isOpen()) {
ServiceMenu::get().toggle();
consumed = true;
const int SERVICE_BTN = serviceButtonForGamepadEvent(it->gbutton.which);
if (SERVICE_BTN >= 0 && static_cast<int>(it->gbutton.button) == SERVICE_BTN) {
ServiceMenu::get().toggle();
consumed = true;
}
}
if (consumed) {
it = EventBuffer::events.erase(it);
+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*;
+98 -32
View File
@@ -14,17 +14,28 @@
#include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/system/event_buffer.hpp"
#include "game/editor/editor_config.hpp"
#include "game/levels/level_manager.hpp"
#include "game/map/tilemap.hpp"
#include "utils/defaults.hpp"
#include "utils/defines.hpp"
namespace {
// Gruix del border 1 px del picker: reservat dins del rectangle perquè
// no envaeixi els tiles ni se surti fora de la caixa. La cella efectiva
// de cada tile té `pitch = tile_size + spacing` i la graella s'inicia
// un píxel cap dins de cada vora.
constexpr int PICKER_BORDER = 1;
// Geometria del TilePicker en espai canvas. `cols × rows_visible` tiles
// encapsulats dins d'un rectangle amb `PICKER_MARGIN` per vora. Com que
// el tileset real (p.ex. 32×16 tiles de 16 px = 512×256) no cabria a
// 1× dins del canvas 336×240, linealitzem els índexs i els repartim
// en una graella que SÍ cap, amb scroll vertical per veure la resta.
// encapsulats dins d'un rectangle amb `picker.margin` per vora i un
// border 1 px que el delimita. Si el tileset cap en amplada (la suma
// de tiles + spacing + border ≤ INNER_W), en respectem la graella
// natural — els tiles surten ordenats igual que al GIF; si no,
// linealitzem els índexs en una graella més ampla que sí cap. Quan el
// contingut cap sense scroll, la caixa s'arrossega a alçada mínima i
// es centra verticalment dins de l'àrea interna.
struct PickerLayout {
int x{0};
int y{0};
@@ -33,6 +44,8 @@ namespace {
int cols{0};
int rows_visible{0};
int tile_size{0};
int spacing{0};
int pitch{0}; // tile_size + spacing — pas entre orígens de tiles veïns.
int total_tiles{0};
int max_scroll{0};
};
@@ -40,27 +53,59 @@ namespace {
auto layoutPicker(const Tilemap& tm) -> PickerLayout {
PickerLayout lay{};
lay.tile_size = tm.getTileSize();
const int M = Defaults::Editor::PICKER_MARGIN;
lay.spacing = std::max(0, EditorConfig::get().pickerSpacing());
lay.pitch = lay.tile_size + lay.spacing;
const int M = EditorConfig::get().pickerMargin();
const int INNER_W = GameCanvas::WIDTH - (2 * M);
const int INNER_H = GameCanvas::HEIGHT - (2 * M);
if (lay.tile_size <= 0) { return lay; }
lay.cols = INNER_W / lay.tile_size;
lay.rows_visible = INNER_H / lay.tile_size;
lay.x = M;
lay.y = M;
lay.w = lay.cols * lay.tile_size;
lay.h = lay.rows_visible * lay.tile_size;
if (lay.tile_size <= 0 || lay.pitch <= 0) { return lay; }
// Espai útil per a tiles, descomptat el border 1 px de cada vora.
// Inversió de N·ts + (N-1)·spacing ≤ AVAIL → N ≤ (AVAIL+spacing)/pitch.
const int AVAIL_W = INNER_W - (2 * PICKER_BORDER);
const int AVAIL_H = INNER_H - (2 * PICKER_BORDER);
const int MAX_COLS = std::max(0, (AVAIL_W + lay.spacing) / lay.pitch);
const int MAX_ROWS = std::max(0, (AVAIL_H + lay.spacing) / lay.pitch);
int tcols = 0;
const auto TILESET = Resource::Cache::get().getSurface(tm.getTilesetPath());
if (TILESET != nullptr) {
const int TCOLS = TILESET->getWidth() / lay.tile_size;
tcols = TILESET->getWidth() / lay.tile_size;
const int TROWS = TILESET->getHeight() / lay.tile_size;
lay.total_tiles = TCOLS * TROWS;
lay.total_tiles = tcols * TROWS;
}
// Com que els índexs del tileset són row-major (idx = row·TCOLS + col),
// fer servir cols = TCOLS reprodueix exactament la disposició del GIF.
const bool FITS_W = (tcols > 0 && tcols <= MAX_COLS);
lay.cols = FITS_W ? tcols : MAX_COLS;
const int TOTAL_ROWS = (lay.cols > 0) ? ((lay.total_tiles + lay.cols - 1) / lay.cols) : 0;
// Si tot el contingut cap sense scroll, encongim la caixa a les
// files reals; així podem centrar-la verticalment.
lay.rows_visible = std::min(MAX_ROWS, TOTAL_ROWS);
lay.max_scroll = std::max(0, TOTAL_ROWS - lay.rows_visible);
// Mida real de la caixa: tiles + spacing intern + 1 px de border per cara.
const int TILES_W = (lay.cols > 0) ? ((lay.cols * lay.tile_size) + ((lay.cols - 1) * lay.spacing)) : 0;
const int TILES_H = (lay.rows_visible > 0) ? ((lay.rows_visible * lay.tile_size) + ((lay.rows_visible - 1) * lay.spacing)) : 0;
lay.w = TILES_W + (2 * PICKER_BORDER);
lay.h = TILES_H + (2 * PICKER_BORDER);
// Centrat horitzontal i vertical dins de l'àrea interna; quan el
// picker arriba a l'alçada màxima, el centrat vertical degenera a
// origen=M (no hi ha espai extra que repartir).
lay.x = M + ((INNER_W - lay.w) / 2);
lay.y = M + ((INNER_H - lay.h) / 2);
return lay;
}
// Origen (cantonada superior-esquerra) del tile a la cel·la (vc, vr)
// dins del picker. Centralitzat perquè render i hit-test no divergeixin.
auto pickerTileOrigin(const PickerLayout& lay, int vc, int vr) -> std::pair<int, int> {
const int PX = lay.x + PICKER_BORDER + (vc * lay.pitch);
const int PY = lay.y + PICKER_BORDER + (vr * lay.pitch);
return {PX, PY};
}
} // namespace
void Editor::enter() {
@@ -76,11 +121,13 @@ void Editor::exit(Tilemap& tilemap) {
if (!active_) { return; }
active_ = false;
picker_open_ = false;
// Resolem la ruta via Resource::Helper perquè en Debug el root de
// dades és AEE_SOURCE_DIR (ruta absoluta al repo), no el CWD. Sense
// això, el writer obriria `data/maps/pyramid.yaml` relatiu al CWD
// i fallaria sempre excepte si el binari es llança des del repo root.
const auto ABS_PATH = Resource::Helper::resolve(Defaults::Editor::SAVE_PATH);
// El writer salva sempre sobre la sala actualment carregada (segons
// el LevelManager): així el "9" de l'editor desa el que tens davant,
// no una constant fixa. Resolem la ruta via Resource::Helper perquè
// en Debug el root de dades és AEE_SOURCE_DIR (ruta absoluta al repo),
// no el CWD; sense això, el writer obriria el path relatiu al CWD i
// fallaria si el binari no es llança des del repo root.
const auto ABS_PATH = Resource::Helper::resolve(Levels::LevelManager::get().currentPath());
const bool SAVED = tilemap.save(ABS_PATH);
Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error"));
}
@@ -168,11 +215,21 @@ void Editor::updateMap(Tilemap& tilemap, int canvas_x, int canvas_y) {
void Editor::updatePicker(const Tilemap& tilemap, int canvas_x, int canvas_y) {
const auto LAY = layoutPicker(tilemap);
picker_hover_ = -1;
if (canvas_x >= LAY.x && canvas_x < LAY.x + LAY.w && canvas_y >= LAY.y && canvas_y < LAY.y + LAY.h && LAY.tile_size > 0) {
const int VC = (canvas_x - LAY.x) / LAY.tile_size;
const int VR = (canvas_y - LAY.y) / LAY.tile_size;
const int IDX = ((picker_scroll_ + VR) * LAY.cols) + VC;
if (IDX < LAY.total_tiles) { picker_hover_ = IDX; }
// Espai útil per als tiles dins de la caixa (descomptat el border 1 px).
const int INNER_X = LAY.x + PICKER_BORDER;
const int INNER_Y = LAY.y + PICKER_BORDER;
const int RX = canvas_x - INNER_X;
const int RY = canvas_y - INNER_Y;
if (RX >= 0 && RY >= 0 && LAY.tile_size > 0 && LAY.pitch > 0 && LAY.cols > 0) {
const int VC = RX / LAY.pitch;
const int VR = RY / LAY.pitch;
// Píxels de spacing entre cel·les: el clic cau a la "vora morta".
const bool IN_SPACING_X = (RX % LAY.pitch) >= LAY.tile_size;
const bool IN_SPACING_Y = (RY % LAY.pitch) >= LAY.tile_size;
if (!IN_SPACING_X && !IN_SPACING_Y && VC < LAY.cols && VR < LAY.rows_visible) {
const int IDX = ((picker_scroll_ + VR) * LAY.cols) + VC;
if (IDX < LAY.total_tiles) { picker_hover_ = IDX; }
}
}
for (const auto& ev : EventBuffer::events) {
@@ -203,7 +260,7 @@ void Editor::render(const Tilemap& tilemap) const {
tilemap.renderTilePreview(PX, PY, brush_);
}
constexpr auto BORDER = Defaults::Editor::BORDER_ARGB;
const auto BORDER = EditorConfig::get().borderArgb();
screen.fillRect(0, 0, W, 1, BORDER);
screen.fillRect(0, H - 1, W, 1, BORDER);
screen.fillRect(0, 0, 1, H, BORDER);
@@ -212,34 +269,43 @@ void Editor::render(const Tilemap& tilemap) const {
void Editor::renderPicker(const Tilemap& tilemap) const {
const auto LAY = layoutPicker(tilemap);
if (LAY.cols <= 0 || LAY.rows_visible <= 0) { return; }
auto& screen = Screen::get();
screen.fillRect(LAY.x, LAY.y, LAY.w, LAY.h, Defaults::Editor::PICKER_BG_ARGB);
const auto& cfg = EditorConfig::get();
screen.fillRect(LAY.x, LAY.y, LAY.w, LAY.h, cfg.pickerBgArgb());
for (int vr = 0; vr < LAY.rows_visible; ++vr) {
const int LOGICAL_ROW = picker_scroll_ + vr;
for (int vc = 0; vc < LAY.cols; ++vc) {
const int IDX = (LOGICAL_ROW * LAY.cols) + vc;
if (IDX >= LAY.total_tiles) { break; }
const int PX = LAY.x + (vc * LAY.tile_size);
const int PY = LAY.y + (vr * LAY.tile_size);
const auto [PX, PY] = pickerTileOrigin(LAY, vc, vr);
tilemap.renderTilePreview(PX, PY, IDX);
}
}
if (picker_hover_ >= 0 && LAY.cols > 0) {
if (picker_hover_ >= 0) {
const int VR = (picker_hover_ / LAY.cols) - picker_scroll_;
const int VC = picker_hover_ % LAY.cols;
if (VR >= 0 && VR < LAY.rows_visible) {
const int PX = LAY.x + (VC * LAY.tile_size);
const int PY = LAY.y + (VR * LAY.tile_size);
const auto [PX, PY] = pickerTileOrigin(LAY, VC, VR);
const int TS = LAY.tile_size;
constexpr auto C = Defaults::Editor::PICKER_HOVER_ARGB;
const auto C = cfg.pickerHoverArgb();
screen.fillRect(PX, PY, TS, 1, C);
screen.fillRect(PX, PY + TS - 1, TS, 1, C);
screen.fillRect(PX, PY, 1, TS, C);
screen.fillRect(PX + TS - 1, PY, 1, TS, C);
}
}
// Border 1 px del picker, dibuixat al damunt del fons i dels tiles
// perquè delimite clarament la caixa també quan hi ha tiles que
// arriben a la vora.
const auto BORDER = cfg.borderArgb();
screen.fillRect(LAY.x, LAY.y, LAY.w, 1, BORDER);
screen.fillRect(LAY.x, LAY.y + LAY.h - 1, LAY.w, 1, BORDER);
screen.fillRect(LAY.x, LAY.y, 1, LAY.h, BORDER);
screen.fillRect(LAY.x + LAY.w - 1, LAY.y, 1, LAY.h, BORDER);
}
#endif // _DEBUG
+85
View File
@@ -0,0 +1,85 @@
#include "game/editor/editor_config.hpp"
#ifdef _DEBUG
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <string>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/defaults.hpp"
namespace {
template <typename T>
void readField(const fkyaml::node& node, const char* key, T& dst) {
if (node.contains(key)) { dst = node[key].get_value<T>(); }
}
// Llegeix un color ARGB. Accepta enter (0xFF2244FF al YAML) o cadena
// ("0xFF2244FF" / "#FF2244FF"). std::stoul amb base=0 detecta el prefix
// "0x" automàticament; si el valor és invàlid, deixem `dst` intacte i
// avisem perquè l'error no passi en silenci.
void readArgb(const fkyaml::node& node, const char* key, std::uint32_t& dst) {
if (!node.contains(key)) { return; }
try {
const auto& v = node[key];
if (v.is_integer()) {
dst = static_cast<std::uint32_t>(v.get_value<std::uint64_t>());
return;
}
auto s = v.get_value<std::string>();
if (!s.empty() && s.front() == '#') { s.erase(0, 1); }
dst = static_cast<std::uint32_t>(std::stoul(s, nullptr, 0));
} catch (const std::exception& e) {
std::cerr << "EditorConfig: color '" << key << "' invàlid (" << e.what() << ") — ignorat\n";
}
}
} // namespace
EditorConfig::EditorConfig()
: picker_margin_(Defaults::Editor::PICKER_MARGIN),
picker_spacing_(Defaults::Editor::PICKER_SPACING),
border_argb_(Defaults::Editor::BORDER_ARGB),
picker_bg_argb_(Defaults::Editor::PICKER_BG_ARGB),
picker_hover_argb_(Defaults::Editor::PICKER_HOVER_ARGB) {
}
auto EditorConfig::get() -> EditorConfig& {
static EditorConfig instance_;
return instance_;
}
void EditorConfig::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path);
if (bytes.empty()) {
std::cerr << "EditorConfig: no s'ha pogut obrir " << file_path << " — s'usaran els defaults compilats\n";
return;
}
try {
const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
if (yaml.contains("picker")) {
const auto& p = yaml["picker"];
readField(p, "margin", picker_margin_);
readField(p, "spacing", picker_spacing_);
}
if (yaml.contains("colors")) {
const auto& c = yaml["colors"];
readArgb(c, "border", border_argb_);
readArgb(c, "picker_bg", picker_bg_argb_);
readArgb(c, "picker_hover", picker_hover_argb_);
}
std::cout << "EditorConfig: carregat des de " << file_path << '\n';
} catch (const std::exception& e) {
std::cerr << "EditorConfig: error parsejant " << file_path << ": " << e.what() << '\n';
}
}
#endif // _DEBUG
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#ifdef _DEBUG
#include <cstdint>
#include <string>
// Configuració visual de l'editor de tilemap (Debug-only). Carregada des
// de data/editor/editor.yaml; les claus absents cauen als valors compilats
// a Defaults::Editor. Mateix patró que UiConfig (Meyers singleton + load).
class EditorConfig {
public:
static auto get() -> EditorConfig&;
EditorConfig(const EditorConfig&) = delete;
EditorConfig(EditorConfig&&) = delete;
auto operator=(const EditorConfig&) -> EditorConfig& = delete;
auto operator=(EditorConfig&&) -> EditorConfig& = delete;
void load(const std::string& file_path);
[[nodiscard]] auto pickerMargin() const -> int { return picker_margin_; }
[[nodiscard]] auto pickerSpacing() const -> int { return picker_spacing_; }
[[nodiscard]] auto borderArgb() const -> std::uint32_t { return border_argb_; }
[[nodiscard]] auto pickerBgArgb() const -> std::uint32_t { return picker_bg_argb_; }
[[nodiscard]] auto pickerHoverArgb() const -> std::uint32_t { return picker_hover_argb_; }
private:
EditorConfig();
~EditorConfig() = default;
int picker_margin_;
int picker_spacing_;
std::uint32_t border_argb_;
std::uint32_t picker_bg_argb_;
std::uint32_t picker_hover_argb_;
};
#endif // _DEBUG
+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};
};
+18 -4
View File
@@ -6,6 +6,7 @@
#include "game/entities/entity_factory.hpp"
#include "game/entities/event_bus.hpp"
#include "game/entities/player.hpp"
#include "game/map/map.hpp"
namespace {
@@ -65,21 +66,34 @@ void Door::update(float delta_time) {
}
void Door::onPlayerContact(Player* player) {
if (state_ != State::OPEN || level_complete_emitted_) { return; }
// Només la porta OPEN despawna jugadors. CLOSED/OPENING ignoren el
// contacte (i a més, en CLOSED, el `Map::collides` ja bloqueja el
// moviment, així que el contacte no es dóna en condicions normals).
if (state_ != State::OPEN || player == nullptr || player->isExited()) { return; }
// Marquem el jugador com a EXITED i publiquem l'event d'agregació.
// El check `isExited` de la guarda anterior garanteix que només
// emetem l'event en la primera transició — `EntityManager` compta
// emissions per decidir quan publicar `level_complete`.
player->exit();
if (bus_ != nullptr) {
bus_->publish(Event{
.name = "level_complete",
.name = "player_exited",
.payload = std::monostate{},
.player_id = (player != nullptr) ? player->getId() : -1,
.player_id = player->getId(),
});
}
level_complete_emitted_ = true;
}
void Door::open() {
if (state_ != State::CLOSED) { return; }
state_ = State::OPENING;
setAnimation(anim_opening_);
// Marquem el passadís com a transitable: a partir d'aquest tic
// `Map::collides` deixa de bloquejar el rect del passadís. La
// transició d'animació OPENING → OPEN encara dura el seu temps,
// però la geometria ja és permeable — coherent amb el feel de
// "la porta s'obri i el jugador pot avançar al moment".
Map::door_open = true;
// Edge case: si `opening` té ≤1 frame i és loop=true, mai es
// completarà. Per força, forcem loop=false del frame únic aquí. Els
// placeholders defineixen `loop: false` explícitament al YAML, així
+7 -5
View File
@@ -7,11 +7,14 @@
#include "game/entities/interactable.hpp"
// Porta de sortida del nivell. Tres estats:
// CLOSED → inicial; bloqueja el pas.
// CLOSED → inicial; el passadís de Map bloqueja el pas via `Map::collides`.
// OPENING → animació d'obertura en curs. Quan el component acaba →
// transita a OPEN.
// OPEN → accessible. Al contacte amb el jugador publica
// `level_complete` al bus (si n'hi ha).
// transita a OPEN. El passadís ja és transitable (s'allibera
// a `open()` amb `Map::door_open = true`).
// OPEN → accessible. Al contacte amb un jugador, crida `Player::exit()`
// — el jugador queda despawnat i emet `player_exited` al bus.
// L'agregació "tots els jugadors han eixit → `level_complete`"
// viu a EntityManager.
//
// El que decideix quan s'obri és extern (`EntityManager` comptant sarcs
// oberts) via `open()`. L'animació `opening` es fa servir com a
@@ -38,7 +41,6 @@ class Door : public Interactable {
void setAnimation(const AnimationComponent& anim);
State state_{State::CLOSED};
bool level_complete_emitted_{false};
// Les 3 animacions precalculades al ctor des del YAML. L'activa
// s'assigna a `anim_` (heretat d'Entity) segons l'estat.
@@ -10,6 +10,10 @@ FloorTrap::FloorTrap(const fkyaml::node& yaml) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("floor_trap");
collides_ = false;
interactable_ = false;
// Trigger d'àrea: la trampa no bloqueja el pas però vol rebre
// `onPlayerContact` quan el jugador la solapa. Tanca el gap latent
// que la deixava muda (`checkPlayerCollisions` filtrava pel `collides`).
triggers_on_overlap_ = true;
if (yaml.contains("cycle")) {
const auto& c = yaml["cycle"];
+54
View File
@@ -0,0 +1,54 @@
#include "game/entities/concrete/hole.hpp"
#include <SDL3/SDL.h>
#include <string>
#include "game/entities/entity_factory.hpp"
#include "game/entities/player.hpp"
Hole::Hole(const fkyaml::node& yaml) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("hole");
// Sense xoc físic ni interacció — només trigger d'àrea. El bucle de
// contacte ens admet via `triggers_on_overlap_=true`. Damage queda a
// 0 (no publiquem `player_hit`); la mort la dispara `integrateJump`
// del jugador quan supera `HOLE_FALL_DEPTH`.
collides_ = false;
interactable_ = false;
triggers_on_overlap_ = true;
// Decal de sòl: `Entity::getFeetY()` torna sentinel mínim si aquest
// flag està actiu — així el forat es pinta abans que qualsevol actor.
is_floor_decal_ = true;
setDamage(0);
}
void Hole::onPlayerContact(Player* player) {
if (player == nullptr) { return; }
// Només mossega quan el jugador trepitja el forat de peu (GROUNDED).
// En l'aire, el filtre d'altura del bucle ja l'ha rebutjat (height_=0
// i jump_z_>0 prou per superar-ho); però mantenim el gat per defensa
// davant de futurs canvis al filtre.
if (player->getState() != Player::State::GROUNDED) { return; }
// Re-verificació estricta: la hit-box interna del jugador (8×8) ha
// d'estar COMPLETAMENT continguda dins del rect del forat (16×16). El
// bucle de col·lisió del manager ja ens ha cridat per qualsevol solape;
// ací filtrem perquè la caiguda només es dispari quan els "dos peus"
// estan dins del forat, no quan només la vora frega — sinó es percep
// com un imant que succiona el jugador al centre.
const SDL_FRect HIT = player->getHitBox();
const SDL_FRect BOX = getCollisionRect();
const bool CONTAINED = HIT.x >= BOX.x && HIT.y >= BOX.y && HIT.x + HIT.w <= BOX.x + BOX.w && HIT.y + HIT.h <= BOX.y + BOX.h;
if (!CONTAINED) { return; }
// Snap al centre XY del forat + Y de la base del tile (per al clip
// rect del sprite mentre cau). `enterHoleFall` rep coordenades
// absolutes en el canvas — el player s'encarrega de convertir a la
// seua convenció (cantó superior-esquerra del sprite).
const float CENTER_X = BOX.x + (BOX.w * 0.5F);
const float CENTER_Y = BOX.y + (BOX.h * 0.5F);
const float FLOOR_Y = BOX.y + BOX.h;
player->enterHoleFall(CENTER_X, CENTER_Y, FLOOR_Y);
}
REGISTER_ENTITY("hole", Hole)
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include "external/fkyaml_node.hpp"
#include "game/entities/hazard.hpp"
// Forat al sòl: hazard estàtic de 16×16 que NO bloqueja el pas però
// detecta quan el jugador hi cau a sobre. Característiques:
// - `collides_=false` + `triggers_on_overlap_=true`: rep `onPlayerContact`
// pel bucle de `EntityManager::checkPlayerCollisions` sense bloquejar
// el moviment del jugador.
// - `height_=0`: el filtre per altura del bucle (`jump_z > height`) NO
// s'activa amb height a zero, així que el bucle de contacte SÍ crida
// `onPlayerContact` també per a jugadors AIRBORNE. Qui rebutja els
// estats distints de GROUNDED (saltant, caient, HURT, DYING...) és el
// guard explícit dins de `Hole::onPlayerContact`. El filtre d'altura
// és una optimització ortogonal del bucle, no la defensa principal.
// - Damage 0: el contacte no publica `player_hit`. En lloc d'això,
// `onPlayerContact` invoca `Player::enterHoleFall`, que transiciona
// el jugador a AIRBORNE amb subestat `falling_through_hole_`.
// - Verificació amb hit-box interna: `onPlayerContact` re-verifica
// l'overlap amb `Player::getHitBox()` (8×8 centrada al sprite),
// així només es dispara quan el centre del jugador entra al tile.
// - Z-order: `is_floor_decal_=true` (heretat) — `Entity::getFeetY()`
// retorna sentinel mínim, així el forat es pinta abans que qualsevol
// actor i les momies passen per davant. L'efecte "el sòl es traga el
// jugador" no es fa amb Y-sort: el `Player::render` aplica un clip
// rect mentre `falling_through_hole_=true` que retalla el sprite per
// sota de la base del tile del forat.
class Hole : public StaticHazard {
public:
explicit Hole(const fkyaml::node& yaml);
// No publica `player_hit`. En el seu lloc, dispara
// `Player::enterHoleFall(center_x, center_y, floor_y)` perquè el
// jugador armi el clip rect a la Y de la base del forat.
void onPlayerContact(Player* player) override;
};
// TODO(hole-feature): afegir reset de `falling_through_hole_` i `hole_clip_bottom_y_` a `Player::disable()` per simetria.
// TODO(hole-feature): afegir tests dedicats del comportament de la mòmia amb forats (no requereixen Player real).
Loaded 100 of 135 files, more files were not shown because too many files have changed in this diff. Show more