Compare commits

...
108 Commits
Author SHA1 Message Date
JailDesigner fa77f4cee3 chore(version): bump 0.45.0 -> 0.46.0 2026-05-13 21:43:00 +02:00
JailDesigner a02706ac8b Merge branch 'refactor/player-instance-config' 2026-05-13 21:42:49 +02:00
JailDesigner 2aef925286 refactor(player): config a membres d'instancia (P1/P2 YAMLs independents) 2026-05-13 21:40:02 +02:00
JailDesigner 8eba52353b chore(version): bump 0.44.0 -> 0.45.0 2026-05-13 19:44:47 +02:00
JailDesigner 15a4016407 Merge branch 'feature/lighting-additive' 2026-05-13 19:44:31 +02:00
JailDesigner 2272d3a37a feat(lighting): flicker proporcional al radi amb overrides per emissor 2026-05-13 19:38:34 +02:00
JailDesigner 3a42c7958a feat(lighting): offset del centre de la llum per emissor 2026-05-13 18:26:19 +02:00
JailDesigner dacc4a9909 feat(lighting): corba additive separada al config 2026-05-13 17:53:32 +02:00
JailDesigner 771ff1f6cd feat(lighting): mode additive amb color per emissor 2026-05-13 14:11:51 +02:00
JailDesigner 24609d6c5c Merge branch 'feature/animation-loader-unification' 2026-05-13 13:27:51 +02:00
JailDesigner 9d0dde38f9 feat(lighting): override d'emissors per sala i per spawn 2026-05-13 12:52:08 +02:00
JailDesigner b76a3eb1f3 refactor(entities): unifica loader d'animacio via AnimationComponent::fromYaml i elimina fps: 2026-05-13 10:06:56 +02:00
JailDesigner a779533d61 fix(release): mou GIFs a la carpeta de l'entitat i silencia -Wcomment a director 2026-05-13 08:18:53 +02:00
JailDesigner bc6ad6f718 chore(version): bump 0.43.0 -> 0.44.0 2026-05-12 22:41:43 +02:00
JailDesigner 9bd5e7db33 Merge branch 'feature/lighting' 2026-05-12 22:40:55 +02:00
JailDesigner 5cc8e349a5 feat(lighting): seed estable per emissor (no deriva pel moviment) 2026-05-12 22:37:27 +02:00
JailDesigner 0046d526b0 fix(lighting): llig flicker tambe del YAML del jugador 2026-05-12 22:26:17 +02:00
JailDesigner fc9947b87f feat(data): flicker tambe a jugadors i momies 2026-05-12 22:17:36 +02:00
JailDesigner 35aaa6c519 feat(lighting): flicker tipus foc per antorxes 2026-05-12 22:07:25 +02:00
JailDesigner 3195746189 feat(data): activa la llum del jugador, antorxes i mòmies + sala test 2026-05-12 21:14:16 +02:00
JailDesigner d1d5679d14 feat(game_scene): wire lighting pass entre actors i HUD 2026-05-12 21:12:06 +02:00
JailDesigner ebb161f1f5 feat(entities): light_radius + getLightCenter a Entity i Player 2026-05-12 21:06:01 +02:00
JailDesigner 68be8e55ec feat(map): Map::light_level per sala des del YAML 2026-05-12 20:58:30 +02:00
JailDesigner 5ed963b53d feat(rendering): lighting module CPU + gradient config YAML 2026-05-12 20:56:27 +02:00
JailDesigner 5e5ff835a9 Merge branch 'feature/player-stats-bus' 2026-05-12 19:55:50 +02:00
JailDesigner f5d251c33b feat(stats): Player com a font de veritat de vides/score/tresors i sarcs amb recompensa 2026-05-12 19:51:42 +02:00
JailDesigner d186f66c2c art(player): paletes d'immunitat per-personatge (wash, ice, gold) 2026-05-12 19:10:00 +02:00
JailDesigner da361a1452 feat(editor): mostra mommies i altres dynamic hazards al mode editor 2026-05-12 13:58:45 +02:00
JailDesigner a43ae21014 feat(editor): preview del brush d'entitat i fons del picker mes clar 2026-05-12 13:35:01 +02:00
JailDesigner 6749780356 feat(entities): afegeix stone i column com a muros decoratius 2026-05-12 13:03:35 +02:00
JailDesigner 1c49248b72 art(debris): afegeix column.gif i stone.gif 2026-05-12 11:06:42 +02:00
JailDesigner 63a03ae632 refactor(pause): elimina pausa F11 (cobreix-la el menú de servei) 2026-05-12 10:52:46 +02:00
JailDesigner a8bff31a7c chore(fonts): regenera ari-w9500 sense accents 2026-05-12 10:39:10 +02:00
JailDesigner 793e87a203 feat(hud): parpadeig configurable als prompts inactius 2026-05-12 10:27:23 +02:00
JailDesigner 642e0653b2 fix(sarcophagus): inclou les cantonades diagonals al perímetre 2026-05-12 09:00:56 +02:00
JailDesigner d26abcb9d5 chore(version): bump 0.42.0 -> 0.43.0 2026-05-11 22:56:14 +02:00
JailDesigner a358d8ddbd Merge branch 'feat/assets-pipeline' 2026-05-11 22:55:50 +02:00
JailDesigner f7c4d62234 feat(screenshot): desa-les a la carpeta de sistema per plataforma 2026-05-11 22:50:45 +02:00
JailDesigner 06a49e09ba style(hud): rètol de sala daurat sobre banda violeta 2026-05-11 22:31:12 +02:00
JailDesigner 629baaaec3 feat(hud): famílies de color per bloc + ombra del text 2026-05-11 22:18:56 +02:00
JailDesigner 932d195eb0 feat(audio): so del sarcòfag amb pitch aleatori per obertura 2026-05-11 20:46:18 +02:00
JailDesigner 3e31a88f78 fix(anim): walk_* arrenca en frame distint del stand 2026-05-11 19:56:16 +02:00
JailDesigner cba46642c6 feat(mummy): SPAWN dirigit per l'animació raise 2026-05-11 19:47:28 +02:00
JailDesigner 8c84892250 art(dust): retocs al sprite 2026-05-11 17:44:10 +02:00
JailDesigner d14da0e61f fix(make): el target pack no canvia el build_type 2026-05-11 17:30:15 +02:00
JailDesigner 8bdc994b75 feat(render): sombra com a sprite 16x16 compartit 2026-05-11 17:29:05 +02:00
JailDesigner 6a70d6de9f style(hud): sentence case als prompts del slot inactiu 2026-05-11 16:32:57 +02:00
JailDesigner 2523661c8f refactor(game): unifica renderitzat dels rètols de transició via TextScreenSpec 2026-05-11 16:32:57 +02:00
JailDesigner fa27308897 chore(version): bump 0.41.0 -> 0.42.0 2026-05-11 13:34:01 +02:00
JailDesigner e58eb10d6d Merge branch 'feat/single-bucket-polymorphism'
Refactor del sistema d'entitats per substituir filtres i
dynamic_casts dispersos per dispatch polimòrfic.

Quatre virtuals nous a Entity (`onWhipHit`, `onJustSpawned`,
`onRemoved`, `shouldReceiveContact`) encapsulen dins de cada
subclasse les decisions que abans vivien al manager:
- WhipAttack::applyDamage ja no fa `dynamic_cast<Mummy*>`.
- spawnPendingActors/spawnActor ja no fan `dynamic_cast<DynamicHazard*>`.
- removeEntity ja no compara `getName() == "sarcophagus"`.
- checkPlayerCollisions filtra amb un sol predicat virtual.

Amb el polimorfisme madurat, el sac dual `map_entities_`/`actors_`
introduit a la rama anterior ja no aporta valor: tornem a un sol
`entities_` (manté `pending_actors_` per al spawn diferit). El
manager itera un sol vector i cada entitat decideix què fa.

Cap dynamic_cast nou per a lògica de joc — només queden els
inevitables (validació de factory i ruteig de la Door).
2026-05-11 13:33:46 +02:00
JailDesigner a29ea641ab refactor(em): un sol vector entities_ — polimorfisme per la
discriminació

Reverteix la separació en sacs introduïda al commit 62f4b85 (rama
anterior). Amb els virtuals afegits al commit precedent
(`onWhipHit`, `onJustSpawned`, `shouldReceiveContact`, `onRemoved`),
ja no cal separar `map_entities_` i `actors_`: cada entitat decideix
internament què fa, el manager itera un sol vector i confia en el
dispatch virtual.

Canvis:

- `entities_` torna a ser el contenidor únic. Es desfà el dispatch
  `dynamic_cast<DynamicHazard*>` que `add()` feia per enrutar.
- `addSarcophagus`, `removeEntity`, `update`, `notifyPlayerTilePainted`,
  `findFloorTriggerAt`, `renderAll`, `reload` simplificats: un sol
  bucle, sense distingir categories.
- `checkPlayerCollisions` només recorre `entities_` cridant
  `shouldReceiveContact` polimòrfic — la lògica de "saltar SPAWN"
  viu a `DynamicHazard::shouldReceiveContact`.
- `getMapEntities()` i `getActors()` desapareixen. Es manté
  `getEntities()` com a accessor únic.
- `WhipAttack::applyDamage` itera `getEntities()` cridant `onWhipHit`
  polimòrfic. Les entitats no-whippables retornen false (cost: una
  crida virtual buida) i el callsite no discrimina tipus.
- `GameScene::render` mergeja entities + players (dos transforms en
  comptes de tres) per al z-order.

`pending_actors_` es manté: és un concepte ortogonal — entitats que
encara no s'han instanciat al món fins que GameScene les "spawneja"
al text-exit de ROOM_INTRO.

Beneficis del nou disseny:
- Cap dynamic_cast dispers per a la lògica de joc (només queden els
  inevitables: validació de factory i ruteig de la Door).
- Afegir un nou enemic implica `class X : public DynamicHazard` i
  override dels virtuals rellevants — res al manager.
- Si un sarc, antorxa, o decoració futura volgués reaccionar al
  fueteig (trencar-se, encendre's), basta amb un `onWhipHit` override
  i no cal tocar `WhipAttack::applyDamage`.
2026-05-11 13:14:53 +02:00
JailDesigner 28ff425c89 refactor(entities): polimorfisme per substituir dynamic_casts
Quatre virtuals nous a `Entity` capten decisions que abans vivien al
manager com a discriminacions de tipus:

- `onWhipHit(WhipAttack*)` → bool. Default no-op/false. Override a
  `Mummy` per entrar HURT si és whippable. WhipAttack::applyDamage
  passa de `dynamic_cast<Mummy*>` + `isWhippable()` a una crida
  polimòrfica neta. RollingRock no override → immune al látigo per
  disseny, sense flag.

- `onJustSpawned()`. Default no-op. Override a `DynamicHazard` per
  disparar `triggerSpawnExit()`. `spawnPendingActors` i `spawnActor`
  passen de `dynamic_cast<DynamicHazard*>(observer)->triggerSpawnExit()`
  a `observer->onJustSpawned()`.

- `onRemoved()`. Default no-op. Override a `Sarcophagus` per cridar
  `entity_manager_->onSarcophagusRemoved()`. `removeEntity` passa de
  `getName() == "sarcophagus"` a dispatch polimòrfic.

- `shouldReceiveContact()`. Default `collides() || triggersOnOverlap()`.
  Override a `DynamicHazard` per excloure SPAWN. `checkPlayerCollisions`
  passa de tres condicions inline per a sacs distints a una crida
  unificada.

`whipDirToMummyDir` es mou de whip_attack.cpp a mummy.cpp — el mapping
viu on s'usa (dins de `Mummy::onWhipHit`).

`hitMummyIfApplicable` es renomena a `hitEntityIfApplicable`: ja no
discrimina tipus, aplica `entity->onWhipHit` i marca `already_hit_`
segons el retorn.

Comportament idèntic, tests verds. Aquest commit prepara el següent:
amb el polimorfisme madurat, el sac dual ja no aporta valor i podem
tornar a un únic `entities_`.
2026-05-11 13:01:26 +02:00
JailDesigner e8418f89e0 chore(version): bump 0.40.0 -> 0.41.0 2026-05-11 11:31:24 +02:00
JailDesigner 9ca09484a6 Merge branch 'feat/dynamic-hazard-spawn-state'
Spawn on-demand de DynamicHazards via dos sacs a EntityManager:
les mòmies (i futurs hazards mòbils) ara s'instancien just quan
GameScene les "spawneja", no abans. Cinemàtica ROOM_INTRO neta:
mapa buit durant el text, mòmies surten emergint al text-exit
(fase 4, durada 1.0 s), jugadors cauen del cel a continuació.

Estat SPAWN/ACTIVE a DynamicHazard reutilitzable per a futurs
generadors (gotes, roques, etc.) via `EntityManager::spawnActor`.

Refactor col·lateral: EntityManager separa `map_entities_` /
`actors_` / `pending_actors_`. Render mergeja els tres orígens
(amb players). dynamic_casts dispersos a WhipAttack i col·lisió
substituïts per iteracions sobre el sac d'actors.

Bonus: arregla 4 tests latents de test_whip_attack que mai no
havien passat (timing off-by-one al primer update).
2026-05-11 11:31:03 +02:00
JailDesigner 9291ef6572 test(whip): arregla W4/W6 — first update ha d'arribar al primer active frame
El YAML del whip declara frame_ms=80, així que el frame 0 viu [0, 80] ms
i `active_frames=[1, 2]` comença a 80 ms. Els tests feien `update(0.06F)`
amb el comentari "60 ms → frame 1 (active)" — incorrecte: 60 < 80, encara
al frame 0, sense danyar res. Tres asserts d'`getState() == HURT` mai
no es complien.

Canvi a `update(0.09F)`: assegura que el primer tick entra al frame 1
i `applyDamage()` corre. Comentari actualitzat amb el càlcul correcte.
2026-05-11 11:28:51 +02:00
JailDesigner 6f8ab74d6b refactor(whip): applyDamage itera getActors() en lloc de getEntities() 2026-05-11 11:23:14 +02:00
JailDesignerandClaude Opus 4.7 881e0ea1f6 feat(room): mòmies surten emergint quan comença text-exit (fase 4)
Tres canvis coordinats que tanquen la cinemàtica desitjada de
ROOM_INTRO:

1. `spawn.duration_s` de mummy.yaml passa de 0.6 a 1.0 s. Les mòmies
   tarden ~1 segon a "emergir" — temps suficient perquè el jugador
   noti que estan apareixent, no que ja eren a la sala.

2. El disparador es mou de fase 5 (music_tail) a fase 4 (text-exit).
   Ara, just quan el text "PIRÀMIDE X, SALA Y" comença a sortir de
   pantalla, GameScene crida `entity_manager_.spawnPendingActors()`
   i les mòmies s'instancien ja en EMERGING. Visualment apareixen
   alhora que el text desapareix; durant la fase 5 acaben d'emergir
   i a l'entrada a PLAYING ja són ACTIVE — coincidint amb la caiguda
   aèria dels jugadors.

3. `completeIntro` afegeix un fallback: si arribem a PLAYING sense
   haver passat per fase 4 (config patològica, fases molt curtes),
   instanciem els pending igualment per garantir que la sala mai no
   queda sense enemics.

Renomenat: `hazards_spawn_triggered_` → `pending_actors_spawned_`,
nom que reflecteix què representa el flag al model nou.

El mètode privat `triggerHazardSpawnExit()` desapareix: era un
wrapper que ja només cridava `spawnPendingActors()`. Crida directa
des de `updateRoomTransition`, `respawnPlayersInstant` i
`completeIntro` (fallback).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:40:28 +02:00
JailDesignerandClaude Opus 4.7 6a6a1a52c7 feat(em): spawn diferit de DynamicHazards (pending_actors_)
`loadFromLevel` ja no instancia immediatament les DynamicHazards del
YAML del mapa: les acumula a `pending_actors_` (estructura {type,
spawn_node}). El loader decideix per la categoria semàntica de la
plantilla (`type:` del YAML d'entity: "dynamic_hazard" → pending; la
resta → addSpawn d'instant).

Dues APIs noves consumeixen pending_actors_:
- `spawnPendingActors()` — itera la cua, instancia tots via addSpawn
  i arma `triggerSpawnExit()` a cada un. Vacia la cua. El path
  d'aparició massiva al final del text-exit de ROOM_INTRO.
- `spawnActor(type, col, row)` — instancia un únic actor on-demand
  des d'un spawn YAML sintètic. Pensat per a futurs generadors
  (StaticHazards que produeixen DynamicHazards en runtime: gotes,
  roques, etc.). Cada actor neix ja en EMERGING.

`GameScene::triggerHazardSpawnExit` ara és un wrapper de
`spawnPendingActors()` — el dispar segueix en la subfase actual
(fase 5 de ROOM_INTRO) i a `respawnPlayersInstant`. El canvi de timing
ve en el commit següent.

`reload` també buida pending_actors_. Log: "N map entities + M actors
pending des de PATH" — visible per a diagnòstic.

Sense canvis de comportament observables al joc: les mòmies segueixen
apareixent al final del text-exit, però ara la "aparició" és la seua
instanciació real (abans ja existien quietes des del fade-in).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:35:02 +02:00
JailDesignerandClaude Opus 4.7 62f4b857ef refactor(em): separa entitats en dos sacs (map_entities_/actors_)
EntityManager guanya dues llistes lògiques:
- `map_entities_` — decoració, static_hazards (Hole, FloorTrap, WallTrap)
  i interactables (Sarcophagus, Door). Persistents tota la sala.
- `actors_` — DynamicHazards (Mummy, RollingRock, futurs enemics).
  Tipats com `unique_ptr<DynamicHazard>`, sense dynamic_cast.

`add()` enruta automàticament segons sigui DynamicHazard o no.
`update`, `notifyPlayerTilePainted`, `findFloorTriggerAt`, `removeEntity`,
`reload` i `renderAll` iteren el(s) sac(s) que toca.

`checkPlayerCollisions`: el filtre de SPAWN sobre el sac d'actors ja no
necessita dynamic_cast — `actor->isSpawning()` és directe. El filtre per
floor decals només toca map_entities_ (els decals viuen exclusivament
allà). Filtres per-jugador extrets a helpers per acotar complexitat.

API nova: `getMapEntities()` i `getActors()` exposen cada sac per
separat. `getEntities()` es manté com a concatenació per retro-compat
(editor, z-sort debug).

`GameScene::triggerHazardSpawnExit` itera `getActors()` directe — fora
dynamic_cast.

`GameScene::render` mergeja els tres orígens (map + actors + players)
en un sol vector temporal abans del stable_sort per `feet_y` — mateix
patró que ja s'usava amb players, ara amb una transformació més.

Comportament idèntic: les momies segueixen instanciant-se al
loadFromLevel. La preparació per al spawn diferit ve en el commit
següent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:23:44 +02:00
JailDesignerandClaude Opus 4.7 3b378e2b29 feat(room): allibera DynamicHazards del SPAWN al final del text-INTRO
GameScene ara crida `triggerHazardSpawnExit()` quan ROOM_INTRO entra a
la fase 5 (music_tail, immediatament després del text-exit): itera els
DynamicHazards de la sala i els arma el comptador `spawn.duration_s`.

Així la cinemàtica queda:
  fade-in → text-enter → text-hold → text-exit
    └─> mòmies surten "del terra" (durada `spawn.duration_s` del YAML)
        └─> jugadors cauen del cel (delay player_spawn_delay_ms)

`respawnPlayersInstant` (salt debug a sala) també crida el trigger
perquè les mòmies no quedin congelades quan es curt-circuita la
cinemàtica.

Flag `hazards_spawn_triggered_` garanteix idempotència dins el cicle;
es reseteja a `enterRoomIntro` i al `setState(ROOM_INTRO)` defensiu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:56:14 +02:00
JailDesignerandClaude Opus 4.7 041447eaa4 feat(mummy): naix en SPAWN, llig spawn.duration_s del YAML
La mòmia ara entra a l'estat SPAWN al construir-se: visible al
pixel_rect_ inicial, sense moviment ni dany, esperant que algú extern
(GameScene) li armi el comptador via `triggerSpawnExit()`.

La durada (`spawn.duration_s` al YAML, default 0) controla quan passa
a ROAMING una vegada armat el comptador. Posat a 0.6s a mummy.yaml.

Aquest commit encara no enganxa el trigger: la mòmia es queda quieta
tota la partida — comportament visual intermedi, esperat. El commit
següent posarà el trigger a GameScene al final del text-exit de
ROOM_INTRO.

Tests de smoke i whip ajustats: les mòmies construïdes en stand-alone
necessiten `triggerSpawnExit()` + un update gran abans d'interactuar
amb `enterHurt`/contacto. Helper `exitMummySpawn` afegit a cada fitxer.

Guards defensius a `enterHurt` i `onPlayerContact`: encara que el filtre
central a EntityManager salta DynamicHazards en SPAWN, blindem la
crida directa per a futurs camins (whip propers, scripts...).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:49:23 +02:00
JailDesignerandClaude Opus 4.7 a6b2b02930 feat(hazard): afegeix estat SPAWN a DynamicHazard
DynamicHazard guanya un mecanisme de SPAWN amb dues fases: SPAWNING
(visible, sense moviment ni dany, comptador en pausa) i ACTIVE (lògica
normal). `enterSpawn(duration_s)` deixa l'entitat congelada esperant
indefinidament; `triggerSpawnExit()` arma el comptador i, en arribar a
zero, transita a ACTIVE.

Per defecte les noves DynamicHazard arrenquen en ACTIVE — backward-compat
amb subclasses existents (RollingRock) que encara no usen el mecanisme.

EntityManager::checkPlayerCollisions salta DynamicHazards en SPAWN: no
disparen onPlayerContact, així no fan dany ni triggers durant la fase.

Aquest commit només afegeix la infraestructura — cap subclasse l'usa
encara, comportament del joc inalterat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:43:38 +02:00
JailDesigner b7811180ba feat(game): spawn inicial de ROOM_INTRO sense immunitat 2026-05-11 07:53:56 +02:00
JailDesigner 2cbf1bb6e0 chore(version): bump 0.39.0 -> 0.40.0 2026-05-08 23:00:06 +02:00
JailDesigner 8890fa9e1f Merge branch 'feat/player-join-and-game-over' 2026-05-08 22:59:47 +02:00
JailDesigner 23f7cb03a6 chore(data): restaura lives:3 a sam i morcus despres de proves 2026-05-08 22:59:43 +02:00
JailDesigner 486a440668 feat(game): skip de countdown amb JUMP/ATTACK quan numero <= 7 2026-05-08 22:20:39 +02:00
JailDesigner a717411edd feat(audio): musica acelerada en CONTINUE?, game_over.ogg, tick del countdown 2026-05-08 22:06:44 +02:00
JailDesigner 5e862ceadc fix(game): HUD lives desde YAML, prompt P1 en single-P2, countdown 9..0 configurable 2026-05-08 21:14:33 +02:00
JailDesignerandClaude Opus 4.7 78e164fce4 feat(game): CONTINUE? + GAME OVER + fade-to-LOGO (Fase 2)
Sub-FSM nova `GameOverFsm` (CONTINUE_PROMPT → GAME_OVER → FADE_OUT →
DONE) que viu en paral·lel a la `RoomFsm`. Es dispara quan tots els
jugadors queden DISABLED durant PLAYING, mostra el rètol "Continuar?"
amb countdown reusant el panel del room text, i si ningú pulsa START
acaba en "Fi del Joc" + fade negre i sol·licita la transició a LOGO.
Pulsar START durant CONTINUE? cancel·la el countdown i fa join (revive)
del jugador. Després de GAME OVER, el HUD passa a "ESPERA / PER JUGAR"
i el START queda bloquejat fins a la transició d'escena.

Els helpers d'animació del room text (X i altura del panel) ara reben
phase + elapsed_ms per paràmetre per poder ser reaprofitats per les
dues sub-FSMs sense duplicar la lògica visual.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:30:37 +02:00
JailDesignerandClaude Opus 4.7 c13b11c501 feat(player): join in progress + DISABLED en agotar vidas (Fase 1)
Habilita la regla 1 de la nova FSM de joc: durant PLAYING, qualsevol
slot DISABLED pot pulsar START per entrar a partida amb spawn aeri al
seu costat de la porta i vides reseteades. Quan un jugador esgota les
vides, `respawnFromDying` el deixa a DISABLED i publica `player_disabled`
al bus; GameScene i HUD reaccionen, deixant el slot disponible per a un
nou join. Afegeix prompts al HUD ("PULSA START / PER JUGAR" o
"ESPERA / PER JUGAR") segons l'estat de la RoomFsm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:04:33 +02:00
JailDesigner 8921b9fd0c chore(version): bump 0.38.0 -> 0.39.0 2026-05-08 13:43:44 +02:00
JailDesigner 1d9fea28fe Merge branch 'feat/gameplay-tweaks'
Tweaks de jugabilitat persistents (gameplay.yaml a la carpeta de
sistema) amb tres flags i submenú "Jugabilitat" al ServiceMenu:
látec travessa sarcs, foc amic, col·lisió entre jugadors.
2026-05-08 13:43:24 +02:00
JailDesigner d0d5cb0e66 fix(player): players_collide també anul·la resolver i landing overlap 2026-05-08 13:40:30 +02:00
JailDesigner e25ccd02fd feat(menu): submenú "Jugabilitat" al menú de servei amb tres toggles
Nou apartat al ServiceMenu (entre Controls i Sistema) amb tres toggles
ON/OFF connectats a GameplayConfig: látec travessa sarcs, foc amic,
col·lisió entre jugadors. Cada canvi crida save() immediatament, així
l'estat persisteix entre sessions sense necessitat de "aplicar".

Locales ca/en: claus `tweaks` + tres `menu.items.*` corresponents.
2026-05-08 13:29:04 +02:00
JailDesigner 3635cebf25 feat(player): cabla flag players.collide_with_each_other
A `collidesWithWorld` saltem la branca actor↔actor quan el flag està
desactivat: els jugadors travessen els altres lliurement però segueixen
topant amb murs i sarcs. A `tryPushSnapBlockers` retornem early per la
mateixa raó: si no hi ha col·lisió entre jugadors, no hi ha ningú a
empentar quan el snap del corredor s'aplica.
2026-05-08 13:26:47 +02:00
JailDesigner cfbcdc236b feat(whip): cabla flag combat.friendly_fire a hitPlayerIfApplicable
Quan el flag està desactivat, el látigo no fa dany a altres jugadors
(l'owner ja quedava exclòs per la guarda existent). Default true:
manté el comportament actual.

Sense canvis als tests del whip — friendly fire ja no es cobreix
allà perquè els stubs de Player no porten estat real (vegeu el comentari
del top de test_whip_attack.cpp).
2026-05-08 13:24:33 +02:00
JailDesigner 7871d3930e feat(whip): cabla flag whip.damage_pierces_walls a computeDamageRect
Quan el flag està actiu (default), l'iteració píxel a píxel ignora
`Map::collides` i només respecta `insidePlayfield`. El mur extern,
doncs, sempre retalla — fora del playfield no hi ha res a colpejar.

Els tests existents passen amb la nova lògica perquè els seus setups
posen el damage_rect a corredors lliures (sense sarcs a la trajectòria),
i el test W2 segueix verificant el retall del playfield.
2026-05-08 13:22:52 +02:00
JailDesigner 932b457725 feat(config): afegeix GameplayConfig persistent amb versionatge defensiu
Singleton paral·lel a UiConfig però escrivible: viu a la carpeta de
sistema (gameplay.yaml). Defaults compilats: friendly_fire,
players_collide i whip_damage_pierces_walls a true. Davant qualsevol
irregularitat (fitxer absent/buit, parse error, version absent o
desconeguda, clau requerida mal tipada o absent), reseteja a defaults
i reescriu el fitxer en lloc de preservar estat parcial.

Cablejat al Director::start després de Options::loadFromFile. Encara
sense consumidors — els flags es cablejaran a WhipAttack i al sistema
de col·lisió de jugadors en commits posteriors.
2026-05-08 13:17:28 +02:00
JailDesigner e9f99a0542 fix(mummy): evita soft-lock per drift sub-pixel a HURT vora del mur 2026-05-08 12:07:18 +02:00
JailDesigner 0d58da536c chore(version): bump 0.37.0 -> 0.38.0 2026-05-08 11:16:22 +02:00
JailDesigner 583acb9053 fix(config): rutes per plataforma a createSystemFolder 2026-05-08 11:15:15 +02:00
JailDesigner babb111b6f chore(version): bump 0.36.0 -> 0.37.0 2026-05-08 09:59:23 +02:00
JailDesignerandClaude Opus 4.7 866e133aa9 Merge branch 'feat/logger'
Sistema de log unificat (Log:: wrapper sobre SDL_Log) substituint
els 176 std::cout/std::cerr dispersos per source/. API basada en
std::format, filtre runtime via AEE_LOG (sintaxi *:info,Tag:debug),
i marcadors literals via Log::raw().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:59:16 +02:00
JailDesignerandClaude Opus 4.7 170521aba6 feat(log): afegeix Log::raw() per a marcadors sense prefix
Recupera el comportament històric dels marcadors "Game start" i "Bye!"
que han de sortir pelats (no com a "App: Game start"). Log::raw(level, msg)
és una variant prima sobre SDL_LogMessage que no anteposa cap tag i cau a
SDL_LOG_CATEGORY_APPLICATION (filtrable per AEE_LOG global).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:54:13 +02:00
JailDesignerandClaude Opus 4.7 c8882a3f6e refactor(log): migra els subsistemes restants i elimina iostream
Acaba la migració dels std::cout/cerr a Log::*:

- core/locale, core/ui/ui_config, core/rendering/{screen,text,gif,
  surface, sprite/animated_sprite, sdl3gpu/*}.
- game/{options, debug_config, editor/editor_config, levels/level_manager,
  map/{map,tilemap,paint_layer}, entities/entity_manager_loadlevel}.

Severitats coherents: warns per a configs absents amb fallback,
errors per a YAML malformat o E/S fallida, infos per a estats.

Resultat:
- 0 occurrences de std::cout/std::cerr a source/.
- Cap #include <iostream> orfà (eliminat de surface.cpp).
- Output funcional idèntic al històric; AEE_LOG filtra (ex.
  `AEE_LOG=warn` només mostra warnings, `AEE_LOG=*:warn,Tag:info`
  combina globals i overrides).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:34:24 +02:00
JailDesignerandClaude Opus 4.7 5fce52e0ad refactor(log): migra game/scenes i game/ui a Log::
- logo_scene, title_scene, intro_scene, room_fsm: missatges de
  cicle d'escena com a Log::info amb tag "scene" o "RoomFsm".
  Eliminats els TODO obsolets sobre el logger pendent.
- game_scene (16 logs): events del bus (LEVEL COMPLETE, PLAYER HIT,
  PLAYER KILLED) com a info; transicions de ROOM com a info; warns
  als casos d'àudio absent o música no arrencada.
- hud (3 logs): init i events com a info.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:24:11 +02:00
JailDesignerandClaude Opus 4.7 0b708f1515 refactor(log): migra core/input i core/audio a Log::
- gamepad_manager.cpp (12 logs): connect/disconnect com a info,
  errors d'open/init com a error, warning per BD no carregada.
- key_config.cpp (5 logs): error per arxius malformats, info
  per al recompte de bindings carregats.
- gamepad_bindings.cpp (4 logs): error/info anàleg.
- audio_effects.cpp (2 errors de format).
- sound_effects_config.cpp (3 logs): warn quan no hi ha presets,
  error de parse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:17:12 +02:00
JailDesignerandClaude Opus 4.7 6e99fd0a8b refactor(log): migra core/resources i core/system a Log::
- resource_pack.cpp i resource_cache.cpp: tots els std::cerr passen a
  Log::error.
- director.cpp: std::cout/cerr/fprintf → Log::info / error / critical
  segons severitat.
- printWithDots (utils.cpp): ara és un wrapper sobre Log::info que
  manté l'amplada total (~50 chars) reproduint l'alineació de punts.
  Els callers pasen el tag sense " : " (ja l'afegeix el logger).
- pack_resources: afegit log.cpp i SDL3 al target perquè
  resource_pack.cpp ara depèn de Log::.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:10:03 +02:00
JailDesignerandClaude Opus 4.7 1243b457be feat(log): introdueix Log:: wrapper sobre SDL_Log
Logger lleuger a source/utils/log.{hpp,cpp} amb API basada en std::format
i filtre runtime via la variable d'entorn AEE_LOG (sintaxi
*:info,Resource::Helper:warn,scene:debug). Substitueix gradualment els
std::cout/cerr dispersos.

- Categories CUSTOM registrades sota demanda; per defecte INFO.
- Categories SDL natives no es toquen tret que l'usuari posi *:level.
- Migra resource_helper.cpp i els marcadors d'inici/fi (main.cpp) com a
  testimoni; la resta de subsistemes es migra en commits següents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:01:09 +02:00
JailDesigner c910a16834 tune(whip): puja swing a 320ms (80ms × 4 frames) 2026-05-07 20:36:15 +02:00
JailDesigner 0ec1a2548a feat(mummy): so mummy_hurt en colpejada de fuet 2026-05-07 20:25:23 +02:00
JailDesigner 4ea8ab7b6d art(whip): actualitza els 4 GIFs direccionals del fuet 2026-05-07 20:12:57 +02:00
JailDesigner 05465b791e refactor(data): reorganitza data/ i centralitza paths a defaults.hpp 2026-05-07 19:28:28 +02:00
JailDesigner 9949096fba chore(rename): aee_2026 -> aee_arcade 2026-05-07 18:39:02 +02:00
JailDesigner ea5762265e chore(version): bump 0.35.0 -> 0.36.0 2026-05-07 16:46:11 +02:00
JailDesigner 78732042cb merge: feature/logo-timeline-redesign 2026-05-07 16:45:34 +02:00
JailDesigner f10671d9c0 feat(logo): timeline lineal de 7 fases amb glitch tail i durades al YAML 2026-05-07 16:44:34 +02:00
JailDesigner 7d5a4cee8e chore(version): bump 0.34.0 -> 0.35.0 2026-05-07 14:07:44 +02:00
JailDesigner d560623e00 merge: feature/logo-scanline-reveal 2026-05-07 14:07:26 +02:00
JailDesigner 3f88dfaaee feat(logo): afegit data/sounds/glitch.wav 2026-05-07 14:07:21 +02:00
JailDesigner e1bd75e00d feat(logo): so glitch.wav sincronitzat amb el scanline reveal 2026-05-07 14:04:48 +02:00
JailDesigner e32cff9dac feat(logo): scanline reveal CRT amb glitch, bursts VHS i pulse de banda 2026-05-07 13:52:57 +02:00
JailDesigner 243ced7498 feat(title): seqüència d'intro temporitzada amb slide-in del peu 2026-05-07 13:08:13 +02:00
JailDesigner fceeb11cdd feat(title): cosmètics i posicions a YAML, font Tiny5 al copyright 2026-05-07 11:51:00 +02:00
JailDesigner 233fcb8226 feat(title): game_logo.gif al lloc del text i font PixeloidSans-Bold per al copyright 2026-05-06 22:35:27 +02:00
JailDesigner 8bc45ccedc feat(logo): FSM amb fade-in/middle/fade-out (500/5000/500 ms) i música del títol a 2,5 s del centre 2026-05-06 20:01:31 +02:00
JailDesigner f7ab6a641a feat(logo,menu): reiniciar des de servei → LOGO amb reset (atura música) i logo_jailgames.gif centrat 2026-05-06 19:54:30 +02:00
193 changed files with 7814 additions and 2148 deletions

No files matched your search

+2 -2
View File
@@ -8,8 +8,8 @@ dist/
*.dylib *.dylib
*.dll *.dll
*.exe *.exe
aee_2026 aee_arcade
_aee_2026 _aee_arcade
.vscode/ .vscode/
.idea/ .idea/
.cache/ .cache/
+18 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(aee_2026 VERSION 0.1.0 LANGUAGES CXX) project(aee_arcade VERSION 0.1.0 LANGUAGES CXX)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE) set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
@@ -52,6 +52,7 @@ set(ENTITY_SOURCES_BASE
source/game/entities/concrete/floor_trap.cpp source/game/entities/concrete/floor_trap.cpp
source/game/entities/concrete/hole.cpp source/game/entities/concrete/hole.cpp
source/game/entities/concrete/dust.cpp source/game/entities/concrete/dust.cpp
source/game/entities/concrete/solid_props.cpp
) )
# Entitats que criden directament símbols de Player (com WhipAttack via # Entitats que criden directament símbols de Player (com WhipAttack via
@@ -80,6 +81,7 @@ set(APP_SOURCES
source/core/input/mouse.cpp source/core/input/mouse.cpp
source/core/rendering/screen.cpp source/core/rendering/screen.cpp
source/core/rendering/canvas.cpp source/core/rendering/canvas.cpp
source/core/rendering/lighting.cpp
source/core/rendering/notifier.cpp source/core/rendering/notifier.cpp
source/core/rendering/render_info.cpp source/core/rendering/render_info.cpp
source/core/rendering/gif.cpp source/core/rendering/gif.cpp
@@ -93,6 +95,8 @@ set(APP_SOURCES
source/core/rendering/sprite/animated_sprite.cpp source/core/rendering/sprite/animated_sprite.cpp
source/core/rendering/sprite/animated_sprite_parser.cpp source/core/rendering/sprite/animated_sprite_parser.cpp
source/core/rendering/effects/heat_haze.cpp source/core/rendering/effects/heat_haze.cpp
source/core/rendering/effects/scan_reveal.cpp
source/core/rendering/effects/shadow.cpp
source/core/rendering/transitions/mosaic.cpp source/core/rendering/transitions/mosaic.cpp
source/core/rendering/transitions/fade_to_black.cpp source/core/rendering/transitions/fade_to_black.cpp
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
@@ -103,18 +107,22 @@ set(APP_SOURCES
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
source/core/resources/palette_loader.cpp source/core/resources/palette_loader.cpp
source/utils/easing.cpp source/utils/easing.cpp
source/utils/log.cpp
source/utils/utils.cpp source/utils/utils.cpp
source/game/debug_config.cpp source/game/debug_config.cpp
source/game/gameplay_config.cpp
source/game/options.cpp source/game/options.cpp
source/game/options_apply.cpp source/game/options_apply.cpp
source/game/ui/service_menu.cpp source/game/ui/service_menu.cpp
source/game/ui/hud.cpp source/game/ui/hud.cpp
source/game/scenes/placeholder_input.cpp source/game/scenes/placeholder_input.cpp
source/game/scenes/start_input.cpp
source/game/scenes/logo_scene.cpp source/game/scenes/logo_scene.cpp
source/game/scenes/title_scene.cpp source/game/scenes/title_scene.cpp
source/game/scenes/intro_scene.cpp source/game/scenes/intro_scene.cpp
source/game/scenes/game_scene.cpp source/game/scenes/game_scene.cpp
source/game/scenes/room_fsm.cpp source/game/scenes/room_fsm.cpp
source/game/scenes/game_over_fsm.cpp
source/game/entities/player.cpp source/game/entities/player.cpp
source/game/map/map.cpp source/game/map/map.cpp
source/game/map/tilemap.cpp source/game/map/tilemap.cpp
@@ -217,8 +225,10 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
add_executable(pack_resources EXCLUDE_FROM_ALL add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
source/utils/log.cpp
) )
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source") target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_link_libraries(pack_resources PRIVATE SDL3::SDL3)
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack --- # --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
@@ -360,9 +370,11 @@ if(BUILD_TESTS)
add_executable(test_entities_smoke add_executable(test_entities_smoke
tests/test_entities_smoke.cpp tests/test_entities_smoke.cpp
tests/hole_player_stubs.cpp tests/hole_player_stubs.cpp
tests/audio_cache_stubs.cpp
source/core/resources/resource_helper.cpp source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
source/game/map/map.cpp source/game/map/map.cpp
source/utils/log.cpp
${ENTITY_SOURCES_BASE} ${ENTITY_SOURCES_BASE}
) )
target_include_directories(test_entities_smoke PRIVATE target_include_directories(test_entities_smoke PRIVATE
@@ -388,9 +400,12 @@ if(BUILD_TESTS)
tests/test_whip_attack.cpp tests/test_whip_attack.cpp
tests/whip_attack_player_stubs.cpp tests/whip_attack_player_stubs.cpp
tests/hole_player_stubs.cpp tests/hole_player_stubs.cpp
tests/audio_cache_stubs.cpp
source/core/resources/resource_helper.cpp source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
source/game/gameplay_config.cpp
source/game/map/map.cpp source/game/map/map.cpp
source/utils/log.cpp
${ENTITY_SOURCES_BASE} ${ENTITY_SOURCES_BASE}
source/game/entities/concrete/whip_attack.cpp source/game/entities/concrete/whip_attack.cpp
) )
@@ -415,6 +430,7 @@ if(BUILD_TESTS)
source/core/input/key_config.cpp source/core/input/key_config.cpp
source/core/resources/resource_helper.cpp source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
source/utils/log.cpp
) )
target_include_directories(test_scene_flow PRIVATE target_include_directories(test_scene_flow PRIVATE
"${CMAKE_SOURCE_DIR}/source" "${CMAKE_SOURCE_DIR}/source"
@@ -454,6 +470,7 @@ if(BUILD_TESTS)
source/core/resources/resource_helper.cpp source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
source/utils/easing.cpp source/utils/easing.cpp
source/utils/log.cpp
) )
target_include_directories(test_game_fsm PRIVATE target_include_directories(test_game_fsm PRIVATE
"${CMAKE_SOURCE_DIR}/source" "${CMAKE_SOURCE_DIR}/source"
+8 -2
View File
@@ -1,4 +1,4 @@
PROJECT := aee_2026 PROJECT := aee_arcade
BUILDDIR := build BUILDDIR := build
# Detecció de plataforma. En Windows CMake defaulteja a "NMake Makefiles" # Detecció de plataforma. En Windows CMake defaulteja a "NMake Makefiles"
@@ -293,8 +293,14 @@ rebuild: clean all
# Empaqueta data/ a $(BUILDDIR)/resources.pack (format AE26). Força un rebuild # Empaqueta data/ a $(BUILDDIR)/resources.pack (format AE26). Força un rebuild
# del pack encara que res no hagi canviat dins data/. L'empaquetat també es fa # del pack encara que res no hagi canviat dins data/. L'empaquetat també es fa
# automàticament a cada `make`/`make debug` via el target CMake `resource_pack`. # automàticament a cada `make`/`make debug` via el target CMake `resource_pack`.
#
# NO passem `-DCMAKE_BUILD_TYPE`: així respectem el tipus ja configurat al
# `CMakeCache.txt` (Debug si vens de `make debug`, Release si vens de
# `make release`). Si forcéssim Release ací, el següent build de debug
# invalidaria la cache i recompilaria-ho tot. El binari `pack_resources`
# és una utilitat petita on el build type és irrellevant.
pack: pack:
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS) @cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) $(CMAKE_DEFS)
@cmake --build $(BUILDDIR) --target pack_resources @cmake --build $(BUILDDIR) --target pack_resources
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack @./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.
Binary file not shown.
File renamed without changes.
File renamed without changes.
@@ -8,6 +8,6 @@ picker:
# Colors ARGB en hex. Accepta enter (0xFF2244FF) o cadena ("#FF2244FF"). # Colors ARGB en hex. Accepta enter (0xFF2244FF) o cadena ("#FF2244FF").
colors: colors:
border: "0xFF2244FF" # vora 1 px que envolta canvas i picker border: "0xFF6A86C8" # vora 1 px que envolta canvas i picker (blau suau)
picker_bg: "0xFF101824" # fons del picker picker_bg: "0xFFC8C0D8" # fons del picker (lavanda clara)
picker_hover: "0xFFFFFFFF" # vora 1 px del tile sota el cursor picker_hover: "0xFFFFFFFF" # vora 1 px del tile sota el cursor
@@ -1,4 +1,4 @@
# AEE 2026 — Tecles d'UI (Global Inputs) # AEE ARCADE — Tecles d'UI (Global Inputs)
# #
# Mapeig id → codi SDL (el que reconeix SDL_GetScancodeFromName). Les tecles # Mapeig id → codi SDL (el que reconeix SDL_GetScancodeFromName). Les tecles
# de moviment del jugador viuen a part (Defaults::KeysGame); ací només hi ha # de moviment del jugador viuen a part (Defaults::KeysGame); ací només hi ha
@@ -35,9 +35,6 @@ keys:
- id: toggle_render_info - id: toggle_render_info
code: "F10" code: "F10"
desc: "Cicle de l'overlay de debug: apagat / dalt / baix" desc: "Cicle de l'overlay de debug: apagat / dalt / baix"
- id: pause
code: "F11"
desc: "Pausa el joc (o botó START del mando)"
- id: menu_toggle - id: menu_toggle
code: "F12" code: "F12"
desc: "Obri / tanca el menú de servei" desc: "Obri / tanca el menú de servei"
+56
View File
@@ -0,0 +1,56 @@
# Configuració global del sistema d'il·luminació.
#
# Cada llum emissora (player, torxa, mòmia, ...) té un únic paràmetre
# propi: el radi en píxels. La forma de la llum dins d'aquest radi
# (els tres anells del gradient) viu en aquest fitxer perquè és una
# decisió de "feel" comuna a totes les entitats — així pots tocar la
# corba aquí sense haver de modificar 10 YAMLs.
#
# Forma del gradient (centre → vora):
# 0..inner_ratio del radi → inner_intensity (zona totalment il·luminada)
# inner_ratio..mid_ratio → mid_intensity (anell mig)
# mid_ratio..1.0 → outer_intensity (anell exterior)
# fora del radi → light_level de la sala
#
# Hi ha dues corbes paral·leles, una per mode, perquè la mateixa escala
# 0..1 vol dir coses molt diferents:
# `gradient` → subtractive: 1.0 = "no fosqueix" el píxel
# (l'emissor "trenca" una sala fosca).
# `gradient_additive` → additive: 1.0 = "afegeix el color sencer"
# (satura). Valors molt més baixos donen un
# halo cromàtic subtil sobre una sala normal.
gradient:
inner_ratio: 0.5
mid_ratio: 0.85
inner_intensity: 1.0
mid_intensity: 0.8
outer_intensity: 0.6
# Corba del mode additive. Valors petits a propòsit: només volem un
# toc de tint cromàtic prop de l'emissor — fora del nucli, la sala
# original ha de quedar pràcticament intacta. Si pugeu massa l'inner,
# els colors de l'emissor "inunden" la sala (additiu satura ràpid).
gradient_additive:
inner_ratio: 0.5
mid_ratio: 0.85
inner_intensity: 0.15
mid_intensity: 0.10
outer_intensity: 0.05
# Modulació del radi quan una entitat declara `flicker: true` al seu
# bloc `light:`. El radi efectiu per frame és:
# r_eff = r_base + (r_amp1·r_base)·sin(2π·f1·t + φ1) + (r_amp2·r_base)·sin(2π·f2·t + φ2)
# on (φ1, φ2) es deriven de la seed de l'emissor per a que cada
# antorxa balancegi en una fase diferent (sense estat per emissor).
# Les amplituds són **ràtios** sobre el radi base: així el feel del
# flicker és independent del radi (una torxa de 24 px oscil·la el
# mateix % que una de 40 px). Combinant dues sinusoides amb freqüències
# incommensurables s'obté un patró "fluid" que el ull llegeix com a
# foc; pujar `noise_*` aporta més inquietud, baixar-ho fa la flama
# més serena. Aquests són els defaults globals; cada tipus d'entitat
# (o cada spawn / sala) pot sobreescriure'ls al seu `light.flicker_params:`.
flicker:
wave_amplitude_ratio: 0.05 # 5% del radi (per a radi 40 → ±2 px, feel actual)
wave_frequency_hz: 3.0
noise_amplitude_ratio: 0.025 # 2.5% (per a radi 40 → ±1 px, feel actual)
noise_frequency_hz: 7.0
File renamed without changes.
+128
View File
@@ -0,0 +1,128 @@
# Tema de la UI: índexs de paleta, timings, fonts i flags visuals. Tota
# clau absent cau al valor compilat a Defaults::UI (veure defaults.hpp).
colors:
render_info_text: 58
render_info_shadow: 16
canvas_bg: 72
notification_text: 232
notification_bg: 64
exit_text: 71
exit_bg: 2
menu_bg: 2
menu_border: 232
menu_title: 237
menu_label: 58
menu_value: 232
menu_cursor: 71
menu_disabled: 16
hud_bg: 9
hud_flash: 63
# 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. Comparteix la família del CENTER del HUD
# (or sobre marró) perquè "Piràmide N / Sala N" sigui sempre daurat
# arreu del joc.
room_text: 55 # or brillant (#FFDA00, = hud_center.value)
room_text_outline: 10 # marró cremat (#482400, = hud_center.shadow)
timings:
notification_ms: 4000
exit_ms: 4000
fonts:
hud:
name: "8bithud"
gif: "data/fonts/8bithud.gif"
fnt: "data/fonts/8bithud.fnt"
kerning: 1
placeholder:
name: "ari-w9500-condensed-display"
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"
fnt: "data/fonts/ari-w9500-display.fnt"
kerning: 0
menu_items:
name: "ari-w9500-condensed-display"
gif: "data/fonts/ari-w9500-condensed-display.gif"
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
kerning: 0
# Copyright de la TITLE: pixel-art estricte (Tiny5-Regular size 5,
# caixa 3×6 px) generat amb font-gen. Diferenciada de la `placeholder`
# que pinta el "press start" — així la jerarquia tipogràfica del peu
# queda neta.
copyright:
name: "Tiny5-Regular"
gif: "data/fonts/Tiny5-Regular.gif"
fnt: "data/fonts/Tiny5-Regular.fnt"
kerning: 1
hud:
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
# Parpadeig dels prompts inactius ("Polsa start..." / "Espera..."). Un
# únic temporitzador global al Hud sincronitza P1 i P2 i no es reinicia
# quan el text canvia entre CAN_JOIN i WAIT. cycle<=0 o ratio>=1 →
# sempre visible (disable trivial).
blink_cycle_ms: 1000 # durada del cicle complet
blink_visible_ratio: 0.8 # proporció del cicle visible (0..1)
# Famílies de color del HUD per bloc. Cada bloc té `label` (to apagat
# per etiquetes), `value` (to viu per a la xifra) i `shadow` (to fosc
# del mateix matís — efecte embossing 1 px a baix-dreta). Si manca un
# camp, cau al default compilat a `Defaults::UI`. El `colors.hud_flash`
# és comú a totes tres famílies.
hud_p1:
label: 7 # vermell pur
value: 183 # crema-préssec
shadow: 2 # vermell fosc
hud_center:
label: 29 # ambre cremat
value: 55 # or brillant
shadow: 10 # marró cremat
hud_p2:
label: 248 # cian pur
value: 253 # blau pàl·lid
shadow: 64 # blau marí
# Geometria del popup superior (Notifier). En píxels del canvas, llevat de
# slide_ms (durada en ms de l'animació d'entrada i d'eixida; constant
# independentment de la mida de la caixa). El padding vertical és asimètric a
# propòsit: padding_top > padding_bottom puja una mica el text dins la caixa.
# Corbes disponibles: linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
notifier:
padding_h: 6
padding_top: 4
padding_bottom: 3
line_gap: 1
margin_top: 3
border_px: 1
slide_ms: 300
slide_in_curve: outCubic
slide_out_curve: inCubic
# Sons del menú de servei. Els fitxers viuen a data/audio/sounds/. SELECT
# acompanya navegació (amunt/avall) i back; ACCEPT acompanya modificació
# d'opció (esquerra/dreta/retorn) o entrada a un submenú.
sounds:
menu_select: "menu_select.wav"
menu_accept: "menu_accept.wav"
# Disparat quan un jugador prem START a la TITLE (primera pulsació o
# la del segon jugador dins de la finestra de STARTING).
player_start: "player_start.wav"
# Soroll/scratch sincronitzat amb la fase SCAN de LogoScene (scanline
# reveal). Es dispara al constructor de l'escena.
logo_glitch: "glitch.wav"
Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

+22
View File
@@ -0,0 +1,22 @@
name: column
type: decoration
editable: true
# Sub-picker de frames a l'editor: 6 variants (atlas 2×3 de sub-sprites 16×32).
editable_variants: true
sprite:
path: column.gif
size: [16, 32]
# Ancoratge "feet": el tile [col, row] del spawn és la base de la columna.
# El sprite de 16×32 sobresurt 16 px cap amunt sense afectar col·lisió ni
# z-order (la caixa lògica és el tile inferior).
anchor: feet
tile_size: [1, 1]
collision:
size: [16, 16]
# Mur infranquejable al salt: el jugador no pot passar pel damunt.
height: 999
flags:
collides: true
interactable: false
animations:
default: { frames: [0], frame_ms: 1000, loop: false }
+3 -3
View File
@@ -21,9 +21,9 @@ flags:
collides: true collides: true
interactable: true interactable: true
animations: animations:
closed: { frames: [1], fps: 1, loop: true } closed: { frames: [1], frame_ms: 1000, loop: true }
opening: { frames: [0], fps: 6, loop: false } opening: { frames: [0], frame_ms: 167, loop: false }
open: { frames: [0], fps: 1, loop: true } open: { frames: [0], frame_ms: 1000, loop: true }
behaviour: behaviour:
states: [CLOSED, OPENING, OPEN] states: [CLOSED, OPENING, OPEN]
initial_state: CLOSED initial_state: CLOSED
Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 B

After

Width:  |  Height:  |  Size: 303 B

+3 -3
View File
@@ -16,7 +16,7 @@ flags:
# apropiada via `setVariant` abans del primer tick. # apropiada via `setVariant` abans del primer tick.
# jump: frames 0..6 (7 frames) → reproduït al despegue. # jump: frames 0..6 (7 frames) → reproduït al despegue.
# land: frames 7..12 (6 frames) → reproduït a l'aterratge. # land: frames 7..12 (6 frames) → reproduït a l'aterratge.
# fps: 10 = 100 ms per frame. # frame_ms: 67 ≈ 15 fps (1000 / 67 ≈ 14.9 fps).
animations: animations:
jump: { frames: [0, 1, 2, 3, 4, 5, 6], fps: 15, loop: false } jump: { frames: [0, 1, 2, 3, 4, 5, 6], frame_ms: 67, loop: false }
land: { frames: [7, 8, 9, 10, 11, 12], fps: 15, loop: false } land: { frames: [7, 8, 9, 10, 11, 12], frame_ms: 67, loop: false }
+2 -2
View File
@@ -13,8 +13,8 @@ flags:
collides: false collides: false
interactable: false interactable: false
animations: animations:
inactive: { frames: [0], fps: 1, loop: true } inactive: { frames: [0], frame_ms: 1000, loop: true }
active: { frames: [0], fps: 6, loop: true } active: { frames: [0], frame_ms: 167, loop: true }
cycle: cycle:
active_duration: 0.3 active_duration: 0.3
inactive_duration: 2.0 inactive_duration: 2.0
+1 -1
View File
@@ -22,6 +22,6 @@ flags:
# lectura genèrica de flags. # lectura genèrica de flags.
triggers_on_overlap: true triggers_on_overlap: true
animations: animations:
idle: { frames: [0], fps: 1, loop: true } idle: { frames: [0], frame_ms: 1000, loop: true }
stats: stats:
damage: 0 damage: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

+28 -4
View File
@@ -32,14 +32,28 @@ flags:
# no decideix flip per direcció (només selecciona quina animació mostrar). # no decideix flip per direcció (només selecciona quina animació mostrar).
# La momia no té variants `stand_*` perquè mai no para. # La momia no té variants `stand_*` perquè mai no para.
animations: animations:
walk_south: { frames: [0, 1, 2, 3], fps: 8, loop: true } walk_south: { frames: [0, 1, 2, 3], frame_ms: 125, loop: true }
walk_north: { frames: [4, 5, 6, 7], fps: 8, loop: true } walk_north: { frames: [4, 5, 6, 7], frame_ms: 125, loop: true }
walk_east: { frames: [8, 9, 10, 11], fps: 8, loop: true } walk_east: { frames: [8, 9, 10, 11], frame_ms: 125, loop: true }
walk_west: { frames: [8, 9, 10, 11], fps: 8, loop: true, flip_h: true } walk_west: { frames: [8, 9, 10, 11], frame_ms: 125, loop: true, flip_h: true }
# Alçament: els 15 primers frames passen ràpid (125 ms cadascun) i el
# darrer aguanta 1 s perquè el jugador pugui veure la mòmia ja alçada
# abans que comenci a deambular. `frame_ms` accepta una llista per-frame
# que mapeja 1:1 contra `frames`.
raise:
frames: [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]
frame_ms: [125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 1000]
loop: false
ai: ai:
type: corridor_wanderer type: corridor_wanderer
direction_choice: random direction_choice: random
initial_direction: east initial_direction: east
# Estat SPAWN: la mòmia s'instancia al pixel_rect_ inicial quan
# GameScene crida `spawnPendingActors()` (subfase text-exit de
# ROOM_INTRO, fase 4) amb l'animació `raise` activa. La duració del
# SPAWN la marca la pròpia animació (no-loop): al `completed()`,
# transita a ROAMING i comença a deambular.
stats: stats:
speed: 30 speed: 30
damage: 1 damage: 1
@@ -58,3 +72,13 @@ hurt:
duration_ms: 600 duration_ms: 600
speed: 60 speed: 60
whippable: true whippable: true
# So opcional: basename del fitxer dins `data/audio/sounds/`. Es dispara
# un sol cop per fuetada quan la mòmia entra a HURT. Si manca, no sona.
sound: mummy_hurt.wav
# Halo tènue al voltant de la mòmia. Radi més curt que el del jugador
# perquè el rastre lumínic delati la seva posició sense "il·luminar
# la sala" — vol ser una pista visual a la foscor.
light:
radius: 24
flicker: true
File renamed without changes.
@@ -30,20 +30,20 @@ animations:
stun_east: { frames: [18] } stun_east: { frames: [18] }
stun_west: { frames: [18], flip_h: true } stun_west: { frames: [18], flip_h: true }
walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 } walk_south: { frames: [1, 2, 3, 0], frame_ms: 100 }
walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 } walk_north: { frames: [5, 6, 7, 4], frame_ms: 100 }
walk_east: { frames: [8, 9, 10, 11], frame_ms: 100 } walk_east: { frames: [9, 10, 11, 8], frame_ms: 100 }
walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true } walk_west: { frames: [9, 10, 11, 8], frame_ms: 100, flip_h: true }
hurt_south: { 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_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
hurt_east: { frames: [8, 9, 10, 11], 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_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 } attack_south: { frames: [20, 21, 22, 23], frame_ms: 80 }
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 } attack_north: { frames: [24, 25, 26, 27], frame_ms: 80 }
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 } attack_east: { frames: [28, 29, 30, 31], frame_ms: 80 }
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true } attack_west: { frames: [28, 29, 30, 31], frame_ms: 80, flip_h: true }
recover_south: { frames: [23] } recover_south: { frames: [23] }
recover_north: { frames: [27] } recover_north: { frames: [27] }
@@ -97,13 +97,10 @@ turn:
south_to_north: west south_to_north: west
# Salt vertical (px de z, no toca la posició al món). # 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: jump:
impulse: 200 impulse: 200
gravity: 600 gravity: 600
landing_stun_ms: 400 landing_stun_ms: 400
shadow_y_offset: 3
sound: jump.wav sound: jump.wav
land_sound: land.wav land_sound: land.wav
@@ -119,7 +116,7 @@ hurt:
# recuperació en què la hitbox ja no fa dany. # recuperació en què la hitbox ja no fa dany.
whip: whip:
range_tiles: 3 range_tiles: 3
swing_ms: 200 swing_ms: 320
cooldown_ms: 200 cooldown_ms: 200
damage: 1 damage: 1
sound: whip.wav sound: whip.wav
@@ -136,10 +133,18 @@ death:
# poden ferir ni matar el jugador (els forats sí que el maten — la # poden ferir ni matar el jugador (els forats sí que el maten — la
# caiguda no és dany convencional). Visualment es manifesta amb un # caiguda no és dany convencional). Visualment es manifesta amb un
# parpadeig que alterna la paleta normal del sprite amb la paleta # parpadeig que alterna la paleta normal del sprite amb la paleta
# `palette` (basename de `data/player/<name>.pal`) cada # `palette` (basename de `data/entities/player/<name>.pal`) cada
# `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la # `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la
# mecànica queda desactivada i el comportament és l'antic. # mecànica queda desactivada i el comportament és l'antic.
immunity: immunity:
duration_ms: 3000 duration_ms: 3000
blink_period_ms: 100 blink_period_ms: 100
palette: immune palette: morcus_wash
# Llum emesa pel jugador. `radius` és el radi del cercle de llum
# en píxels del canvas; el gradient (3 anells: 1.0 / 0.9 / 0.8) viu
# a data/config/lighting.yaml i és compartit per tots els emissors.
# Omet el bloc per desactivar la llum d'aquest personatge.
light:
radius: 64
flicker: true
+19
View File
@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0 255
150 133 88 255
253 233 185 255
237 215 165 255
214 193 145 255
255 252 208 255
252 234 189 255
239 222 176 255
175 190 128 255
173 175 115 255
159 159 104 255
246 211 157 255
229 192 139 255
213 177 127 255
243 226 184 255
0 0 0 255
+19
View File
@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0 255
102 118 132 255
226 238 247 255
206 216 222 255
179 190 198 255
245 255 255 255
223 239 253 255
208 224 238 255
124 190 178 255
125 170 162 255
109 151 150 255
221 210 210 255
200 187 189 255
181 169 175 255
213 229 247 255
0 0 0 255
+19
View File
@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0 255
190 190 190 255
255 255 255 255
255 248 239 255
239 233 226 255
255 255 255 255
255 255 255 255
254 254 254 255
184 244 213 255
193 229 202 255
184 216 198 255
255 242 225 255
255 228 213 255
251 216 207 255
255 255 255 255
0 0 0 255
File renamed without changes.
@@ -30,20 +30,20 @@ animations:
stun_east: { frames: [18] } stun_east: { frames: [18] }
stun_west: { frames: [18], flip_h: true } stun_west: { frames: [18], flip_h: true }
walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 } walk_south: { frames: [1, 2, 3, 0], frame_ms: 100 }
walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 } walk_north: { frames: [5, 6, 7, 4], frame_ms: 100 }
walk_east: { frames: [8, 9, 10, 11], frame_ms: 100 } walk_east: { frames: [9, 10, 11, 8], frame_ms: 100 }
walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true } walk_west: { frames: [9, 10, 11, 8], frame_ms: 100, flip_h: true }
hurt_south: { 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_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
hurt_east: { frames: [8, 9, 10, 11], 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_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 } attack_south: { frames: [20, 21, 22, 23], frame_ms: 80 }
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 } attack_north: { frames: [24, 25, 26, 27], frame_ms: 80 }
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 } attack_east: { frames: [28, 29, 30, 31], frame_ms: 80 }
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true } attack_west: { frames: [28, 29, 30, 31], frame_ms: 80, flip_h: true }
recover_south: { frames: [23] } recover_south: { frames: [23] }
recover_north: { frames: [27] } recover_north: { frames: [27] }
@@ -97,13 +97,10 @@ turn:
south_to_north: west south_to_north: west
# Salt vertical (px de z, no toca la posició al món). # 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: jump:
impulse: 200 impulse: 200
gravity: 600 gravity: 600
landing_stun_ms: 400 landing_stun_ms: 400
shadow_y_offset: 3
sound: jump.wav sound: jump.wav
land_sound: land.wav land_sound: land.wav
@@ -119,7 +116,7 @@ hurt:
# recuperació en què la hitbox ja no fa dany. # recuperació en què la hitbox ja no fa dany.
whip: whip:
range_tiles: 3 range_tiles: 3
swing_ms: 200 swing_ms: 320
cooldown_ms: 200 cooldown_ms: 200
damage: 1 damage: 1
sound: whip.wav sound: whip.wav
@@ -136,10 +133,18 @@ death:
# poden ferir ni matar el jugador (els forats sí que el maten — la # poden ferir ni matar el jugador (els forats sí que el maten — la
# caiguda no és dany convencional). Visualment es manifesta amb un # caiguda no és dany convencional). Visualment es manifesta amb un
# parpadeig que alterna la paleta normal del sprite amb la paleta # parpadeig que alterna la paleta normal del sprite amb la paleta
# `palette` (basename de `data/player/<name>.pal`) cada # `palette` (basename de `data/entities/player/<name>.pal`) cada
# `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la # `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la
# mecànica queda desactivada i el comportament és l'antic. # mecànica queda desactivada i el comportament és l'antic.
immunity: immunity:
duration_ms: 3000 duration_ms: 3000
blink_period_ms: 100 blink_period_ms: 100
palette: immune palette: sam_wash
# Llum emesa pel jugador. `radius` és el radi del cercle de llum
# en píxels del canvas; el gradient (3 anells: 1.0 / 0.9 / 0.8) viu
# a data/config/lighting.yaml i és compartit per tots els emissors.
# Omet el bloc per desactivar la llum d'aquest personatge.
light:
radius: 64
flicker: true
+19
View File
@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0 255
150 133 88 255
202 176 123 255
191 165 112 255
181 156 103 255
217 158 115 255
194 142 101 255
188 143 98 255
214 193 145 255
196 176 128 255
181 161 115 255
246 211 157 255
229 192 139 255
213 177 127 255
243 226 184 255
0 0 0 255
+19
View File
@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0 255
102 118 132 255
166 169 171 255
152 156 158 255
140 144 147 255
191 144 160 255
161 125 146 255
152 127 142 255
179 190 198 255
157 168 179 255
139 151 164 255
221 210 210 255
200 187 189 255
181 169 175 255
213 229 247 255
0 0 0 255
+19
View File
@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0 255
190 190 190 255
235 220 205 255
226 212 197 255
219 205 191 255
255 194 197 255
248 184 193 255
238 188 189 255
239 233 226 255
226 220 216 255
214 210 208 255
255 242 225 255
255 228 213 255
251 216 207 255
255 255 255 255
0 0 0 255
+1 -1
View File
@@ -13,7 +13,7 @@ flags:
collides: true collides: true
interactable: false interactable: false
animations: animations:
roll: { frames: [0], fps: 10, loop: true } roll: { frames: [0], frame_ms: 100, loop: true }
ai: ai:
type: straight_line type: straight_line
initial_direction: right initial_direction: right
Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 1.6 KiB

+21 -3
View File
@@ -19,12 +19,30 @@ flags:
collides: true collides: true
interactable: true interactable: true
animations: animations:
closed: { frames: [0], fps: 1, loop: true } closed: { frames: [0], frame_ms: 1000, loop: true }
opening: { frames: [0], fps: 6, loop: false } opening: { frames: [1, 2], frame_ms: 167, loop: false }
open: { frames: [0], fps: 1, loop: true } open: { frames: [3], frame_ms: 1000, loop: true }
behaviour: behaviour:
states: [CLOSED, OPENING, OPEN] states: [CLOSED, OPENING, OPEN]
initial_state: CLOSED initial_state: CLOSED
on_perimeter_complete: OPENING on_perimeter_complete: OPENING
on_opening_end: OPEN on_opening_end: OPEN
emits_event_on_open: sarcophagus_opened emits_event_on_open: sarcophagus_opened
# Sons opcionals. `opening` es dispara un sol cop en la transició
# CLOSED → OPENING (quan el jugador completa el perímetre). Si la clau
# manca, no sona res. El basename val tant de clau al Resource::Cache
# com de path (`Defaults::Paths::AUDIO_SOUNDS_DIR + key`).
#
# `opening_pitch_spread`: variació uniforme del ratio de reproducció
# centrada a 1.0. Amb 0.05 cada obertura sona entre 0.95× i 1.05× la
# velocitat normal (puja/baixa també el to, efecte arcade clàssic).
# 0 ⇒ sempre 1.0 (sense variació).
sound:
opening: open_sarc.wav
opening_pitch_spread: 0.5
# Recompensa que rep el jugador que pinta el tile que tanca el perímetre
# (el GameScene la reparteix consumint `sarcophagus_opened`). Si es lleva
# aquest bloc o un dels camps, el camp absent val 0 i no s'aplica.
reward:
points: 100
treasures: 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

+22
View File
@@ -0,0 +1,22 @@
name: stone
type: decoration
editable: true
# Sub-picker de frames a l'editor: ofereix una graella amb les 24 variants
# del atlas (6×4 sub-sprites 16×16) per escollir quina representa cada
# instància col·locada.
editable_variants: true
sprite:
path: stone.gif
size: [16, 16]
tile_size: [1, 1]
collision:
size: [16, 16]
# Mur infranquejable al salt: el jugador no la pot saltar per damunt.
height: 999
flags:
collides: true
interactable: false
# Animació "default" amb un sol frame: el spawn YAML pot sobreescriure-la
# via `frame: N` per fixar la variant escollida al editor.
animations:
default: { frames: [0], frame_ms: 1000, loop: false }
+16 -1
View File
@@ -15,4 +15,19 @@ flags:
# El torch.gif és un strip vertical 16x64 amb 4 frames apilats. # El torch.gif és un strip vertical 16x64 amb 4 frames apilats.
# El render clipa cada frame per `anim.currentFrame()` × 16 píxels en Y. # El render clipa cada frame per `anim.currentFrame()` × 16 píxels en Y.
animations: animations:
idle: { frames: [0, 1, 2, 3], fps: 8, loop: true } idle: { frames: [0, 1, 2, 3], frame_ms: 125, loop: true }
# Cercle de llum centrat a l'antorxa. El radi és en píxels del canvas;
# el gradient global (data/config/lighting.yaml) hi posa la forma (3
# anells). Amb `flicker: true` el radi efectiu oscil·la per frame
# (suma de dues sinusoides amb fase derivada de la posició) per a
# que el cercle "respiri" com una flama; els paràmetres del flicker
# també viuen al config global. `offset: [x, y]` desplaça el centre
# del cercle: la flama del sprite no està al centre del bounding box
# sinó cap a dalt, així que pujem la llum 4 px (Y creix cap avall)
# per a que el halo "neixi" de la flama, no del peu. Omet el bloc
# per fer una antorxa "apagada".
light:
radius: 40
flicker: true
offset: [0, -4]
+2 -2
View File
@@ -13,8 +13,8 @@ flags:
collides: false collides: false
interactable: false interactable: false
animations: animations:
inactive: { frames: [0], fps: 1, loop: true } inactive: { frames: [0], frame_ms: 1000, loop: true }
active: { frames: [0], fps: 6, loop: true } active: { frames: [0], frame_ms: 167, loop: true }
cycle: cycle:
active_duration: 0.5 active_duration: 0.5
inactive_duration: 1.5 inactive_duration: 1.5
+2 -2
View File
@@ -25,7 +25,7 @@ sprites:
# Animació compartida entre les 4 direccions: la seqüència de frames i les # 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. # durades són idèntiques; només canvia quina surface/mida es clipa.
# - `frames` : índex de tile dins l'spritesheet (graella deduïda). # - `frames` : índex de tile dins l'spritesheet (graella deduïda).
# - `frame_ms` : durada uniforme per frame; 50 ms × 4 ≈ 200 ms swing. # - `frame_ms` : durada uniforme per frame; 80 ms × 4 = 320 ms swing.
# - `loop: false` : el látigo viu una animació i mor (`markDead`). # - `loop: false` : el látigo viu una animació i mor (`markDead`).
# - `active_frames` : valors dins de `frames` durant els quals la # - `active_frames` : valors dins de `frames` durant els quals la
# hitbox aplica dany. Wind-up (0) i recovery (3) no # hitbox aplica dany. Wind-up (0) i recovery (3) no
@@ -33,6 +33,6 @@ sprites:
animations: animations:
swing: swing:
frames: [0, 1, 2, 3] frames: [0, 1, 2, 3]
frame_ms: 50 frame_ms: 80
loop: false loop: false
active_frames: [1, 2] active_frames: [1, 2]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 B

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 327 B

+133
View File
@@ -0,0 +1,133 @@
# Font: Tiny5-Regular — generado desde Tiny5-Regular.ttf size 8
# Generado con font_gen.py
box_width 6
box_height 9
columns 15
# codepoint_decimal ancho_visual
32 2 # U+0020
33 1 # !
34 3 # "
35 5 # #
36 3 # $
37 4 # %
38 4 # &
39 1 # '
40 2 # (
41 2 # )
42 3 # *
43 3 # +
44 2 # ,
45 3 # -
46 1 # .
47 2 # /
48 3 # 0
49 2 # 1
50 3 # 2
51 3 # 3
52 3 # 4
53 3 # 5
54 3 # 6
55 3 # 7
56 3 # 8
57 3 # 9
58 1 # :
59 2 # ;
60 3 # <
61 3 # =
62 3 # >
63 3 # ?
64 4 # @
65 4 # A
66 4 # B
67 4 # C
68 4 # D
69 4 # E
70 4 # F
71 4 # G
72 4 # H
73 1 # I
74 3 # J
75 4 # K
76 3 # L
77 5 # M
78 4 # N
79 4 # O
80 4 # P
81 4 # Q
82 4 # R
83 4 # S
84 3 # T
85 4 # U
86 3 # V
87 5 # W
88 3 # X
89 3 # Y
90 3 # Z
91 2 # [
92 2 # \
93 2 # ]
94 3 # ^
95 3 # _
96 2 # `
97 3 # a
98 3 # b
99 3 # c
100 3 # d
101 3 # e
102 2 # f
103 3 # g
104 3 # h
105 1 # i
106 1 # j
107 3 # k
108 1 # l
109 5 # m
110 3 # n
111 3 # o
112 3 # p
113 3 # q
114 2 # r
115 3 # s
116 2 # t
117 3 # u
118 3 # v
119 5 # w
120 3 # x
121 3 # y
122 3 # z
123 3 # {
124 1 # |
125 3 # }
126 2 # ~
192 4 # À
193 4 # Á
200 4 # È
201 4 # É
204 2 # Ì
205 2 # Í
210 4 # Ò
211 4 # Ó
217 4 # Ù
218 4 # Ú
209 4 # Ñ
199 4 # Ç
224 3 # à
225 3 # á
232 3 # è
233 3 # é
236 2 # ì
237 2 # í
242 3 # ò
243 3 # ó
249 3 # ù
250 3 # ú
241 3 # ñ
231 3 # ç
161 1 # ¡
191 3 # ¿
171 5 # «
187 5 # »
183 1 # ·
169 3 # ©
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+7 -7
View File
@@ -2,7 +2,7 @@
# Generado con font_gen.py # Generado con font_gen.py
box_width 12 box_width 12
box_height 14 box_height 13
columns 15 columns 15
# codepoint_decimal ancho_visual # codepoint_decimal ancho_visual
@@ -105,8 +105,8 @@ columns 15
193 8 # Á 193 8 # Á
200 7 # È 200 7 # È
201 7 # É 201 7 # É
204 4 # Ì 204 3 # Ì
205 4 # Í 205 3 # Í
210 9 # Ò 210 9 # Ò
211 9 # Ó 211 9 # Ó
217 7 # Ù 217 7 # Ù
@@ -117,8 +117,8 @@ columns 15
225 7 # á 225 7 # á
232 7 # è 232 7 # è
233 7 # é 233 7 # é
236 4 # ì 236 3 # ì
237 4 # í 237 3 # í
242 7 # ò 242 7 # ò
243 7 # ó 243 7 # ó
249 7 # ù 249 7 # ù
@@ -127,7 +127,7 @@ columns 15
231 7 # ç 231 7 # ç
161 3 # ¡ 161 3 # ¡
191 7 # ¿ 191 7 # ¿
171 10 # « 171 7 # «
187 10 # » 187 7 # »
183 3 # · 183 3 # ·
169 10 # © 169 10 # ©
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

+7 -7
View File
@@ -2,7 +2,7 @@
# Generado con font_gen.py # Generado con font_gen.py
box_width 12 box_width 12
box_height 14 box_height 13
columns 15 columns 15
# codepoint_decimal ancho_visual # codepoint_decimal ancho_visual
@@ -105,8 +105,8 @@ columns 15
193 9 # Á 193 9 # Á
200 8 # È 200 8 # È
201 8 # É 201 8 # É
204 4 # Ì 204 3 # Ì
205 4 # Í 205 3 # Í
210 10 # Ò 210 10 # Ò
211 10 # Ó 211 10 # Ó
217 8 # Ù 217 8 # Ù
@@ -117,8 +117,8 @@ columns 15
225 8 # á 225 8 # á
232 8 # è 232 8 # è
233 8 # é 233 8 # é
236 4 # ì 236 3 # ì
237 4 # í 237 3 # í
242 8 # ò 242 8 # ò
243 8 # ó 243 8 # ó
249 8 # ù 249 8 # ù
@@ -127,7 +127,7 @@ columns 15
231 8 # ç 231 8 # ç
161 3 # ¡ 161 3 # ¡
191 8 # ¿ 191 8 # ¿
171 10 # « 171 7 # «
187 10 # » 187 7 # »
183 3 # · 183 3 # ·
169 10 # © 169 10 # ©
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

+20 -3
View File
@@ -1,4 +1,4 @@
# AEE 2026 — Català # AEE ARCADE — Català
# lang: ca # lang: ca
notification: notification:
@@ -14,7 +14,7 @@ input:
shader_off: "Shaders desactivats" shader_off: "Shaders desactivats"
vsync_on: "VSync activat" vsync_on: "VSync activat"
vsync_off: "VSync desactivat" vsync_off: "VSync desactivat"
screenshot: "Captura" screenshot: "Imatge {} guardada a {}"
shader: "Shader" shader: "Shader"
preset: "Preset" preset: "Preset"
scaling_disabled: "Escalat sense" scaling_disabled: "Escalat sense"
@@ -23,7 +23,6 @@ input:
scaling_overscan: "Escalat overscan" scaling_overscan: "Escalat overscan"
scaling_integer: "Escalat enter" scaling_integer: "Escalat enter"
press_again_exit: "Prem una altra volta per eixir" press_again_exit: "Prem una altra volta per eixir"
paused: "Pausa"
editor_on: "Editor ON" editor_on: "Editor ON"
editor_off: "Editor OFF" editor_off: "Editor OFF"
editor_save_error: "Editor: error guardant el mapa" editor_save_error: "Editor: error guardant el mapa"
@@ -38,6 +37,13 @@ hud:
treasures: "Tres" treasures: "Tres"
pyramid: "Piramide" pyramid: "Piramide"
room: "Sala" room: "Sala"
# Prompts mostrats al bloc d'un slot inactiu (en lloc dels zeros)
# quan un jugador encara no ha pulsat START o ha esgotat vides.
# Dues línies per fer-los caber al bloc lateral (~106 px).
join_prompt_can_join_l1: "Polsa start"
join_prompt_can_join_l2: "per jugar"
join_prompt_wait_l1: "Espera"
join_prompt_wait_l2: "per jugar"
# Textos de l'escena TITLE. # Textos de l'escena TITLE.
title: title:
@@ -53,6 +59,12 @@ game:
room_intro_room: "Sala {r}" room_intro_room: "Sala {r}"
room_complete_line1: "Fase" room_complete_line1: "Fase"
room_complete_line2: "Completada" room_complete_line2: "Completada"
# Rètol del game over (sub-FSM de la GameScene). El countdown es
# pinta com a segona línia (un enter) durant la fase 1 de
# CONTINUE_PROMPT — només cal traduir el rètol superior.
continue_prompt: "Continuar?"
game_over_line1: "Fi del"
game_over_line2: "Joc"
menu: menu:
titles: titles:
@@ -62,6 +74,7 @@ menu:
controls: "Controls" controls: "Controls"
keyboard: "Teclat" keyboard: "Teclat"
gamepad: "Comandament" gamepad: "Comandament"
tweaks: "Jugabilitat"
system: "Sistema" system: "Sistema"
labels: labels:
player_1: "Jugador 1" player_1: "Jugador 1"
@@ -73,6 +86,7 @@ menu:
controls_submenu: "Controls" controls_submenu: "Controls"
keyboard_submenu: "Teclat" keyboard_submenu: "Teclat"
gamepad_submenu: "Comandament" gamepad_submenu: "Comandament"
tweaks_submenu: "Jugabilitat"
system_submenu: "Sistema" system_submenu: "Sistema"
move_up: "Amunt" move_up: "Amunt"
move_down: "Avall" move_down: "Avall"
@@ -101,6 +115,9 @@ menu:
music_volume: "Volum música" music_volume: "Volum música"
sounds: "Sons" sounds: "Sons"
sounds_volume: "Volum sons" sounds_volume: "Volum sons"
whip_pierces_walls: "Látec travessa sarcs"
friendly_fire: "Foc amic"
players_collide: "Col·lisió entre jugadors"
restart: "Reiniciar" restart: "Reiniciar"
exit_game: "Eixir del joc" exit_game: "Eixir del joc"
values: values:
+20 -3
View File
@@ -1,4 +1,4 @@
# AEE 2026 — English # AEE ARCADE — English
# lang: en # lang: en
notification: notification:
@@ -14,7 +14,7 @@ input:
shader_off: "Shaders off" shader_off: "Shaders off"
vsync_on: "VSync on" vsync_on: "VSync on"
vsync_off: "VSync off" vsync_off: "VSync off"
screenshot: "Screenshot" screenshot: "Image {} saved at {}"
shader: "Shader" shader: "Shader"
preset: "Preset" preset: "Preset"
scaling_disabled: "Scaling off" scaling_disabled: "Scaling off"
@@ -23,7 +23,6 @@ input:
scaling_overscan: "Scaling overscan" scaling_overscan: "Scaling overscan"
scaling_integer: "Scaling integer" scaling_integer: "Scaling integer"
press_again_exit: "Press again to exit" press_again_exit: "Press again to exit"
paused: "Paused"
editor_on: "Editor ON" editor_on: "Editor ON"
editor_off: "Editor OFF" editor_off: "Editor OFF"
editor_save_error: "Editor: map save failed" editor_save_error: "Editor: map save failed"
@@ -38,6 +37,13 @@ hud:
treasures: "Tre" treasures: "Tre"
pyramid: "Pyramid" pyramid: "Pyramid"
room: "Room" room: "Room"
# Prompts shown in an inactive slot's block (instead of zeros) while
# a player hasn't pressed START yet or has run out of lives. Two
# short lines so they fit in the side block (~106 px wide).
join_prompt_can_join_l1: "Press start"
join_prompt_can_join_l2: "to play"
join_prompt_wait_l1: "Please"
join_prompt_wait_l2: "wait"
# Texts of the TITLE scene. # Texts of the TITLE scene.
title: title:
@@ -52,6 +58,12 @@ game:
room_intro_room: "Room {r}" room_intro_room: "Room {r}"
room_complete_line1: "Stage" room_complete_line1: "Stage"
room_complete_line2: "Complete" room_complete_line2: "Complete"
# Game over labels (sub-FSM in GameScene). The countdown is drawn as
# a numeric second line during CONTINUE_PROMPT phase 1, so only the
# top label needs translation.
continue_prompt: "Continue?"
game_over_line1: "Game"
game_over_line2: "Over"
menu: menu:
titles: titles:
@@ -61,6 +73,7 @@ menu:
controls: "Controls" controls: "Controls"
keyboard: "Keyboard" keyboard: "Keyboard"
gamepad: "Gamepad" gamepad: "Gamepad"
tweaks: "Gameplay"
system: "System" system: "System"
labels: labels:
player_1: "Player 1" player_1: "Player 1"
@@ -72,6 +85,7 @@ menu:
controls_submenu: "Controls" controls_submenu: "Controls"
keyboard_submenu: "Keyboard" keyboard_submenu: "Keyboard"
gamepad_submenu: "Gamepad" gamepad_submenu: "Gamepad"
tweaks_submenu: "Gameplay"
system_submenu: "System" system_submenu: "System"
move_up: "Up" move_up: "Up"
move_down: "Down" move_down: "Down"
@@ -100,6 +114,9 @@ menu:
music_volume: "Music volume" music_volume: "Music volume"
sounds: "Sounds" sounds: "Sounds"
sounds_volume: "Sound volume" sounds_volume: "Sound volume"
whip_pierces_walls: "Whip pierces sarcs"
friendly_fire: "Friendly fire"
players_collide: "Player collision"
restart: "Restart" restart: "Restart"
exit_game: "Exit game" exit_game: "Exit game"
values: values:
+33 -4
View File
@@ -10,11 +10,38 @@
# la cel·la buida (transparent), útil per caselles que ocuparà un actor. # la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 1 pyramid: 1
room: 1 room: 2
tileset: data/tilesets/pyramid_1.gif tileset: data/tilesets/pyramid_1.gif
tile_size: 16 tile_size: 16
origin: [-8, 0] origin: [-8, 0]
# Banc de proves del mode `additive` d'il·luminació. La sala es veu
# "normal" (level 1.0, el GIF intacte fora del cercle de les torxes)
# i només les antorxes hi sumen un halo càlid suau. Player i mummies
# desactivats explícitament perquè aquí no volem cap halo al voltant
# d'actors — el toc cromàtic és només l'antorxa. La corba additiva
# (intensitats baixes a propòsit perquè additive satura ràpid) viu a
# `gradient_additive:` de data/config/lighting.yaml.
lighting:
mode: additive
level: 1.0
emitters:
torches:
radius: 20
color: [255, 170, 60]
# Valors default del flicker explicitats per anar-los ajustant
# in-situ. Coincideixen amb els del global a data/config/lighting.yaml;
# baixar wave_frequency_hz fa el "palpitar" més lent, baixar les
# ratios fa l'oscil·lació més subtil. Qualsevol camp es pot
# eliminar i caurà al valor del global.
flicker_params:
wave_amplitude_ratio: 0.04
wave_frequency_hz: 3.0
noise_amplitude_ratio: 0.025
noise_frequency_hz: 3.0
players: false
mummies: false
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl # Í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 # pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat # es distingeix a simple vista. Provisional fins que l'art del pintat
@@ -82,6 +109,8 @@ data:
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat # d'obertura (262/263). La momia al corredor central. L'antorxa al costat
# superior esquerre per verificar render d'una entitat decorativa. # superior esquerre per verificar render d'una entitat decorativa.
entities: entities:
- type: torch
position: [4, 2]
- type: torch - type: torch
position: [8, 2] position: [8, 2]
- type: torch - type: torch
@@ -128,8 +157,8 @@ entities:
sarc_position: [2, 4] sarc_position: [2, 4]
- type: sarcophagus - type: sarcophagus
sarc_position: [3, 4] sarc_position: [3, 4]
- type: torch
position: [4, 2]
- type: door - type: door
- type: mummy - type: mummy
position: [10, 11] position: [10, 7]
- type: mummy
position: [7, 9]
+56 -13
View File
@@ -10,11 +10,38 @@
# la cel·la buida (transparent), útil per caselles que ocuparà un actor. # la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 1 pyramid: 1
room: 2 room: 1
tileset: data/tilesets/pyramid_1.gif tileset: data/tilesets/pyramid_1.gif
tile_size: 16 tile_size: 16
origin: [-8, 0] origin: [-8, 0]
# Il·luminació de la sala. Bloc opcional; si manca, la sala queda
# totalment il·luminada (1.0) i tots els tipus d'entitat emeten amb els
# valors declarats al seu YAML de tipus (comportament històric).
#
# level → atenuació base de la sala (0..1). 1.0 = res
# d'efecte, 0.0 = negre fora del radi dels emissors.
# El gradient dels emissors viu a
# data/config/lighting.yaml.
# emitters.<tipus> → override per tipus d'entitat. Tres formes:
# false → apaga el tipus
# true → defaults del tipus
# { radius: N, flicker: B } → camps presents
# sobreescriuen el
# default; els
# absents cauen al
# default del tipus.
# Plural per llegir-se natural ("totes les momies de la sala"). Per a
# casos particulars, un spawn pot dur el seu propi `light:` (mateixes
# formes) que té prioritat sobre l'override de sala. La forma antiga
# (`light_level:` plà a l'arrel) també segueix sent vàlida per
# retrocompatibilitat.
lighting:
level: 0.15
emitters:
players: true
mummies: false
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl # Í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 # pintable. Un índex per jugador perquè a multijugador local el recorregut
# es distingeix a simple vista. Provisional fins que l'art del pintat # es distingeix a simple vista. Provisional fins que l'art del pintat
@@ -82,16 +109,12 @@ data:
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat # d'obertura (262/263). La momia al corredor central. L'antorxa al costat
# superior esquerre per verificar render d'una entitat decorativa. # superior esquerre per verificar render d'una entitat decorativa.
entities: entities:
- type: torch
position: [4, 2]
- type: torch - type: torch
position: [8, 2] position: [8, 2]
- type: torch - type: torch
position: [12, 2] position: [12, 2]
- type: torch - type: torch
position: [16, 2] position: [16, 2]
- type: sarcophagus
sarc_position: [0, 0]
- type: sarcophagus - type: sarcophagus
sarc_position: [1, 0] sarc_position: [1, 0]
- type: sarcophagus - type: sarcophagus
@@ -118,8 +141,6 @@ entities:
sarc_position: [0, 3] sarc_position: [0, 3]
- type: sarcophagus - type: sarcophagus
sarc_position: [1, 3] sarc_position: [1, 3]
- type: sarcophagus
sarc_position: [2, 3]
- type: sarcophagus - type: sarcophagus
sarc_position: [3, 3] sarc_position: [3, 3]
- type: sarcophagus - type: sarcophagus
@@ -130,11 +151,33 @@ entities:
sarc_position: [2, 4] sarc_position: [2, 4]
- type: sarcophagus - type: sarcophagus
sarc_position: [3, 4] sarc_position: [3, 4]
- type: torch
position: [4, 2]
- type: door - type: door
# Sense `position`: la porta sempre viu al mur trasser. La columna - type: column
# transitable la dicta `geometry.door_col`; el render i la caixa position: [3, 4]
# de col·lisió es deriven d'aquesta dada al loadFromLevel. frame: 2
- type: stone
position: [5, 4]
frame: 16
- type: stone
position: [4, 4]
frame: 19
- type: column
position: [13, 10]
frame: 4
- type: stone
position: [12, 10]
frame: 13
- type: stone
position: [11, 10]
frame: 23
- type: hole
position: [5, 9]
- type: hole
position: [9, 11]
- type: stone
position: [10, 10]
frame: 18
- type: mummy - type: mummy
position: [10, 7] position: [10, 13]
- type: mummy
position: [7, 9]
-35
View File
@@ -1,35 +0,0 @@
JASC-PAL
0100
32
0 0 0
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
217 217 217
-48
View File
@@ -1,48 +0,0 @@
# 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
+96
View File
@@ -0,0 +1,96 @@
# 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/config/ui.yaml >
# 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/config/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/config/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:
# Violeta cerimonial: contrasta amb l'or del text (room_text), no
# compite amb el blau del bloc P2 del HUD, i deixa la sorra de les
# sales respirar al darrere.
color: [72, 36, 85]
alpha: 160
height_px: 32
# Sub-FSM de game over: dispara CONTINUE? quan tots els jugadors
# queden fora de partida durant PLAYING, fa countdown, i si ningú
# pulsa START acaba en GAME OVER + fade-to-black + transició a LOGO.
# Tots els valors són ms (excepte `continue_count_steps`, que és el
# nombre de tics visibles del countdown). Si manca el fitxer o alguna
# clau, GameScene cau als fallbacks compilats (vegeu `GameOverFsm::Config`).
game_over:
# CONTINUE? — animació d'entrada i sortida idèntica al room text.
continue_text_enter_ms: 400
# Hold del countdown: 10 s = (continue_count_start + 1) graons d'1 s.
# La sèrie visible és [continue_count_start, ..., 1, 0]; el "0" es
# manté l'últim segon abans del salt a GAME OVER.
continue_text_hold_ms: 10000
continue_text_exit_ms: 400
continue_count_start: 9
# GAME OVER — apareix sense enter anim (panel ja amunt del CONTINUE?),
# es manté un temps i marxa amb la mateixa exit anim del room text.
game_over_text_hold_ms: 1500
game_over_text_exit_ms: 400
game_over_wait_ms: 400
# Fade final cap a LOGO. `black_hold_ms` és la pausa en negre opac
# abans que el GameScene demani la transició d'escena.
fade_out_ms: 500
black_hold_ms: 500
# Velocitat (frequency ratio) de la música mentre dura el countdown
# de CONTINUE_PROMPT. 1.0 = sense canvi, 1.5 = un 50% més ràpid (i
# amb el to més agut: efecte arcade clàssic). Es restableix a 1.0 al
# sortir del prompt — tant si el jugador reviu com si entra GAME OVER.
continue_music_speed: 1.05
# Pista de música que sona durant l'estat GAME_OVER (sense loop).
# La fase de hold del text es sincronitza amb la durada real de la
# pista, així que el rètol "Fi del Joc" es manté visible mentre dura
# la música; després ve l'exit anim, el wait i el fade a LOGO.
game_over_music_track: "game_over.ogg"
# So que sona cada vegada que apareix un nou número del countdown
# (al primer cop que es mostra `continue_count_start`, a cada canvi,
# i quan apareix el 0 — però NO al saltar a GAME OVER). Buit = silenci.
countdown_sound_key: "countdown.wav"
# Llindar a partir del qual qualsevol botó de salt o atac de
# qualsevol jugador avança al següent número del countdown.
# 7 → skip habilitat mentre el número visible és 7..1 (no a 0).
# -1 = mai. Útil per accelerar la decisió un cop la urgència ja
# és evident al jugador.
skip_below_or_equal: 7
+24
View File
@@ -0,0 +1,24 @@
# Configuració de la LogoScene (logotip de l'editora). El revelat del
# logo a la fase SCAN imita un escaneig CRT: una banda lluminosa baixa
# de dalt a baix; per damunt es veu la imatge ja "impressa", per sota
# encara és negre. Al voltant de la banda hi ha glitch (offset
# horitzontal de files amb falloff per distància) i ocasionalment
# apareixen bursts VHS (franges desplaçades) damunt la part ja
# revelada. Quan la banda toca el peu del logo, un flash blanc tanca
# l'efecte i s'entra a MIDDLE (logo estable).
logo:
scan:
duration_ms: 1500 # durada total del scan top→bottom
band_height_px: 4 # alçada (px) de la banda lluminosa
band_alpha: 200 # opacitat (0..255) de la capa blanca de la banda
band_pulse_hz: 20.0 # freqüència del pulse de brillantor (Hz)
band_pulse_depth: 0.45 # profunditat del pulse (0..1; 0 = sense pulse)
# Glitch d'impacte (files al voltant de la banda)
glitch_amplitude_px: 6 # offset horitzontal màxim a la banda
glitch_band_radius_px: 12 # files damunt/sota la banda amb glitch (falloff lineal)
# Bursts VHS sobre la part ja revelada
vhs_burst_per_sec: 3 # bursts/segon mitjà
vhs_burst_duration_ms: 80 # durada de cada burst
vhs_burst_height_px: 8 # alçada de la franja afectada
vhs_burst_amplitude_px: 8 # offset horitzontal del burst
flash_ms: 100 # flash blanc final quan acaba el scan
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

+77
View File
@@ -0,0 +1,77 @@
# Configuració de la TitleScene.
# `bg_color` i `text_color` són índexs sobre la paleta "ui" (uzebox.pal):
# 1 = (36,0,0) gairebé negre amb un mínim de càlid; 31 = (255,109,0)
# taronja pur. `duration_ms` és el timeout abans de saltar a la
# següent escena (skipable amb tecla no reservada). Els dos
# `blink_*_ms` defineixen el cicle del prompt "press start": el text
# es veu durant `visible_ms` i s'amaga durant `hidden_ms` repetint-se.
title:
duration_ms: 30000
bg_color: 1
text_color: 31
# Color del text del © de copyright (peu de la TITLE). Off-white càlid:
# índex 183 = (255,218,170), tipus crema/peach, ~90% blanc amb tint càlid.
copyright_color: 183
# Color de l'outline del prompt "PRESS START". Marró fosc càlid (índex
# 19 = 109,72,0), coherent amb la paleta del logo. Es manté separat de
# `copyright_color` perquè l'outline i el text del © tenen rols visuals
# diferents (contrast vs. peu informatiu).
press_start_outline_color: 19
# Posicions Y dels tres elements verticals de la TITLE, en percentatge
# sobre l'alçada del canvas (240 px). El logo s'ancora pel seu top
# (la imatge cau cap avall des d'aquest Y); el prompt i el copyright
# es centren verticalment sobre la línia.
logo_y_percent: 15
prompt_y_percent: 68
copyright_y_percent: 92
# Separació vertical en píxels entre el minilogo i el text del © al peu.
mini_logo_gap_px: 2
blink_visible_ms: 800
blink_hidden_ms: 200
# Fase STARTING (entre la primera pulsació de START i el salt a la
# següent escena). `starting_ms` és la durada fixa; durant aquesta
# fase el parpadeig accelera per `starting_blink_speedup` (2.0 = doble
# de freqüència). Si l'altre jugador prem START dins d'aquesta
# finestra, el comptador es reseteja i s'arrenca amb BOTH.
starting_ms: 3000
# Durant STARTING el prompt commuta a un cicle simètric 50/50
# (visible/ocult) amb aquest període total. 200 ms = 5 Hz, prou
# ràpid per llegir-se com a "pulsació confirmada".
starting_blink_period_ms: 200
# Fade-out estil "mosaic dissolve" durant l'última part de STARTING.
# `fade_out_ms` ha de ser <= starting_ms (els últims X ms del STARTING
# es fan amb el dissolve actiu fins a quedar negre). `fade_out_max_block`
# és la mida màxima de bloc en píxels al final del fade.
fade_out_ms: 800
fade_out_max_block: 32
# Fade-out de `title.ogg` en entrar a STARTING. Independent del visual:
# la música pot acabar abans o després del mosaic dissolve.
music_fade_out_ms: 3000
# Fade-to-black per a la sortida d'attract (timeout cap a LOGO). NO és
# mosaic — capa negra lineal damunt del canvas. La música no es toca:
# `title.ogg` continua sonant cap a LOGO, que ja veurà què en fa.
attract_fade_to_black_ms: 800
# Seqüència d'aparició dels elements de la TITLE en fase IDLE. Tots
# els temps són absoluts des de l'entrada a l'escena. `fade_in_ms` és
# la capa negra inicial que s'esvaeix cap al fons; els altres marquen
# quan apareix cada element. Si l'usuari prem START en qualsevol
# moment, la seqüència es completa instantàniament.
intro:
fade_in_ms: 500 # fade-in negre inicial sobre el fons
game_logo_at_ms: 1500 # visibilitat binària del game_logo
mini_logo_at_ms: 2000 # inici del slide-in del mini-logo
mini_logo_slide_ms: 500 # durada del slide
copyright_at_ms: 2200 # inici del slide-in del copyright
copyright_slide_ms: 500 # durada del slide
slide_curve: outCubic # corba compartida per mini_logo i copyright
press_start_at_ms: 3000 # quan apareix PRESS START amb parpadeig
# Distorsió sinusoidal del fons (efecte de calor del desert).
# `amplitude_px`: desplaçament màxim de cada fila en píxels.
# `wavelength_px`: distància vertical entre dos pics de l'ona.
# `speed_hz`: velocitat amb què avança la fase (cicles per segon).
# Si `enabled` és false, el fons es pinta sense distorsió.
heat_haze:
enabled: true
amplitude_px: 1.5
wavelength_px: 32.0
speed_hz: 0.6
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

-155
View File
@@ -1,155 +0,0 @@
# Tema de la UI: índexs de paleta, timings, fonts i flags visuals. Tota
# clau absent cau al valor compilat a Defaults::UI (veure defaults.hpp).
colors:
render_info_text: 58
render_info_shadow: 16
canvas_bg: 72
notification_text: 232
notification_bg: 64
exit_text: 71
exit_bg: 2
menu_bg: 2
menu_border: 232
menu_title: 237
menu_label: 58
menu_value: 232
menu_cursor: 71
menu_disabled: 16
hud_bg: 9
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
exit_ms: 4000
fonts:
hud:
name: "8bithud"
gif: "data/fonts/8bithud.gif"
fnt: "data/fonts/8bithud.fnt"
kerning: 1
placeholder:
name: "ari-w9500-condensed-display"
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"
fnt: "data/fonts/ari-w9500-display.fnt"
kerning: 0
menu_items:
name: "ari-w9500-condensed-display"
gif: "data/fonts/ari-w9500-condensed-display.gif"
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
kerning: 0
hud:
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
# Geometria del popup superior (Notifier). En píxels del canvas, llevat de
# slide_ms (durada en ms de l'animació d'entrada i d'eixida; constant
# independentment de la mida de la caixa). El padding vertical és asimètric a
# propòsit: padding_top > padding_bottom puja una mica el text dins la caixa.
# Corbes disponibles: linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
notifier:
padding_h: 6
padding_top: 4
padding_bottom: 3
line_gap: 1
margin_top: 3
border_px: 1
slide_ms: 300
slide_in_curve: outCubic
slide_out_curve: inCubic
# Pausa graduada de la GameScene: el món no para d'un colp, s'atura amb
# easing i torna a arrancar amb una altra corba. Els timings es poden
# afinar independentment, i les corbes permeten estils diferents entrant
# i sortint (p.ex. frenar suau i arrancar seca). Corbes disponibles:
# linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
pause:
enter_ms: 400
exit_ms: 250
enter_curve: outCubic
exit_curve: inCubic
# Sons del menú de servei. Els fitxers viuen a data/sounds/. SELECT
# acompanya navegació (amunt/avall) i back; ACCEPT acompanya modificació
# d'opció (esquerra/dreta/retorn) o entrada a un submenú.
sounds:
menu_select: "menu_select.wav"
menu_accept: "menu_accept.wav"
# Disparat quan un jugador prem START a la TITLE (primera pulsació o
# la del segon jugador dins de la finestra de STARTING).
player_start: "player_start.wav"
# Escena TITLE (placeholder visual mentre no hi haja artwork).
# `bg_color` i `text_color` són índexs sobre la paleta "ui" (uzebox.pal):
# 1 = (36,0,0) gairebé negre amb un mínim de càlid; 31 = (255,109,0)
# taronja pur. `duration_ms` és el timeout abans de saltar a la
# següent escena (skipable amb tecla no reservada). Els dos
# `blink_*_ms` defineixen el cicle del prompt "press start": el text
# es veu durant `visible_ms` i s'amaga durant `hidden_ms` repetint-se.
title:
duration_ms: 30000
bg_color: 1
text_color: 31
copyright_color: 246
blink_visible_ms: 800
blink_hidden_ms: 200
# Fase STARTING (entre la primera pulsació de START i el salt a la
# següent escena). `starting_ms` és la durada fixa; durant aquesta
# fase el parpadeig accelera per `starting_blink_speedup` (2.0 = doble
# de freqüència). Si l'altre jugador prem START dins d'aquesta
# finestra, el comptador es reseteja i s'arrenca amb BOTH.
starting_ms: 3000
# Durant STARTING el prompt commuta a un cicle simètric 50/50
# (visible/ocult) amb aquest període total. 200 ms = 5 Hz, prou
# ràpid per llegir-se com a "pulsació confirmada".
starting_blink_period_ms: 200
# Fade-out estil "mosaic dissolve" durant l'última part de STARTING.
# `fade_out_ms` ha de ser <= starting_ms (els últims X ms del STARTING
# es fan amb el dissolve actiu fins a quedar negre). `fade_out_max_block`
# és la mida màxima de bloc en píxels al final del fade.
fade_out_ms: 800
fade_out_max_block: 32
# Fade-out de `title.ogg` en entrar a STARTING. Independent del visual:
# la música pot acabar abans o després del mosaic dissolve.
music_fade_out_ms: 3000
# Fade-to-black per a la sortida d'attract (timeout cap a LOGO). NO és
# mosaic — capa negra lineal damunt del canvas. La música no es toca:
# `title.ogg` continua sonant cap a LOGO, que ja veurà què en fa.
attract_fade_to_black_ms: 800
# Fade-in inicial: capa negra al començament que s'esvaeix cap al fons.
# Mateix valor que `room_intro.fade_in_ms` (data/scenes/game.yaml) perquè
# l'arribada a la TITLE i a una sala de joc tinguin el mateix ritme.
intro_fade_in_ms: 500
# Distorsió sinusoidal del fons (efecte de calor del desert).
# `amplitude_px`: desplaçament màxim de cada fila en píxels.
# `wavelength_px`: distància vertical entre dos pics de l'ona.
# `speed_hz`: velocitat amb què avança la fase (cicles per segon).
# Si `enabled` és false, el fons es pinta sense distorsió.
heat_haze:
enabled: true
amplitude_px: 1.5
wavelength_px: 32.0
speed_hz: 0.6
+2 -2
View File
@@ -22,8 +22,8 @@ real (logo animat, menú de títol, cutscene).
Fitxer personal de desenvolupament, **fora del repo**. Viu a la mateixa carpeta Fitxer personal de desenvolupament, **fora del repo**. Viu a la mateixa carpeta
de sistema que `config.yaml`: de sistema que `config.yaml`:
- Linux (XDG): `$XDG_CONFIG_HOME/jailgames/aee_2026/debug.yaml` - Linux (XDG): `$XDG_CONFIG_HOME/jailgames/aee_arcade/debug.yaml`
- Per defecte: `~/.config/jailgames/aee_2026/debug.yaml` - Per defecte: `~/.config/jailgames/aee_arcade/debug.yaml`
### Comportament segons build ### Comportament segons build
+4 -4
View File
@@ -1,6 +1,6 @@
# Sistema d'entitats # Sistema d'entitats
Registre del disseny, els treballs fets i els pendents per al sistema d'entitats (sarcòfags, porta, momia, antorxa, trampes, roca) d'aee_2026. Material de referència per a decisions futures — no és documentació d'API. Registre del disseny, els treballs fets i els pendents per al sistema d'entitats (sarcòfags, porta, momia, antorxa, trampes, roca) d'aee_arcade. Material de referència per a decisions futures — no és documentació d'API.
Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`. Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`.
@@ -24,7 +24,7 @@ Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`.
## 1. Context i objectiu inicial ## 1. Context i objectiu inicial
aee_2026 és un clon tipus Oh Mummy! amb estètica Amidar. Els sarcòfags eren fins ara rectangles purament geomètrics calculats a [Map::getSarcRect](../source/game/map/map.cpp#L67); no hi havia entitats de joc (momies, antorxes, trampes, portes). aee_arcade és un clon tipus Oh Mummy! amb estètica Amidar. Els sarcòfags eren fins ara rectangles purament geomètrics calculats a [Map::getSarcRect](../source/game/map/map.cpp#L67); no hi havia entitats de joc (momies, antorxes, trampes, portes).
**Objectiu inicial** (prompt de partida): **Objectiu inicial** (prompt de partida):
@@ -158,7 +158,7 @@ Al CMakeLists: `ENTITY_SOURCES` conté els fitxers core; `APP_SOURCES` afegeix `
- `make build`: OK, 0 warnings nous. - `make build`: OK, 0 warnings nous.
- `./build/test_entities_smoke`: **9/9 OK**. - `./build/test_entities_smoke`: **9/9 OK**.
- `ctest --output-on-failure`: 1/1 suite passed. - `ctest --output-on-failure`: 1/1 suite passed.
- `timeout 5 xvfb-run ./build/aee_2026`: 5s sense crash, stdout inclou: - `timeout 5 xvfb-run ./build/aee_arcade`: 5s sense crash, stdout inclou:
``` ```
Surface : torch.gif ... LOADED Surface : torch.gif ... LOADED
Surface : sarcophagus.gif ... LOADED (×2) Surface : sarcophagus.gif ... LOADED (×2)
@@ -323,7 +323,7 @@ Dos events encadenats perquè el *pintat* no és la *presència*: es filtra per
- `make debug` net, sense warnings ni de clang-tidy ni de cppcheck als fitxers nous. - `make debug` net, sense warnings ni de clang-tidy ni de cppcheck als fitxers nous.
- `./build/test_entities_smoke`: **ALL OK** (14/14). T9 adaptat (publica `tile_painted` en lloc d'`on_tile_entered`); la resta de tests del sistema d'entitats passen sense tocar. - `./build/test_entities_smoke`: **ALL OK** (14/14). T9 adaptat (publica `tile_painted` en lloc d'`on_tile_entered`); la resta de tests del sistema d'entitats passen sense tocar.
- `timeout 5 xvfb-run ./build/aee_2026`: arrenca, log `PaintLayer: 21x15 tiles (336x240 px), tileset data/tilesets/tomb.gif` després de `EntityManager: 27 entitats carregades`, surt amb `Bye!` sense crash. - `timeout 5 xvfb-run ./build/aee_arcade`: arrenca, log `PaintLayer: 21x15 tiles (336x240 px), tileset data/tilesets/tomb.gif` després de `EntityManager: 27 entitats carregades`, surt amb `Bye!` sense crash.
### Pendent derivat ### Pendent derivat
+4 -4
View File
@@ -1,7 +1,7 @@
# Pla d'implementació — Menú de servei # Pla d'implementació — Menú de servei
Pla per a portar el menú de servei de `../aee` (`source/core/rendering/menu.cpp`) Pla per a portar el menú de servei de `../aee` (`source/core/rendering/menu.cpp`)
a `aee_2026`, adaptat a l'arquitectura actual (Scenes + Director instanciat + a `aee_arcade`, adaptat a l'arquitectura actual (Scenes + Director instanciat +
`Options` amb setters persistents + `Screen` ARGB + `Text` bitmap). Document de `Options` amb setters persistents + `Screen` ARGB + `Text` bitmap). Document de
referència per a implementar-ho en una sessió futura; no s'ha escrit cap codi referència per a implementar-ho en una sessió futura; no s'ha escrit cap codi
encara. encara.
@@ -33,7 +33,7 @@ Tot el que segueix és la base; la còpia mental serveix per a no reinventar res
## 2. Diferències d'arquitectura rellevants ## 2. Diferències d'arquitectura rellevants
| Punt | `../aee` | `aee_2026` | | Punt | `../aee` | `aee_arcade` |
| ----------------- | ---------------------------------------------------- | -------------------------------------------------------------------------- | | ----------------- | ---------------------------------------------------- | -------------------------------------------------------------------------- |
| Overlay | `Overlay::render()` dibuixa Menú + Notifier al buffer | No hi ha Overlay. Cada Scene crida `Notifier::render()` i `RenderInfo::render()` | | Overlay | `Overlay::render()` dibuixa Menú + Notifier al buffer | No hi ha Overlay. Cada Scene crida `Notifier::render()` i `RenderInfo::render()` |
| Director | Singleton `Director::get()` amb `requestQuit/requestRestart` | Classe instanciada; sortida via `SDL_EVENT_QUIT` a `EventBuffer` | | Director | Singleton `Director::get()` amb `requestQuit/requestRestart` | Classe instanciada; sortida via `SDL_EVENT_QUIT` a `EventBuffer` |
@@ -223,7 +223,7 @@ es necessita.
### Limitació: pàgina Controls diferida ### Limitació: pàgina Controls diferida
El menú de `../aee` té una pàgina Controls (`buildControls`) que permet El menú de `../aee` té una pàgina Controls (`buildControls`) que permet
remappar `up/down/left/right` i la tecla de menú. A `aee_2026`: remappar `up/down/left/right` i la tecla de menú. A `aee_arcade`:
- Les tecles de joc són `Defaults::KeysGame::{UP,DOWN,LEFT,RIGHT}` — constants - Les tecles de joc són `Defaults::KeysGame::{UP,DOWN,LEFT,RIGHT}` — constants
`constexpr`, no mutables. `constexpr`, no mutables.
@@ -273,7 +273,7 @@ y, w, h, argb, alpha)` que opere sobre `canvas_.data()`. El canvas és
fons del menú pot ser `alpha=220` i es veu el joc apagat per darrere. fons del menú pot ser `alpha=220` i es veu el joc apagat per darrere.
Mida fixa recomanada per al MVP: `BOX_W = 220`, altura dinàmica (igual que Mida fixa recomanada per al MVP: `BOX_W = 220`, altura dinàmica (igual que
`../aee`). Canvas lògic d'`aee_2026` és `GameCanvas::WIDTH`/`HEIGHT` — `../aee`). Canvas lògic d'`aee_arcade` és `GameCanvas::WIDTH`/`HEIGHT` —
confirmar mida real (mateixos 320×200?) a `utils/defines.hpp` abans de confirmar mida real (mateixos 320×200?) a `utils/defines.hpp` abans de
hardcoded. hardcoded.
+3 -3
View File
@@ -7,17 +7,17 @@
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Aventures en Egipte ARCADE</string> <string>Aventures en Egipte ARCADE</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>aee_2026</string> <string>aee_arcade</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>icon</string> <string>icon</string>
<key>CFBundleIconName</key> <key>CFBundleIconName</key>
<string>icon</string> <string>icon</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>org.jailgames.aee_2026</string> <string>org.jailgames.aee_arcade</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>aee_2026</string> <string>aee_arcade</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
@@ -1,2 +1,2 @@
// aee_2026.rc // aee_arcade.rc
IDI_ICON1 ICON "release/icons/icon.ico" IDI_ICON1 ICON "release/icons/icon.ico"
+23
View File
@@ -113,6 +113,13 @@ void Audio::stopMusic() {
} }
} }
// NOLINTNEXTLINE(readability-make-member-function-const)
void Audio::setMusicSpeed(float ratio) {
if (music_enabled_) {
engine_->setMusicSpeed(ratio);
}
}
// Reprodueix un so per nom // Reprodueix un so per nom
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const // NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::playSound(const std::string& name, Group group) { void Audio::playSound(const std::string& name, Group group) {
@@ -129,6 +136,22 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
} }
} }
// Variant amb velocitat (i to) escalats. Apliquem el ratio al canal
// just retornat per `playSound`: així el `SDL_AudioStream` recent creat
// processa tot el sample amb el ratio des del primer pull del callback.
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
// la crida al ratio — sense efectes col·laterals.
// NOLINTNEXTLINE(readability-make-member-function-const)
void Audio::playSound(const std::string& name, Group group, float speed) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; }
const int CH = engine_->playSound(sound, 0, static_cast<int>(group));
if (CH >= 0 && speed != 1.0F) {
engine_->setChannelSpeed(CH, speed);
}
}
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no // 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 // 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. // sec — l'usuari sent el so encara que la cua no s'apliqui.
+12 -1
View File
@@ -76,11 +76,22 @@ class Audio {
void stopMusic(); // Aturar completament la música void stopMusic(); // Aturar completament la música
void fadeOutMusic(int milliseconds); // Fosa de sortida de la música (muta globals de Ja) void fadeOutMusic(int milliseconds); // Fosa de sortida de la música (muta globals de Ja)
void setOnMusicEnded(std::function<void()> callback); // Callback disparat quan la pista actual acaba de drenar (CONV-03) void setOnMusicEnded(std::function<void()> callback); // Callback disparat quan la pista actual acaba de drenar (CONV-03)
// Multiplicador de velocitat de la música actual. 1.0 = normal,
// 1.5 = un 50% més ràpid (efecte "chipmunk" — també puja el to).
// Es reseteja a 1.0 implícitament a cada `playMusic`. No-op si no
// hi ha música activa.
void setMusicSpeed(float ratio);
// --- Control de sons --- // --- Control de sons ---
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja) void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja) void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
// Reprodueix un so processat per un efecte definit a data/sounds.yaml // Reprodueix un so amb la velocitat (i to) escalats per `speed`:
// 1.0 = normal, 0.95 ≈ -5% (més greu i lent), 1.05 ≈ +5% (més
// agut i ràpid). Mateixa semàntica que `setMusicSpeed`. Útil per a
// variacions subtils que eviten la fatiga d'escoltar el mateix
// sample idèntic (p.ex. obertures de sarcòfag, picks d'ítems).
void playSound(const std::string& name, Group group, float speed);
// Reprodueix un so processat per un efecte definit a data/config/sounds.yaml
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix // (preset_name busca a SoundEffectsConfig). Si el preset no existeix
// o el motor està al cap de canals amb efecte, fa fallback a playSound // o el motor està al cap de canals amb efecte, fa fallback a playSound
// sec — l'usuari sent el so igualment, sense la cua. // sec — l'usuari sent el so igualment, sense la cua.
+3 -3
View File
@@ -5,10 +5,10 @@
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <iostream>
#include <vector> #include <vector>
#include "core/audio/jail_audio.hpp" #include "core/audio/jail_audio.hpp"
#include "utils/log.hpp"
namespace AudioEffects { namespace AudioEffects {
@@ -46,11 +46,11 @@ namespace AudioEffects {
} else if (spec.format == SDL_AUDIO_U8) { } else if (spec.format == SDL_AUDIO_U8) {
bytes_per_sample = 1; bytes_per_sample = 1;
} else { } else {
std::cerr << "AudioEffects: format de so no suportat (només U8 o S16)\n"; Log::error("AudioEffects", "format de so no suportat (només U8 o S16)");
return false; return false;
} }
if (spec.channels < 1 || spec.channels > 2) { if (spec.channels < 1 || spec.channels > 2) {
std::cerr << "AudioEffects: el so ha de ser mono o estèreo\n"; Log::error("AudioEffects", "el so ha de ser mono o estèreo");
return false; return false;
} }
+11
View File
@@ -286,6 +286,11 @@ namespace Ja {
} }
} }
void Engine::setMusicSpeed(float ratio) {
if (current_music_ == nullptr || current_music_->stream == nullptr) { return; }
SDL_SetAudioStreamFrequencyRatio(current_music_->stream, ratio);
}
void Engine::pauseMusic() { void Engine::pauseMusic() {
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; } if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
@@ -425,6 +430,12 @@ namespace Ja {
return channel; return channel;
} }
void Engine::setChannelSpeed(const int channel, const float ratio) {
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return; }
if (channels_[channel].stream == nullptr) { return; }
SDL_SetAudioStreamFrequencyRatio(channels_[channel].stream, ratio);
}
void Engine::pauseChannel(const int channel) { void Engine::pauseChannel(const int channel) {
forEachTargetChannel(channel, [](Channel& ch) { forEachTargetChannel(channel, [](Channel& ch) {
if (ch.state == ChannelState::PLAYING) { if (ch.state == ChannelState::PLAYING) {
+16
View File
@@ -171,6 +171,14 @@ namespace Ja {
void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1); void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
[[nodiscard]] auto getMusicState() const -> MusicState; [[nodiscard]] auto getMusicState() const -> MusicState;
auto setMusicVolume(float volume) -> float; auto setMusicVolume(float volume) -> float;
// Multiplicador de velocitat de reproducció de la música actual
// via `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal, 2.0 =
// doble velocitat. Cal saber que també puja el to (efecte
// "chipmunk") — és el comportament arcade clàssic dels comptes
// enrere. Cada `playMusic` crea un stream nou amb ratio 1.0,
// així que un canvi de track reseteja la velocitat
// implícitament. No-op si no hi ha música activa.
void setMusicSpeed(float ratio);
// Registra un callback que es disparà quan la música actual acabi de // Registra un callback que es disparà quan la música actual acabi de
// drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de // drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de
// stopMusic, així que el callback pot invocar playMusic sense córrer. // stopMusic, així que el callback pot invocar playMusic sense córrer.
@@ -184,6 +192,14 @@ namespace Ja {
// --- So --- // --- So ---
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int; auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int; auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
// Ajusta la velocitat de reproducció d'un canal actiu via
// `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal. Igual que a
// `setMusicSpeed`, puja/baixa el to junt amb la velocitat
// (efecte "chipmunk"); per a SFX curts arcade és el que volem.
// No-op si el canal no està actiu. Cridar-lo just després de
// `playSound`/`playSoundOnChannel` perquè el ratio cobreixi
// tota la reproducció.
void setChannelSpeed(int channel, float ratio);
// Reproducció amb so processat per un efecte. Retorna el canal // Reproducció amb so processat per un efecte. Retorna el canal
// assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS). // 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 // El sound original només s'usa per consultar el spec/buffer; el
+4 -4
View File
@@ -1,10 +1,10 @@
#include "core/audio/sound_effects_config.hpp" #include "core/audio/sound_effects_config.hpp"
#include <iostream>
#include <string> #include <string>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -27,7 +27,7 @@ auto SoundEffectsConfig::get() -> SoundEffectsConfig& {
void SoundEffectsConfig::load(const std::string& file_path) { void SoundEffectsConfig::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path); auto bytes = Resource::Helper::loadFile(file_path);
if (bytes.empty()) { if (bytes.empty()) {
std::cerr << "SoundEffectsConfig: no s'ha pogut obrir " << file_path << " — cap preset d'efecte disponible\n"; Log::warn("SoundEffectsConfig", "no s'ha pogut obrir {} — cap preset d'efecte disponible", file_path);
return; return;
} }
@@ -60,9 +60,9 @@ void SoundEffectsConfig::load(const std::string& file_path) {
} }
} }
std::cout << "SoundEffectsConfig: " << echoes_.size() << " preset(s) d'eco i " << reverbs_.size() << " de reverb des de " << file_path << '\n'; Log::info("SoundEffectsConfig", "{} preset(s) d'eco i {} de reverb des de {}", echoes_.size(), reverbs_.size(), file_path);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "SoundEffectsConfig: error parsejant " << file_path << ": " << e.what() << '\n'; Log::error("SoundEffectsConfig", "error parsejant {}: {}", file_path, e.what());
} }
} }
+1 -1
View File
@@ -5,7 +5,7 @@
#include "core/audio/jail_audio.hpp" // Per a Ja::EchoParams / Ja::ReverbParams #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 // Catàleg de presets d'efectes carregat des de data/config/sounds.yaml. La capa
// Audio (playSoundWithEcho/playSoundWithReverb) hi accedeix per nom: si el // Audio (playSoundWithEcho/playSoundWithReverb) hi accedeix per nom: si el
// preset no existeix, el so es reprodueix sec amb un avís a stderr. // preset no existeix, el so es reprodueix sec amb un avís a stderr.
// //
+7 -10
View File
@@ -3,12 +3,12 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <fstream> #include <fstream>
#include <iostream>
#include <sstream> #include <sstream>
#include <string_view> #include <string_view>
#include <utility> #include <utility>
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -90,7 +90,7 @@ auto GamepadBindings::get() -> GamepadBindings& {
} }
auto GamepadBindings::defaultButton(Action action) -> int { auto GamepadBindings::defaultButton(Action action) -> int {
// Defaults històrics de l'aee_2026 (vegeu Player::accumulateGamepadMovement // Defaults històrics de l'aee_arcade (vegeu Player::accumulateGamepadMovement
// i les consultes del Director per al menú). // i les consultes del Director per al menú).
switch (action) { switch (action) {
case Action::JUMP: case Action::JUMP:
@@ -167,8 +167,7 @@ void GamepadBindings::load(const std::string& file_path) {
try { try {
auto yaml = fkyaml::node::deserialize(CONTENT); auto yaml = fkyaml::node::deserialize(CONTENT);
if (!yaml.contains("gamepads") || !yaml["gamepads"].is_sequence()) { if (!yaml.contains("gamepads") || !yaml["gamepads"].is_sequence()) {
std::cerr << "GamepadBindings: secció 'gamepads' absent o invàlida a " Log::error("GamepadBindings", "secció 'gamepads' absent o invàlida a {}", file_path);
<< file_path << '\n';
return; return;
} }
for (const auto& entry : yaml["gamepads"]) { for (const auto& entry : yaml["gamepads"]) {
@@ -188,11 +187,9 @@ void GamepadBindings::load(const std::string& file_path) {
} }
configs_.push_back(std::move(cfg)); configs_.push_back(std::move(cfg));
} }
std::cout << "GamepadBindings: " << configs_.size() << " mandos carregats de " Log::info("GamepadBindings", "{} mandos carregats de {}", configs_.size(), file_path);
<< file_path << '\n';
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "GamepadBindings: error parsejant " << file_path << ": " Log::error("GamepadBindings", "error parsejant {}: {}", file_path, e.what());
<< e.what() << '\n';
} }
} }
@@ -200,7 +197,7 @@ auto GamepadBindings::save() -> bool {
if (file_path_.empty()) { return false; } if (file_path_.empty()) { return false; }
std::ostringstream out; std::ostringstream out;
out << "# AEE 2026 - Vinculacions de mando per dispositiu.\n"; out << "# AEE ARCADE - Vinculacions de mando per dispositiu.\n";
out << "# Auto-generat des del menú de servei. Editable a mà.\n"; out << "# Auto-generat des del menú de servei. Editable a mà.\n";
out << "gamepads:\n"; out << "gamepads:\n";
for (const auto& cfg : configs_) { for (const auto& cfg : configs_) {
@@ -215,7 +212,7 @@ auto GamepadBindings::save() -> bool {
std::ofstream file(file_path_, std::ios::binary); std::ofstream file(file_path_, std::ios::binary);
if (!file) { if (!file) {
std::cerr << "GamepadBindings: no es pot escriure " << file_path_ << '\n'; Log::error("GamepadBindings", "no es pot escriure {}", file_path_);
return false; return false;
} }
file << out.str(); file << out.str();
+12 -20
View File
@@ -1,13 +1,13 @@
#include "core/input/gamepad_manager.hpp" #include "core/input/gamepad_manager.hpp"
#include <algorithm> #include <algorithm>
#include <iostream>
#include <iterator> #include <iterator>
#include <string> #include <string>
#include <utility> #include <utility>
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/notification_presets.hpp" #include "core/rendering/notification_presets.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -94,8 +94,7 @@ void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
if (!SDL_IsGamepad(jid)) { return; } if (!SDL_IsGamepad(jid)) { return; }
SDL_Gamepad* pad = SDL_OpenGamepad(jid); SDL_Gamepad* pad = SDL_OpenGamepad(jid);
if (pad == nullptr) { if (pad == nullptr) {
std::cout << "GamepadManager: SDL_OpenGamepad(" << jid Log::error("GamepadManager", "SDL_OpenGamepad({}) ha fallat: {}", jid, SDL_GetError());
<< ") ha fallat: " << SDL_GetError() << '\n';
return; return;
} }
std::string name = uniqueName(prettyName(SDL_GetGamepadName(pad))); std::string name = uniqueName(prettyName(SDL_GetGamepadName(pad)));
@@ -104,8 +103,7 @@ void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
// reconnexió es resoldrà per nom. // reconnexió es resoldrà per nom.
const char* raw_path = SDL_GetGamepadPath(pad); const char* raw_path = SDL_GetGamepadPath(pad);
std::string path = (raw_path != nullptr) ? raw_path : ""; std::string path = (raw_path != nullptr) ? raw_path : "";
std::cout << "GamepadManager: connectat " << name Log::info("GamepadManager", "connectat {}{}", name, path.empty() ? std::string{} : " [" + path + "]");
<< (path.empty() ? "" : " [" + path + "]") << '\n';
entries_.push_back(Entry{.pad = pad, .instance_id = jid, .pretty_name = name, .path = path}); entries_.push_back(Entry{.pad = pad, .instance_id = jid, .pretty_name = name, .path = path});
// Hot-plug: si alguna assignació pendent encaixa amb aquest mando, // Hot-plug: si alguna assignació pendent encaixa amb aquest mando,
@@ -129,7 +127,7 @@ void GamepadManager::removeGamepad(SDL_JoystickID jid) {
std::string name = std::move(it->pretty_name); std::string name = std::move(it->pretty_name);
SDL_Gamepad* pad = it->pad; SDL_Gamepad* pad = it->pad;
if (pad != nullptr) { SDL_CloseGamepad(pad); } if (pad != nullptr) { SDL_CloseGamepad(pad); }
std::cout << "GamepadManager: desconnectat " << name << '\n'; Log::info("GamepadManager", "desconnectat {}", name);
entries_.erase(it); entries_.erase(it);
// Qualsevol assignació que apuntava a aquest mando queda amb `pad` // Qualsevol assignació que apuntava a aquest mando queda amb `pad`
@@ -145,8 +143,7 @@ void GamepadManager::removeGamepad(SDL_JoystickID jid) {
void GamepadManager::init() { void GamepadManager::init() {
if (initialized_) { return; } if (initialized_) { return; }
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) { if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
std::cout << "GamepadManager: SDL_INIT_GAMEPAD ha fallat: " Log::error("GamepadManager", "SDL_INIT_GAMEPAD ha fallat: {}", SDL_GetError());
<< SDL_GetError() << '\n';
return; return;
} }
// `gamecontrollerdb.txt` és opcional. Viu a l'arrel del projecte // `gamecontrollerdb.txt` és opcional. Viu a l'arrel del projecte
@@ -161,10 +158,9 @@ void GamepadManager::init() {
} }
const int ADDED = SDL_AddGamepadMappingsFromFile(db_path.c_str()); const int ADDED = SDL_AddGamepadMappingsFromFile(db_path.c_str());
if (ADDED < 0) { if (ADDED < 0) {
std::cout << "GamepadManager: no s'ha pogut carregar " << db_path Log::warn("GamepadManager", "no s'ha pogut carregar {}: {}", db_path, SDL_GetError());
<< ": " << SDL_GetError() << '\n';
} else { } else {
std::cout << "GamepadManager: " << ADDED << " mappings carregats de " << db_path << '\n'; Log::info("GamepadManager", "{} mappings carregats de {}", ADDED, db_path);
} }
int count = 0; int count = 0;
@@ -175,7 +171,7 @@ void GamepadManager::init() {
} }
SDL_free(ids); SDL_free(ids);
} }
std::cout << "GamepadManager: " << entries_.size() << " mando(s) detectat(s) a l'inici\n"; Log::info("GamepadManager", "{} mando(s) detectat(s) a l'inici", entries_.size());
initialized_ = true; initialized_ = true;
} }
@@ -268,8 +264,7 @@ void GamepadManager::assignToPlayer(int player_id, const std::string& name, cons
if (a.pad != nullptr) { if (a.pad != nullptr) {
for (auto& other : assignments_) { for (auto& other : assignments_) {
if (&other != &a && other.pad == a.pad) { if (&other != &a && other.pad == a.pad) {
std::cout << "GamepadManager: mando " << a.name Log::info("GamepadManager", "mando {} reassignat (last-wins)", a.name);
<< " reassignat (last-wins)\n";
other.pad = nullptr; other.pad = nullptr;
} }
} }
@@ -277,11 +272,9 @@ void GamepadManager::assignToPlayer(int player_id, const std::string& name, cons
// Traç de l'assignació persistida (vinculada o pendent) perquè quede // Traç de l'assignació persistida (vinculada o pendent) perquè quede
// visible a la consola igual que les autoassignacions. // visible a la consola igual que les autoassignacions.
if (a.pad != nullptr) { if (a.pad != nullptr) {
std::cout << "GamepadManager: assignat " << a.name Log::info("GamepadManager", "assignat {} → P{}", a.name, player_id + 1);
<< " → P" << (player_id + 1) << '\n';
} else { } else {
std::cout << "GamepadManager: P" << (player_id + 1) << " pendent (" Log::info("GamepadManager", "P{} pendent ({} no connectat)", player_id + 1, a.name.empty() ? a.path : a.name);
<< (a.name.empty() ? a.path : a.name) << " no connectat)\n";
} }
} }
@@ -307,8 +300,7 @@ auto GamepadManager::autoAssignFreeSlots() -> std::vector<AutoAssigned> {
slot.name = e.pretty_name; slot.name = e.pretty_name;
slot.path = e.path; slot.path = e.path;
assigned.push_back(AutoAssigned{.player_id = pid, .name = e.pretty_name, .path = e.path}); assigned.push_back(AutoAssigned{.player_id = pid, .name = e.pretty_name, .path = e.path});
std::cout << "GamepadManager: auto-assigned " << e.pretty_name Log::info("GamepadManager", "auto-assigned {} → P{}", e.pretty_name, pid + 1);
<< " → P" << (pid + 1) << '\n';
break; break;
} }
} }
+19 -7
View File
@@ -3,6 +3,7 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
#include <format>
#include <string> #include <string>
#include "core/input/key_config.hpp" #include "core/input/key_config.hpp"
@@ -26,7 +27,7 @@ namespace GlobalInputs {
// La lliguem a la durada de la notificació EXIT perquè el missatge i // La lliguem a la durada de la notificació EXIT perquè el missatge i
// la confirmació expiren alhora — si no, l'usuari podria veure el // la confirmació expiren alhora — si no, l'usuari podria veure el
// text però ja no ser dins del rang de confirmació (o al revés). // text però ja no ser dins del rang de confirmació (o al revés).
// El valor concret viu a data/ui.yaml (timings.exit_ms) via UiConfig. // El valor concret viu a data/config/ui.yaml (timings.exit_ms) via UiConfig.
constexpr int MIN_ZOOM = 1; constexpr int MIN_ZOOM = 1;
constexpr int MAX_ZOOM = 6; constexpr int MAX_ZOOM = 6;
@@ -69,15 +70,26 @@ namespace GlobalInputs {
notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off"); notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off");
} }
// Volca el canvas ARGB actual a PNG dins de `screenshots/`. Si ha // Volca el canvas ARGB actual a PNG dins del directori de
// fallat l'escriptura, el handler no mostra notificació (retorn buit). // captures (configurat pel Director). La notificació informa
// del nom i de la ruta absoluta — així l'usuari sap on anar
// a buscar-la encara que el binari s'executi des d'un sitio
// estrany. Si l'escriptura falla, no es mostra notificació.
void onScreenshot() { void onScreenshot() {
const auto* buffer = Screen::get().getCanvasArgb(); const auto* buffer = Screen::get().getCanvasArgb();
const int W = Screen::get().getCanvasWidth(); const int W = Screen::get().getCanvasWidth();
const int H = Screen::get().getCanvasHeight(); const int H = Screen::get().getCanvasHeight();
const std::string FILENAME = Screenshot::save(buffer, W, H); const auto R = Screenshot::save(buffer, W, H);
if (!FILENAME.empty()) { if (!R.filename.empty()) {
Notification::notify(Locale::get().text("input.screenshot") + ' ' + FILENAME); // `make_format_args` retorna un store que conté referències
// als arguments; cal lligar-lo a una lvalue local perquè
// sobrevisqui fins que `vformat` el consumeixi. Passar-lo
// directament a `vformat` és UB en alguns compiladors
// (DR de C++20 / fix de C++23) i ha provocat un segfault
// post-captura en aquest projecte.
const std::string TEMPLATE = Locale::get().text("input.screenshot");
auto args = std::make_format_args(R.filename, R.folder);
Notification::notify(std::vformat(TEMPLATE, args));
} }
} }
@@ -161,7 +173,7 @@ namespace GlobalInputs {
// Debug helper: dispara una notificació prou llarga perquè // Debug helper: dispara una notificació prou llarga perquè
// `wrapToWidth` la parteixi en (almenys) dues línies. Útil per a // `wrapToWidth` la parteixi en (almenys) dues línies. Útil per a
// verificar visualment el padding asimètric, el line_gap i la caixa // verificar visualment el padding asimètric, el line_gap i la caixa
// multilinia després de tocar la geometria a `data/ui.yaml`. // multilinia després de tocar la geometria a `data/config/ui.yaml`.
void onDebugLongNotification() { void onDebugLongNotification() {
Notification::notify("Notificació de prova llarga per a comprovar que el text es parteix correctament en dues línies dins la caixa."); Notification::notify("Notificació de prova llarga per a comprovar que el text es parteix correctament en dues línies dins la caixa.");
} }
+6 -6
View File
@@ -1,11 +1,11 @@
#include "core/input/key_config.hpp" #include "core/input/key_config.hpp"
#include <fstream> #include <fstream>
#include <iostream>
#include <string> #include <string>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
auto KeyConfig::get() -> KeyConfig& { auto KeyConfig::get() -> KeyConfig& {
static KeyConfig instance_; static KeyConfig instance_;
@@ -15,7 +15,7 @@ auto KeyConfig::get() -> KeyConfig& {
void KeyConfig::load(const std::string& file_path) { void KeyConfig::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path); auto bytes = Resource::Helper::loadFile(file_path);
if (bytes.empty()) { if (bytes.empty()) {
std::cerr << "KeyConfig: no s'ha pogut obrir " << file_path << '\n'; Log::error("KeyConfig", "no s'ha pogut obrir {}", file_path);
return; return;
} }
@@ -24,7 +24,7 @@ void KeyConfig::load(const std::string& file_path) {
const auto* end = begin + bytes.size(); const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end); auto yaml = fkyaml::node::deserialize(begin, end);
if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) { if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) {
std::cerr << "KeyConfig: falta la secció 'keys' a " << file_path << '\n'; Log::error("KeyConfig", "falta la secció 'keys' a {}", file_path);
return; return;
} }
bindings_.clear(); bindings_.clear();
@@ -34,14 +34,14 @@ void KeyConfig::load(const std::string& file_path) {
const auto CODE = entry["code"].get_value<std::string>(); const auto CODE = entry["code"].get_value<std::string>();
const SDL_Scancode SC = SDL_GetScancodeFromName(CODE.c_str()); const SDL_Scancode SC = SDL_GetScancodeFromName(CODE.c_str());
if (SC == SDL_SCANCODE_UNKNOWN) { if (SC == SDL_SCANCODE_UNKNOWN) {
std::cerr << "KeyConfig: codi SDL desconegut '" << CODE << "' per id '" << ID << "'\n"; Log::warn("KeyConfig", "codi SDL desconegut '{}' per id '{}'", CODE, ID);
continue; continue;
} }
bindings_[ID] = SC; bindings_[ID] = SC;
} }
std::cout << "KeyConfig: " << bindings_.size() << " bindings des de " << file_path << '\n'; Log::info("KeyConfig", "{} bindings des de {}", bindings_.size(), file_path);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "KeyConfig: error parsejant " << file_path << ": " << e.what() << '\n'; Log::error("KeyConfig", "error parsejant {}: {}", file_path, e.what());
} }
} }
+1 -1
View File
@@ -5,7 +5,7 @@
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
// Mapeig id → SDL_Scancode carregat des de data/input/keys.yaml. Consumit // Mapeig id → SDL_Scancode carregat des de data/config/input.yaml. Consumit
// per GlobalInputs i (en el futur) per menús de remapat. No té cap cost // per GlobalInputs i (en el futur) per menús de remapat. No té cap cost
// global: si la clau no existeix, retorna SDL_SCANCODE_UNKNOWN i GlobalInputs // global: si la clau no existeix, retorna SDL_SCANCODE_UNKNOWN i GlobalInputs
// ignora l'acció. D'aquesta manera podem afegir nous bindings al YAML sense // ignora l'acció. D'aquesta manera podem afegir nous bindings al YAML sense
+5 -5
View File
@@ -1,11 +1,11 @@
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include <fstream> #include <fstream>
#include <iostream>
#include <string> #include <string>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -36,7 +36,7 @@ auto Locale::get() -> Locale& {
void Locale::load(const std::string& file_path) { void Locale::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path); auto bytes = Resource::Helper::loadFile(file_path);
if (bytes.empty()) { if (bytes.empty()) {
std::cerr << "Locale: no s'ha pogut obrir " << file_path << '\n'; Log::error("Locale", "no s'ha pogut obrir {}", file_path);
return; return;
} }
@@ -46,15 +46,15 @@ void Locale::load(const std::string& file_path) {
auto yaml = fkyaml::node::deserialize(begin, end); auto yaml = fkyaml::node::deserialize(begin, end);
strings_.clear(); strings_.clear();
flatten(yaml, "", strings_); flatten(yaml, "", strings_);
std::cout << "Locale: " << strings_.size() << " traduccions des de " << file_path << '\n'; Log::info("Locale", "{} traduccions des de {}", strings_.size(), file_path);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "Locale: error parsejant " << file_path << ": " << e.what() << '\n'; Log::error("Locale", "error parsejant {}: {}", file_path, e.what());
} }
} }
auto Locale::text(const std::string& key) const -> std::string { auto Locale::text(const std::string& key) const -> std::string {
auto it = strings_.find(key); auto it = strings_.find(key);
if (it != strings_.end()) { return it->second; } if (it != strings_.end()) { return it->second; }
std::cerr << "Locale: clau no trobada: " << key << '\n'; Log::warn("Locale", "clau no trobada: {}", key);
return key; return key;
} }
@@ -0,0 +1,219 @@
#include "core/rendering/effects/scan_reveal.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <numbers>
#include <random>
#include "core/rendering/screen.hpp"
#include "core/rendering/surface.hpp"
namespace Effects {
namespace {
// Hash determinista per a (row, time_bucket): retorna float a
// [-1, 1]. Usat per generar el jitter horitzontal de la banda
// sense necessitat d'un RNG d'estat (cada frame el resultat
// canvia perquè el time_bucket canvia, però dins d'un mateix
// bucket dues files diferents donen valors distints).
auto rowJitter(int row, int time_bucket) -> float {
const auto SEED = static_cast<std::uint32_t>((row * 73856093) ^ (time_bucket * 19349663));
// Mix barato (xorshift) per descorrelacionar bits.
std::uint32_t x = SEED;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
// Normalitzem a [-1, 1].
const float NORM = static_cast<float>(x) / static_cast<float>(UINT32_MAX);
return (NORM * 2.0F) - 1.0F;
}
// Bucket de temps per al jitter: agrupant en blocs de ~50ms el
// jitter "salta" 20 cops per segon — visualment "viu" sense
// ser caos pur a 60fps.
constexpr int JITTER_TIME_BUCKET_MS = 50;
} // namespace
ScanReveal::ScanReveal()
: rng_(std::random_device{}()) {}
void ScanReveal::begin(const Params& params) {
params_ = params;
elapsed_ms_ = 0;
flash_remaining_ms_ = 0;
flash_started_ = false;
burst_remaining_ms_ = 0;
burst_y_norm_ = 0.0F;
burst_dx_ = 0;
tail_started_ = false;
tail_remaining_ms_ = 0;
}
void ScanReveal::tick(int delta_ms) {
if (params_.duration_ms <= 0) { return; }
// 1) Avanç del scan (clampejat a duration_ms).
elapsed_ms_ = std::min(elapsed_ms_ + delta_ms, params_.duration_ms);
// 2) Tick del burst VHS actiu, si n'hi ha.
if (burst_remaining_ms_ > 0) {
burst_remaining_ms_ = std::max(0, burst_remaining_ms_ - delta_ms);
}
// 3) Spawn d'un nou burst. Permès durant el scan i durant el
// glitch tail post-flash; bloquejat exactament durant el
// flash perquè aquell és un punt visual de tancament net.
// Probabilitat per frame ≈ (per_sec/1000) · delta_ms.
const bool TAIL_ACTIVE = tail_started_ && tail_remaining_ms_ > 0;
const bool SCAN_ONGOING = elapsed_ms_ < params_.duration_ms;
if (burst_remaining_ms_ == 0 && (SCAN_ONGOING || TAIL_ACTIVE) && params_.vhs_burst_per_sec > 0 && params_.vhs_burst_duration_ms > 0) {
const float P = (static_cast<float>(params_.vhs_burst_per_sec) / 1000.0F) * static_cast<float>(delta_ms);
std::uniform_real_distribution<float> dist01(0.0F, 1.0F);
if (dist01(rng_) < P) {
burst_remaining_ms_ = params_.vhs_burst_duration_ms;
burst_y_norm_ = dist01(rng_);
std::uniform_int_distribution<int> dist_sign(0, 1);
const int SIGN = (dist_sign(rng_) == 0) ? 1 : -1;
burst_dx_ = SIGN * params_.vhs_burst_amplitude_px;
}
}
// 4) Dispara el flash quan el scan acaba (una sola vegada).
if (!flash_started_ && elapsed_ms_ >= params_.duration_ms && params_.flash_ms > 0) {
flash_started_ = true;
flash_remaining_ms_ = params_.flash_ms;
}
// Decrement del flash mentre estiga actiu.
if (flash_remaining_ms_ > 0) {
flash_remaining_ms_ = std::max(0, flash_remaining_ms_ - delta_ms);
}
// 5) Glitch tail: arranca un sol cop quan el scan ha acabat i el
// flash s'ha esgotat. Si no hi havia flash configurat, també
// arranca tot just acabar el scan.
if (!tail_started_ && elapsed_ms_ >= params_.duration_ms && flash_remaining_ms_ <= 0 && params_.tail_glitch_ms > 0) {
tail_started_ = true;
tail_remaining_ms_ = params_.tail_glitch_ms;
}
if (tail_remaining_ms_ > 0) {
tail_remaining_ms_ = std::max(0, tail_remaining_ms_ - delta_ms);
}
}
auto ScanReveal::isActive() const -> bool {
return params_.duration_ms > 0 && !isFinished();
}
auto ScanReveal::isFinished() const -> bool {
if (params_.duration_ms <= 0) { return true; }
// Acabat = scan al final + flash exhaurit + glitch tail
// exhaurit (o sense tail configurat).
const bool SCAN_DONE = elapsed_ms_ >= params_.duration_ms;
const bool FLASH_DONE = flash_remaining_ms_ <= 0;
const bool TAIL_DONE = (params_.tail_glitch_ms <= 0) || (tail_started_ && tail_remaining_ms_ <= 0);
return SCAN_DONE && FLASH_DONE && TAIL_DONE;
}
void ScanReveal::render(const Surface& src, int x, int y) const {
if (params_.duration_ms <= 0) {
// Sense efecte: render pla.
src.render(x, y);
return;
}
const int H = src.getHeight();
const auto W_F = static_cast<float>(src.getWidth());
const float T = static_cast<float>(elapsed_ms_) / static_cast<float>(params_.duration_ms);
// Banda: comença a y=-band_height i acaba a y=H. Així al t=0 cap
// fila és visible i al t=1 totes ho són.
const float SWEEP_F = -static_cast<float>(params_.band_height_px) + (T * static_cast<float>(H + params_.band_height_px));
const int BAND_TOP = static_cast<int>(std::floor(SWEEP_F));
const int BAND_BOT = BAND_TOP + params_.band_height_px;
const int TIME_BUCKET = elapsed_ms_ / JITTER_TIME_BUCKET_MS;
const int BURST_Y_PX = static_cast<int>(burst_y_norm_ * static_cast<float>(H));
const bool BURST_ACTIVE = burst_remaining_ms_ > 0;
// Durant el glitch tail post-flash, BAND_TOP queda just damunt
// del peu del logo, fet que (amb el codi de jitter que segueix)
// generaria jitter residual a les últimes files. Aquí el
// suprimim explícitament: el tail només viu de bursts VHS.
const bool TAIL_ACTIVE = tail_started_ && tail_remaining_ms_ > 0;
// Render fila a fila (només files revelades).
for (int row = 0; row <= BAND_BOT && row < H; ++row) {
int dx = 0;
// Jitter de banda: amplitud màxima a la banda, falloff
// lineal cap als marges del radi de glitch. Inactiu durant
// el glitch tail (no hi ha banda).
const int DIST_TO_BAND = std::abs(row - BAND_TOP);
if (!TAIL_ACTIVE && params_.glitch_band_radius_px > 0 && DIST_TO_BAND <= params_.glitch_band_radius_px && params_.glitch_amplitude_px > 0) {
const float FALLOFF = 1.0F - (static_cast<float>(DIST_TO_BAND) / static_cast<float>(params_.glitch_band_radius_px));
const float JIT = rowJitter(row, TIME_BUCKET);
dx += static_cast<int>(std::lround(
FALLOFF * static_cast<float>(params_.glitch_amplitude_px) * JIT));
}
// Burst VHS: només damunt la part ja revelada (row < BAND_TOP)
// perquè per sota encara no hi ha imatge "estable" que poder
// distorsionar visualment.
if (BURST_ACTIVE && row < BAND_TOP && row >= BURST_Y_PX && row < BURST_Y_PX + params_.vhs_burst_height_px) {
dx += burst_dx_;
}
SDL_FRect row_rect{0.0F, static_cast<float>(row), W_F, 1.0F};
src.render(x + dx, y + row, &row_rect);
}
auto& screen = Screen::get();
// Capa lluminosa de la banda. Verticalment només cobreix les
// files del logo (és on viu el scan), però horitzontalment
// travessa tot el canvas com un feix CRT, així sobre fons negre
// no es veuen les costures del rectangle del Surface. L'alpha
// base es modula amb un pulse sinusoidal a `band_pulse_hz` per
// simular el ritme de refresc d'un CRT.
if (params_.band_alpha > 0 && params_.band_height_px > 0) {
const int VISIBLE_TOP = std::clamp(BAND_TOP, 0, H);
const int VISIBLE_BOT = std::clamp(BAND_BOT, 0, H);
const int VISIBLE_H = VISIBLE_BOT - VISIBLE_TOP;
if (VISIBLE_H > 0) {
// Pulse: fracció ∈ [0,1] avançada a la freqüència donada,
// mapejada a un multiplicador ∈ [1-depth, 1] sobre l'alpha
// base. Amb depth=0 el pulse és inactiu (multiplicador 1).
const float PULSE_DEPTH = std::clamp(params_.band_pulse_depth, 0.0F, 1.0F);
const float TWO_PI = 2.0F * std::numbers::pi_v<float>;
const float PHASE = TWO_PI * params_.band_pulse_hz * (static_cast<float>(elapsed_ms_) / 1000.0F);
const float WAVE = 0.5F + (0.5F * std::sin(PHASE)); // 0..1
const float MULT = (1.0F - PULSE_DEPTH) + (PULSE_DEPTH * WAVE);
const float ALPHA_F = static_cast<float>(params_.band_alpha) * MULT;
const auto ALPHA = static_cast<std::uint8_t>(
std::clamp(static_cast<int>(std::round(ALPHA_F)), 0, 255));
if (ALPHA > 0) {
screen.blendRect(0, y + VISIBLE_TOP, screen.getCanvasWidth(), VISIBLE_H, 0xFFFFFFFFU, ALPHA);
}
}
}
// Flash final: capa blanca damunt de TOT el canvas (no només
// del rectangle del logo) — així el flash banya l'escena
// sencera com un CRT i no es veuen les costures del Surface
// sobre fons negre. Alpha proporcional a `flash_remaining_ms_
// / flash_ms` (decreix suau cap a 0 al final del flash).
if (flash_remaining_ms_ > 0 && params_.flash_ms > 0) {
const float FLASH_T = static_cast<float>(flash_remaining_ms_) / static_cast<float>(params_.flash_ms);
const auto ALPHA = static_cast<std::uint8_t>(
std::round(255.0F * std::clamp(FLASH_T, 0.0F, 1.0F)));
if (ALPHA > 0) {
screen.blendRect(0, 0, screen.getCanvasWidth(), screen.getCanvasHeight(), 0xFFFFFFFFU, ALPHA);
}
}
}
} // namespace Effects
@@ -0,0 +1,96 @@
#pragma once
#include <cstdint>
#include <random>
class Surface;
namespace Effects {
// Revelat tipus "scanline CRT" d'una Surface. Una banda lluminosa
// baixa des de dalt cap a baix de la imatge: les files damunt de la
// banda es renderitzen normalment (ja revelades), les files sota
// queden invisibles, i a la banda mateixa s'aplica un overlay
// blanc semi-transparent. Al voltant de la banda les files reben un
// jitter horitzontal aleatori (falloff lineal amb la distància) i
// damunt la part ja revelada apareixen ocasionalment "bursts VHS" —
// franges horitzontals desplaçades durant uns ms — per a un toc
// demoscene/VHS deteriorat.
//
// Quan la banda surt per sota del logo i s'esgota `duration_ms`,
// dispara un flash blanc breu. Si `tail_glitch_ms > 0`, després del
// flash el logo queda totalment visible mentre els bursts VHS
// continuen saltant durant aquest temps (glitch tail). Quan el tail
// s'exhaureix `isFinished()=true`. El caller decideix què fa amb el
// control de fase a partir d'aquí.
//
// Ús:
// ScanReveal sr;
// sr.begin({.duration_ms = 1500, ...});
// // cada frame:
// sr.tick(delta_ms);
// sr.render(*logo, x, y);
// if (sr.isFinished()) { ... canvi de fase ... }
class ScanReveal {
public:
struct Params {
int duration_ms;
int band_height_px;
int band_alpha;
float band_pulse_hz;
float band_pulse_depth;
int glitch_amplitude_px;
int glitch_band_radius_px;
int vhs_burst_per_sec;
int vhs_burst_duration_ms;
int vhs_burst_height_px;
int vhs_burst_amplitude_px;
int flash_ms;
// Glitch tail post-flash: el logo queda visible sencer i els
// bursts VHS continuen saltant durant aquest temps. 0 ho
// desactiva (efecte acaba al flash).
int tail_glitch_ms;
};
ScanReveal();
void begin(const Params& params);
void tick(int delta_ms);
// Renderitza un frame del revelat sobre la Surface `src`. El
// caller s'encarrega de pintar negre per sota (les files no
// revelades simplement no es dibuixen — queda el que hi haja al
// canvas, normalment negre).
void render(const Surface& src, int x, int y) const;
[[nodiscard]] auto isActive() const -> bool;
[[nodiscard]] auto isFinished() const -> bool;
private:
Params params_{};
int elapsed_ms_{0};
// Flash post-scan: arranca quan el scan toca fons; decreix amb
// `delta_ms` fins a 0. Mentre `flash_active_` és true encara no
// hem reportat finished (donant marge per pintar el flash sencer).
int flash_remaining_ms_{0};
bool flash_started_{false};
// Burst VHS actiu (com a màxim un alhora — simplifica i ja és
// suficient per al ritme visual). y_norm ∈ [0,1] es resol en
// píxels al `render()` perquè aquí no coneixem l'alçada del logo.
int burst_remaining_ms_{0};
float burst_y_norm_{0.0F};
int burst_dx_{0};
// Glitch tail post-flash: arranca quan el flash s'esgota i
// permet que els bursts continuen saltant amb el logo ja del tot
// visible. `isFinished()` espera al final d'aquest tail.
bool tail_started_{false};
int tail_remaining_ms_{0};
// RNG per als bursts. Seed des de random_device a `begin()`
// perquè cada execució sigui visualment diferent.
std::minstd_rand rng_;
};
} // namespace Effects
+43
View File
@@ -0,0 +1,43 @@
#include "core/rendering/effects/shadow.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp"
namespace Effects::Shadow {
void render(int center_x, int feet_y, float height_factor) {
// Lookup únic al primer ús: la Surface viu a Cache durant tota la
// sessió, així evitem la cerca lineal a cada frame.
static const auto SURFACE = Resource::Cache::get().getSurface(CACHE_KEY);
const int FRAME_COUNT = SURFACE->getWidth() / FRAME_SIZE;
if (FRAME_COUNT <= 0) { return; }
// Mapping a bandes uniformes: factor 0 → últim (gran), factor 1 →
// primer (petit). Per a 3 fotogrames: [0, 1/3) → 2, [1/3, 2/3) → 1,
// [2/3, 1] → 0. Generalitza a qualsevol nombre de fotogrames per
// si afegim mides intermèdies.
const float CLAMPED = std::clamp(height_factor, 0.0F, 1.0F);
int frame_index = static_cast<int>((1.0F - CLAMPED) * static_cast<float>(FRAME_COUNT));
frame_index = std::clamp(frame_index, 0, FRAME_COUNT - 1);
SDL_FRect clip{
static_cast<float>(frame_index * FRAME_SIZE),
0.0F,
static_cast<float>(FRAME_SIZE),
static_cast<float>(FRAME_SIZE),
};
// Ancoratge: vora inferior del fotograma a `feet_y` — així el
// quadrat 16x16 del sprite encaixa exactament al 16x16 inferior
// d'una entitat 16x16/16x32 centrada en `center_x`.
const int X = center_x - (FRAME_SIZE / 2);
const int Y = feet_y - FRAME_SIZE;
SURFACE->render(X, Y, &clip);
}
} // namespace Effects::Shadow
Loaded 100 of 193 files, more files were not shown because too many files have changed in this diff. Show more