Compare commits

..
60 Commits
Author SHA1 Message Date
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
146 changed files with 3686 additions and 1050 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/
+15 -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)
@@ -94,6 +94,7 @@ set(APP_SOURCES
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/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
@@ -104,18 +105,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
@@ -218,8 +223,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 ---
@@ -361,9 +368,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
@@ -389,9 +398,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
) )
@@ -416,6 +428,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"
@@ -455,6 +468,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.
File renamed without changes.
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.
@@ -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
File renamed without changes.
+133
View File
@@ -0,0 +1,133 @@
# 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
# 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
# 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/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.

Before

Width:  |  Height:  |  Size: 306 B

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

+10
View File
@@ -36,10 +36,17 @@ animations:
walk_north: { frames: [4, 5, 6, 7], fps: 8, loop: true } walk_north: { frames: [4, 5, 6, 7], fps: 8, loop: true }
walk_east: { frames: [8, 9, 10, 11], fps: 8, loop: true } walk_east: { frames: [8, 9, 10, 11], fps: 8, loop: true }
walk_west: { frames: [8, 9, 10, 11], fps: 8, loop: true, flip_h: true } walk_west: { frames: [8, 9, 10, 11], fps: 8, loop: true, flip_h: true }
raise: { frames: [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27], fps: 8 , 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 +65,6 @@ 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
File renamed without changes.
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,7 +133,7 @@ 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:
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,7 +133,7 @@ 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:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 1.6 KiB

+14 -2
View File
@@ -20,11 +20,23 @@ flags:
interactable: true interactable: true
animations: animations:
closed: { frames: [0], fps: 1, loop: true } closed: { frames: [0], fps: 1, loop: true }
opening: { frames: [0], fps: 6, loop: false } opening: { frames: [1, 2], fps: 6, loop: false }
open: { frames: [0], fps: 1, loop: true } open: { frames: [3], fps: 1, 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
Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

+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

+20 -2
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"
@@ -38,6 +38,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 +60,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 +75,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 +87,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 +116,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 -2
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"
@@ -38,6 +38,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 +59,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 +74,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 +86,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 +115,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:
-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 > 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/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.
File renamed without changes.
+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
File renamed without changes.
-217
View File
@@ -1,217 +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
# 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
# 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"
# Soroll/scratch sincronitzat amb la fase SCAN de LogoScene (scanline
# reveal). Es dispara al constructor de l'escena.
logo_glitch: "glitch.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
# 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
# Escena LOGO (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
+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;
} }
+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
+25
View File
@@ -0,0 +1,25 @@
#pragma once
namespace Effects::Shadow {
// Mida de cada fotograma del sprite compartit de la sombra.
// El sprite és una tira horitzontal de fotogrames de 16x16, ordenats
// del més petit (índex 0) al més gran (últim índex).
constexpr int FRAME_SIZE = 16;
// Clau amb què `Resource::Cache` té carregada la Surface compartida.
// El precarrega el Director al boot.
constexpr const char* CACHE_KEY = "shadow";
// Pinta la sombra al canvas. `height_factor` selecciona el fotograma:
// 0.0 (entitat al terra) → fotograma més gran, 1.0 (pic del salt o
// entitat més alta del terra) → fotograma més petit. La tira es
// divideix en bandes uniformes; valors fora de [0, 1] es clampen.
//
// El fotograma s'ancora al quadrat 16x16 inferior d'una entitat de
// 16x16/16x32 centrada en `center_x` amb peus a `feet_y`: la vora
// inferior del sprite queda exactament a `feet_y`. La posició
// visual de l'oval dins del fotograma la decideix l'artista al GIF.
void render(int center_x, int feet_y, float height_factor);
} // namespace Effects::Shadow
+6 -7
View File
@@ -1,10 +1,11 @@
#include "core/rendering/gif.hpp" #include "core/rendering/gif.hpp"
#include <cstring> #include <cstring>
#include <iostream>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include "utils/log.hpp"
namespace GIF { namespace GIF {
namespace { namespace {
@@ -111,7 +112,7 @@ namespace GIF {
while (cur_code != -1) { while (cur_code != -1) {
out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte; out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte;
if (dictionary[cur_code].prev == cur_code) { if (dictionary[cur_code].prev == cur_code) {
std::cerr << "Internal error; self-reference detected." << '\n'; Log::error("GIF", "internal error; self-reference detected.");
throw std::runtime_error("Internal error in decompress: self-reference"); throw std::runtime_error("Internal error in decompress: self-reference");
} }
cur_code = dictionary[cur_code].prev; cur_code = dictionary[cur_code].prev;
@@ -155,8 +156,7 @@ namespace GIF {
if (prev > -1 && code_length < LZW_MAX_CODE_LENGTH) { if (prev > -1 && code_length < LZW_MAX_CODE_LENGTH) {
if (code > dictionary_ind) { if (code > dictionary_ind) {
std::cerr << "code = " << std::hex << code Log::error("GIF", "code = {:x}, but dictionary_ind = {}", code, dictionary_ind);
<< ", but dictionary_ind = " << dictionary_ind << '\n';
throw std::runtime_error("LZW error: code exceeds dictionary_ind."); throw std::runtime_error("LZW error: code exceeds dictionary_ind.");
} }
@@ -167,8 +167,7 @@ namespace GIF {
// Verifica que 'code' sea un índice válido antes de usarlo. // Verifica que 'code' sea un índice válido antes de usarlo.
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) { if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
std::cerr << "Invalid LZW code " << code Log::error("GIF", "invalid LZW code {}, dictionary size {}", code, dictionary.size());
<< ", dictionary size " << dictionary.size() << '\n';
throw std::runtime_error("LZW error: invalid code encountered"); throw std::runtime_error("LZW error: invalid code encountered");
} }
@@ -287,7 +286,7 @@ namespace GIF {
} else if (block_type == IMAGE_DESCRIPTOR) { } else if (block_type == IMAGE_DESCRIPTOR) {
return processImageDescriptor(buffer); return processImageDescriptor(buffer);
} else { } else {
std::cerr << "Unrecognized block type " << std::hex << static_cast<int>(block_type) << '\n'; Log::error("GIF", "unrecognized block type {:x}", static_cast<int>(block_type));
return std::vector<uint8_t>{}; return std::vector<uint8_t>{};
} }
block_type = *buffer++; block_type = *buffer++;
@@ -9,7 +9,7 @@
// Temes per a les notificacions de pantalla. Un "preset" captura els dos // Temes per a les notificacions de pantalla. Un "preset" captura els dos
// colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat // colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat
// en mil·lisegons. Els valors concrets viuen a data/ui.yaml (carregat per // en mil·lisegons. Els valors concrets viuen a data/config/ui.yaml (carregat per
// UiConfig); per llançar una notificació amb un preset concret, cridar // UiConfig); per llançar una notificació amb un preset concret, cridar
// Notification::notify(text, preset). Sense preset s'usa el DEFAULT. // Notification::notify(text, preset). Sense preset s'usa el DEFAULT.
namespace Notification { namespace Notification {
@@ -22,7 +22,7 @@ namespace Notification {
// Preset per defecte: text clar sobre fons fosc. Es construeix en cada // Preset per defecte: text clar sobre fons fosc. Es construeix en cada
// crida a partir d'UiConfig perquè els valors no són constexpr: viuen a // crida a partir d'UiConfig perquè els valors no són constexpr: viuen a
// data/ui.yaml i es carreguen a l'arrencada. // data/config/ui.yaml i es carreguen a l'arrencada.
[[nodiscard]] inline auto defaultPreset() -> Preset { [[nodiscard]] inline auto defaultPreset() -> Preset {
return { return {
.text_color = UiConfig::get().notificationText(), .text_color = UiConfig::get().notificationText(),
+48 -21
View File
@@ -12,11 +12,52 @@
namespace { namespace {
// Parteix `text` en una llista de línies que caben cadascuna dins // Parteix `text` en una llista de línies que caben cadascuna dins
// `max_width`. Algoritme de projecte_2026: tall per espais, la paraula // Trenca una "paraula" llarga (sense espais) en trossos contigus
// que no cap passa a la línia següent. Les paraules aïllades més // que càpiguen a `max_width`. Si un sol caràcter ja excedeix
// llargues que `max_width` es queden a la seva pròpia línia sense // (font enorme), el deixa sol per garantir progrés. La part final
// trencar-les (cas improbable amb els pretty_names de gamepad retallats // que encara cap queda a `current` per a fusionar-se amb paraules
// a ~25 caràcters). // posteriors via espai.
void hardWrapWord(const std::string& word, int max_width, const Text& font, int kerning, std::vector<std::string>& lines, std::string& current) {
std::string buf;
for (char c : word) {
std::string candidate = buf;
candidate += c;
if (buf.empty() || font.length(candidate, kerning) <= max_width) {
buf = std::move(candidate);
} else {
lines.push_back(buf);
buf = std::string(1, c);
}
}
current = std::move(buf);
}
// Posa la `word` a la `current` line si hi cap (amb separador
// implícit si current no està buida), o fa flush + reintenta a
// línia neta. Si tampoc cap sola, hard-wrap caràcter a caràcter.
void appendWord(const std::string& word, int max_width, const Text& font, int kerning, std::vector<std::string>& lines, std::string& current) {
if (!current.empty()) {
std::string candidate = current;
candidate += ' ';
candidate += word;
if (font.length(candidate, kerning) <= max_width) {
current = std::move(candidate);
return;
}
lines.push_back(current);
current.clear();
}
if (font.length(word, kerning) <= max_width) {
current = word;
} else {
hardWrapWord(word, max_width, font, kerning, lines, current);
}
}
// `max_width`. Tall per espais; les paraules més llargues que
// `max_width` (rutes, URLs...) es trenquen caràcter a caràcter
// — sense això, una caixa que excedís el canvas faria que
// `fillRect` rebés rangs invertits i petés.
auto wrapToWidth(const std::string& text, int max_width, const Text& font, int kerning) -> std::vector<std::string> { auto wrapToWidth(const std::string& text, int max_width, const Text& font, int kerning) -> std::vector<std::string> {
std::vector<std::string> lines; std::vector<std::string> lines;
if (text.empty()) { if (text.empty()) {
@@ -30,26 +71,12 @@ namespace {
const std::size_t SPACE_POS = text.find(' ', i); const std::size_t SPACE_POS = text.find(' ', i);
const bool LAST = (SPACE_POS == std::string::npos); const bool LAST = (SPACE_POS == std::string::npos);
const std::string WORD = text.substr(i, LAST ? std::string::npos : SPACE_POS - i); const std::string WORD = text.substr(i, LAST ? std::string::npos : SPACE_POS - i);
appendWord(WORD, max_width, font, kerning, lines, current);
if (current.empty()) {
current = WORD;
} else {
std::string candidate = current;
candidate += ' ';
candidate += WORD;
if (font.length(candidate, kerning) <= max_width) {
current = std::move(candidate);
} else {
lines.push_back(current);
current = WORD;
}
}
if (LAST) { break; } if (LAST) { break; }
i = SPACE_POS + 1; i = SPACE_POS + 1;
} }
lines.push_back(current); if (!current.empty()) { lines.push_back(current); }
return lines; return lines;
} }
+9 -3
View File
@@ -6,13 +6,13 @@
#include <cstddef> #include <cstddef>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <iostream>
#include <memory> #include <memory>
#include <ranges> #include <ranges>
#include "core/rendering/canvas.hpp" #include "core/rendering/canvas.hpp"
#include "core/rendering/sdl3gpu/viewport.hpp" #include "core/rendering/sdl3gpu/viewport.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -112,11 +112,11 @@ void Screen::createSdl() {
backend->setVSync(config_.vsync); backend->setVSync(config_.vsync);
if (backend->init(window_, canvas_width_, canvas_height_)) { if (backend->init(window_, canvas_width_, canvas_height_)) {
gpu_driver_ = backend->getDriverName(); gpu_driver_ = backend->getDriverName();
std::cout << "Screen: path GPU actiu (driver: " << gpu_driver_ << ")\n"; Log::info("Screen", "path GPU actiu (driver: {})", gpu_driver_);
shader_backend_ = std::move(backend); shader_backend_ = std::move(backend);
return; return;
} }
std::cerr << "Screen: GPU no disponible, fallback a SDL_Renderer\n"; Log::warn("Screen", "GPU no disponible, fallback a SDL_Renderer");
} }
gpu_driver_ = "sdl"; gpu_driver_ = "sdl";
@@ -226,6 +226,12 @@ void Screen::fillRect(int x, int y, int w, int h, std::uint32_t argb) {
const int Y0 = std::max(y, CLIP.y); const int Y0 = std::max(y, CLIP.y);
const int X1 = std::min(x + w, CLIP.x + CLIP.w); const int X1 = std::min(x + w, CLIP.x + CLIP.w);
const int Y1 = std::min(y + h, CLIP.y + CLIP.h); const int Y1 = std::min(y + h, CLIP.y + CLIP.h);
// Bail-out defensiu: si el rang és buit o invertit després del clip,
// `std::fill(first, last)` amb `first > last` és UB i a la pràctica
// entra en bucle infinit (vist en un crash a l'overflow del Notifier
// quan la caixa quedava més ampla que el canvas). El `blendRect`
// d'aquí baix ja és segur perquè usa `for(ix; ix < X1; ...)`.
if (X1 <= X0 || Y1 <= Y0) { return; }
for (int iy = Y0; iy < Y1; ++iy) { for (int iy = Y0; iy < Y1; ++iy) {
std::uint32_t* row = canvas_.data() + (static_cast<std::ptrdiff_t>(iy) * canvas_width_); std::uint32_t* row = canvas_.data() + (static_cast<std::ptrdiff_t>(iy) * canvas_width_);
std::fill(row + X0, row + X1, argb); std::fill(row + X0, row + X1, argb);
+32 -20
View File
@@ -5,6 +5,7 @@
#include <ctime> #include <ctime>
#include <filesystem> #include <filesystem>
#include <string> #include <string>
#include <utility>
#include <vector> #include <vector>
#include "external/stb_image_write.h" #include "external/stb_image_write.h"
@@ -13,21 +14,18 @@ namespace Screenshot {
namespace { namespace {
// Crea la carpeta `screenshots/` si no existeix i torna la ruta // Estat de mòdul: ruta base on s'escriu la subcarpeta
// completa al fitxer. El paràmetre `filename` rep el nom nu (sense // `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback
// directori) per poder mostrar-lo a la notificació. // si el Director no ha pogut establir la ruta per plataforma).
auto generateFilePath(std::string& filename) -> std::string { std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables)
std::filesystem::create_directories("screenshots");
const auto NOW = std::chrono::system_clock::now(); // Construeix la ruta del directori on van les captures:
const std::time_t TIME = std::chrono::system_clock::to_time_t(NOW); // `<base>/screenshots/`. Si la base és buida, retorna
const std::tm* tm = std::localtime(&TIME); // "screenshots/" relativa al CWD.
auto screenshotsDir() -> std::string {
char timestamp[20]; if (g_base_dir.empty()) { return "screenshots/"; }
std::strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm); const bool ENDS_WITH_SEP = (g_base_dir.back() == '/' || g_base_dir.back() == '\\');
return g_base_dir + (ENDS_WITH_SEP ? "" : "/") + "screenshots/";
filename = std::string("scr_") + timestamp + ".png";
return "screenshots/" + filename;
} }
// Converteix ARGB8888 → RGBA8888 in-place i escriu el PNG. stb_image_write // Converteix ARGB8888 → RGBA8888 in-place i escriu el PNG. stb_image_write
@@ -47,14 +45,28 @@ namespace Screenshot {
} // namespace } // namespace
auto save(const std::uint32_t* buffer, int width, int height) -> std::string { void setBaseDir(std::string dir) {
g_base_dir = std::move(dir);
}
auto save(const std::uint32_t* buffer, int width, int height) -> Result {
const std::string FOLDER = screenshotsDir();
std::error_code ec;
std::filesystem::create_directories(FOLDER, ec);
const auto NOW = std::chrono::system_clock::now();
const std::time_t TIME = std::chrono::system_clock::to_time_t(NOW);
const std::tm* tm = std::localtime(&TIME);
char timestamp[20];
std::strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm);
const std::string FILENAME = std::string("scr_") + timestamp + ".png";
const std::string FILEPATH = FOLDER + FILENAME;
// Còpia local per no tocar el canvas original durant la conversió. // Còpia local per no tocar el canvas original durant la conversió.
std::vector<std::uint32_t> copy(buffer, buffer + (static_cast<size_t>(width) * height)); std::vector<std::uint32_t> copy(buffer, buffer + (static_cast<size_t>(width) * height));
if (!writePng(copy.data(), width, height, FILEPATH)) { return {}; }
std::string filename; return {.filename = FILENAME, .folder = FOLDER};
const std::string FILEPATH = generateFilePath(filename);
return writePng(copy.data(), width, height, FILEPATH) ? filename : "";
} }
} // namespace Screenshot } // namespace Screenshot
+23 -4
View File
@@ -3,11 +3,30 @@
#include <cstdint> #include <cstdint>
#include <string> #include <string>
// Volca un buffer ARGB8888 a un fitxer PNG dins de la carpeta `screenshots/` // Volca un buffer ARGB8888 a un fitxer PNG dins de la subcarpeta
// (relativa al CWD). El nom es genera amb un timestamp i la funció retorna // `screenshots/` del directori configurat amb `setBaseDir`. El nom es
// el nom del fitxer creat, o una cadena buida si l'escriptura ha fallat. // genera amb un timestamp i la funció retorna el basename i la ruta
// del directori on s'ha escrit (o un Result amb `filename` buit si
// l'escriptura ha fallat).
namespace Screenshot { namespace Screenshot {
auto save(const std::uint32_t* buffer, int width, int height) -> std::string; // Configura el directori base on s'escriuran les captures (s'hi
// crea una subcarpeta `screenshots/`). El Director el crida una
// vegada al boot amb `system_folder_` perquè les captures vagin
// al mateix lloc per plataforma que la resta de fitxers generats
// (config.yaml, gamepad_configs.yaml, shaders/, etc.). Si no es
// crida, el fallback és el CWD — comportament antic.
void setBaseDir(std::string dir);
// Resultat d'una captura: el `filename` és el basename del PNG
// (per a notificacions/títol) i el `folder` és la ruta absoluta
// on s'ha guardat (per a poder dir "guardada a {folder}"). Si
// l'escriptura falla, ambdós camps tornen buits.
struct Result {
std::string filename;
std::string folder;
};
auto save(const std::uint32_t* buffer, int width, int height) -> Result;
} // namespace Screenshot } // namespace Screenshot
@@ -1,10 +1,10 @@
#include "core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp" #include "core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp"
#include <cstring> #include <cstring>
#include <iostream>
#include "core/rendering/sdl3gpu/shader_factory.hpp" #include "core/rendering/sdl3gpu/shader_factory.hpp"
#include "core/rendering/sdl3gpu/viewport.hpp" #include "core/rendering/sdl3gpu/viewport.hpp"
#include "utils/log.hpp"
#ifdef __APPLE__ #ifdef __APPLE__
#include "core/rendering/sdl3gpu/msl/crtpi_frag.msl.h" #include "core/rendering/sdl3gpu/msl/crtpi_frag.msl.h"
@@ -29,7 +29,7 @@ namespace Rendering {
SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#endif #endif
if (vert == nullptr || frag == nullptr) { if (vert == nullptr || frag == nullptr) {
std::cerr << "CrtPiPipeline: creació de shaders fallida: " << SDL_GetError() << '\n'; Log::error("CrtPiPipeline", "creació de shaders fallida: {}", SDL_GetError());
if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); } if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); }
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); } if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
return false; return false;
@@ -57,7 +57,7 @@ namespace Rendering {
SDL_ReleaseGPUShader(device_, frag); SDL_ReleaseGPUShader(device_, frag);
if (pipeline_ == nullptr) { if (pipeline_ == nullptr) {
std::cerr << "CrtPiPipeline: creació del pipeline ha fallat: " << SDL_GetError() << '\n'; Log::error("CrtPiPipeline", "creació del pipeline ha fallat: {}", SDL_GetError());
return false; return false;
} }
@@ -2,10 +2,10 @@
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <iostream>
#include "core/rendering/sdl3gpu/shader_factory.hpp" #include "core/rendering/sdl3gpu/shader_factory.hpp"
#include "core/rendering/sdl3gpu/viewport.hpp" #include "core/rendering/sdl3gpu/viewport.hpp"
#include "utils/log.hpp"
#ifdef __APPLE__ #ifdef __APPLE__
#include "core/rendering/sdl3gpu/msl/postfx_frag.msl.h" #include "core/rendering/sdl3gpu/msl/postfx_frag.msl.h"
@@ -59,11 +59,11 @@ namespace Rendering {
const char* driver = preferred_driver_.empty() ? nullptr : preferred_driver_.c_str(); const char* driver = preferred_driver_.empty() ? nullptr : preferred_driver_.c_str();
device_ = SDL_CreateGPUDevice(FORMAT, false, driver); device_ = SDL_CreateGPUDevice(FORMAT, false, driver);
if (device_ == nullptr) { if (device_ == nullptr) {
std::cerr << "SDL3GPUShader: SDL_CreateGPUDevice ha fallat: " << SDL_GetError() << '\n'; Log::error("SDL3GPUShader", "SDL_CreateGPUDevice ha fallat: {}", SDL_GetError());
return false; return false;
} }
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) { if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
std::cerr << "SDL3GPUShader: SDL_ClaimWindowForGPUDevice ha fallat: " << SDL_GetError() << '\n'; Log::error("SDL3GPUShader", "SDL_ClaimWindowForGPUDevice ha fallat: {}", SDL_GetError());
return false; return false;
} }
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_)); SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
@@ -82,7 +82,7 @@ namespace Rendering {
fragment_shader_ = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); fragment_shader_ = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#endif #endif
if (vertex_shader_ == nullptr || fragment_shader_ == nullptr) { if (vertex_shader_ == nullptr || fragment_shader_ == nullptr) {
std::cerr << "SDL3GPUShader: SDL_CreateGPUShader ha fallat: " << SDL_GetError() << '\n'; Log::error("SDL3GPUShader", "SDL_CreateGPUShader ha fallat: {}", SDL_GetError());
return false; return false;
} }
return true; return true;
@@ -105,7 +105,7 @@ namespace Rendering {
pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &info); pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &info);
if (pipeline_ == nullptr) { if (pipeline_ == nullptr) {
std::cerr << "SDL3GPUShader: SDL_CreateGPUGraphicsPipeline ha fallat: " << SDL_GetError() << '\n'; Log::error("SDL3GPUShader", "SDL_CreateGPUGraphicsPipeline ha fallat: {}", SDL_GetError());
return false; return false;
} }
return true; return true;
@@ -137,7 +137,7 @@ namespace Rendering {
sampler_ = SDL_CreateGPUSampler(device_, &samp_info); sampler_ = SDL_CreateGPUSampler(device_, &samp_info);
if (scene_texture_ == nullptr || upload_buffer_ == nullptr || sampler_ == nullptr) { if (scene_texture_ == nullptr || upload_buffer_ == nullptr || sampler_ == nullptr) {
std::cerr << "SDL3GPUShader: creació de recursos GPU ha fallat: " << SDL_GetError() << '\n'; Log::error("SDL3GPUShader", "creació de recursos GPU ha fallat: {}", SDL_GetError());
return false; return false;
} }
return true; return true;
@@ -1,12 +1,12 @@
#include "core/rendering/sprite/animated_sprite.hpp" #include "core/rendering/sprite/animated_sprite.hpp"
#include <cmath> #include <cmath>
#include <iostream>
#include <numeric> #include <numeric>
#include <utility> #include <utility>
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "utils/log.hpp"
// El parser pure de YAML d'animacions (parseAnimationsFromYaml + helpers) // El parser pure de YAML d'animacions (parseAnimationsFromYaml + helpers)
// viu a `animated_sprite_parser.cpp` perquè és testable sense Surface ni // viu a `animated_sprite_parser.cpp` perquè és testable sense Surface ni
@@ -74,10 +74,10 @@ AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
if (!first.frames.empty()) { setClip(first.frames[0].rect); } if (!first.frames.empty()) { setClip(first.frames[0].rect); }
} }
} catch (const fkyaml::exception& e) { } catch (const fkyaml::exception& e) {
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n'; Log::error("AnimatedSprite", "YAML parsing error in animation {}: {}", cached_data.name, e.what());
throw; throw;
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "Error loading animation " << cached_data.name << ": " << e.what() << '\n'; Log::error("AnimatedSprite", "Error loading animation {}: {}", cached_data.name, e.what());
throw; throw;
} }
} }
@@ -140,7 +140,7 @@ void AnimatedSprite::setCurrentAnimation(const std::string& name) {
if (current_animation_ == name) { return; } if (current_animation_ == name) { return; }
auto it = animations_.find(name); auto it = animations_.find(name);
if (it == animations_.end()) { if (it == animations_.end()) {
std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n'; Log::warn("AnimatedSprite", "could not find \"{}\" animation", name);
return; return;
} }
current_animation_ = name; current_animation_ = name;
+1 -2
View File
@@ -2,7 +2,6 @@
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
#include <iostream>
#include <stdexcept> #include <stdexcept>
#include <vector> #include <vector>
@@ -63,7 +62,7 @@ Surface::Surface(const std::string& file_path) {
palette_[i] = Rendering::OPAQUE_ALPHA | rgb[i]; palette_[i] = Rendering::OPAQUE_ALPHA | rgb[i];
} }
printWithDots("Surface : ", getFileName(file_path), "[ LOADED ]"); printWithDots("Surface", getFileName(file_path), "[ LOADED ]");
} }
void Surface::clear(Uint8 color) { void Surface::clear(Uint8 color) {
+3 -3
View File
@@ -3,7 +3,6 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cstddef> #include <cstddef>
#include <iostream>
#include <sstream> #include <sstream>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
@@ -11,6 +10,7 @@
#include "core/rendering/sprite/sprite.hpp" #include "core/rendering/sprite/sprite.hpp"
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "utils/log.hpp"
#include "utils/utils.hpp" #include "utils/utils.hpp"
// Decodifica el pròxim codepoint UTF-8 de la cadena i avança 'pos' al byte // Decodifica el pròxim codepoint UTF-8 de la cadena i avança 'pos' al byte
@@ -55,7 +55,7 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
auto file_data = Resource::Helper::loadFile(file_path); auto file_data = Resource::Helper::loadFile(file_path);
if (file_data.empty()) { if (file_data.empty()) {
std::cerr << "Error: Fitxer no trobat " << getFileName(file_path) << '\n'; Log::error("Text File", "fitxer no trobat: {}", getFileName(file_path));
throw std::runtime_error("Fitxer no trobat: " + getFileName(file_path)); throw std::runtime_error("Fitxer no trobat: " + getFileName(file_path));
} }
@@ -101,7 +101,7 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
} }
} }
printWithDots("Text File : ", getFileName(file_path), "[ LOADED ]"); printWithDots("Text File", getFileName(file_path), "[ LOADED ]");
return tf; return tf;
} }
+10 -10
View File
@@ -1,12 +1,12 @@
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include <algorithm> #include <algorithm>
#include <iostream>
#include <stdexcept> #include <stdexcept>
#include "core/audio/jail_audio.hpp" #include "core/audio/jail_audio.hpp"
#include "core/resources/palette_loader.hpp" #include "core/resources/palette_loader.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "utils/log.hpp"
#include "utils/utils.hpp" #include "utils/utils.hpp"
// Deleters dels recursos d'àudio: necessiten veure el tipus complet de // Deleters dels recursos d'àudio: necessiten veure el tipus complet de
@@ -37,19 +37,19 @@ namespace Resource {
throw std::runtime_error("Cache: cannot load animation " + file_path); throw std::runtime_error("Cache: cannot load animation " + file_path);
} }
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = std::move(bytes)}); animations_.emplace_back(AnimationResource{.name = name, .yaml_data = std::move(bytes)});
printWithDots("Animation : ", getFileName(file_path), "[ LOADED ]"); printWithDots("Animation", getFileName(file_path), "[ LOADED ]");
} }
void Cache::loadPalette(const std::string& name, const std::string& file_path) { void Cache::loadPalette(const std::string& name, const std::string& file_path) {
auto palette = PaletteLoader::loadFromFile(file_path); auto palette = PaletteLoader::loadFromFile(file_path);
palettes_.emplace_back(PaletteResource{.name = name, .palette = palette}); palettes_.emplace_back(PaletteResource{.name = name, .palette = palette});
printWithDots("Palette : ", getFileName(file_path), "[ LOADED ]"); printWithDots("Palette", getFileName(file_path), "[ LOADED ]");
} }
auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> { auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> {
auto it = std::ranges::find_if(surfaces_, [&name](const auto& s) { return s.name == name; }); auto it = std::ranges::find_if(surfaces_, [&name](const auto& s) { return s.name == name; });
if (it == surfaces_.end()) { if (it == surfaces_.end()) {
std::cerr << "Cache: surface not found: " << name << '\n'; Log::error("Cache", "surface not found: {}", name);
throw std::runtime_error("Surface not found: " + name); throw std::runtime_error("Surface not found: " + name);
} }
return it->surface; return it->surface;
@@ -58,7 +58,7 @@ namespace Resource {
auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& { auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& {
auto it = std::ranges::find_if(animations_, [&name](const auto& a) { return a.name == name; }); auto it = std::ranges::find_if(animations_, [&name](const auto& a) { return a.name == name; });
if (it == animations_.end()) { if (it == animations_.end()) {
std::cerr << "Cache: animation not found: " << name << '\n'; Log::error("Cache", "animation not found: {}", name);
throw std::runtime_error("Animation not found: " + name); throw std::runtime_error("Animation not found: " + name);
} }
return *it; return *it;
@@ -67,7 +67,7 @@ namespace Resource {
auto Cache::getPalette(const std::string& name) -> const Palette& { auto Cache::getPalette(const std::string& name) -> const Palette& {
auto it = std::ranges::find_if(palettes_, [&name](const auto& p) { return p.name == name; }); auto it = std::ranges::find_if(palettes_, [&name](const auto& p) { return p.name == name; });
if (it == palettes_.end()) { if (it == palettes_.end()) {
std::cerr << "Cache: palette not found: " << name << '\n'; Log::error("Cache", "palette not found: {}", name);
throw std::runtime_error("Palette not found: " + name); throw std::runtime_error("Palette not found: " + name);
} }
return it->palette; return it->palette;
@@ -83,7 +83,7 @@ namespace Resource {
throw std::runtime_error("Cache: Ja::loadMusic failed for " + file_path); throw std::runtime_error("Cache: Ja::loadMusic failed for " + file_path);
} }
musics_.emplace_back(MusicResource{.name = name, .music = std::unique_ptr<Ja::Music, MusicDeleter>(raw)}); musics_.emplace_back(MusicResource{.name = name, .music = std::unique_ptr<Ja::Music, MusicDeleter>(raw)});
printWithDots("Music : ", getFileName(file_path), "[ LOADED ]"); printWithDots("Music", getFileName(file_path), "[ LOADED ]");
} }
void Cache::loadSound(const std::string& name, const std::string& file_path) { void Cache::loadSound(const std::string& name, const std::string& file_path) {
@@ -96,13 +96,13 @@ namespace Resource {
throw std::runtime_error("Cache: Ja::loadSound failed for " + file_path); throw std::runtime_error("Cache: Ja::loadSound failed for " + file_path);
} }
sounds_.emplace_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(raw)}); sounds_.emplace_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(raw)});
printWithDots("Sound : ", getFileName(file_path), "[ LOADED ]"); printWithDots("Sound", getFileName(file_path), "[ LOADED ]");
} }
auto Cache::getMusic(const std::string& name) -> Ja::Music* { auto Cache::getMusic(const std::string& name) -> Ja::Music* {
auto it = std::ranges::find_if(musics_, [&name](const auto& m) { return m.name == name; }); auto it = std::ranges::find_if(musics_, [&name](const auto& m) { return m.name == name; });
if (it == musics_.end()) { if (it == musics_.end()) {
std::cerr << "Cache: music not found: " << name << '\n'; Log::error("Cache", "music not found: {}", name);
throw std::runtime_error("Music not found: " + name); throw std::runtime_error("Music not found: " + name);
} }
return it->music.get(); return it->music.get();
@@ -111,7 +111,7 @@ namespace Resource {
auto Cache::getSound(const std::string& name) -> Ja::Sound* { auto Cache::getSound(const std::string& name) -> Ja::Sound* {
auto it = std::ranges::find_if(sounds_, [&name](const auto& s) { return s.name == name; }); auto it = std::ranges::find_if(sounds_, [&name](const auto& s) { return s.name == name; });
if (it == sounds_.end()) { if (it == sounds_.end()) {
std::cerr << "Cache: sound not found: " << name << '\n'; Log::error("Cache", "sound not found: {}", name);
throw std::runtime_error("Sound not found: " + name); throw std::runtime_error("Sound not found: " + name);
} }
return it->sound.get(); return it->sound.get();
+8 -10
View File
@@ -1,11 +1,12 @@
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include <fstream> #include <fstream>
#include <iostream>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include "core/resources/resource_pack.hpp" #include "core/resources/resource_pack.hpp"
#include "utils/defaults.hpp"
#include "utils/log.hpp"
namespace Resource::Helper { namespace Resource::Helper {
@@ -26,14 +27,14 @@ namespace Resource::Helper {
// Silenciem si veníem de prefer_disk: aleshores caurem // Silenciem si veníem de prefer_disk: aleshores caurem
// al pack i no és cap error real que el fitxer no estigui // al pack i no és cap error real que el fitxer no estigui
// al disc. // al disc.
if (verbose) { std::cerr << "Resource::Helper: cannot open " << FULL << '\n'; } if (verbose) { Log::error("Resource::Helper", "cannot open {}", FULL); }
return {}; return {};
} }
std::streamsize size = file.tellg(); std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg); file.seekg(0, std::ios::beg);
std::vector<std::uint8_t> data(static_cast<std::size_t>(size)); std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
if (!file.read(reinterpret_cast<char*>(data.data()), size)) { if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
std::cerr << "Resource::Helper: failed to read " << FULL << '\n'; Log::error("Resource::Helper", "failed to read {}", FULL);
return {}; return {};
} }
return data; return data;
@@ -51,14 +52,11 @@ namespace Resource::Helper {
g_pack_loaded = g_pack.loadPack(pack_file); g_pack_loaded = g_pack.loadPack(pack_file);
if (g_pack_loaded) { if (g_pack_loaded) {
std::cout << "Resource::Helper: pack loaded (" << g_pack.getResourceCount() Log::info("Resource::Helper", "pack loaded ({} entries) from {}", g_pack.getResourceCount(), pack_file);
<< " entries) from " << pack_file << '\n';
} else if (enable_fallback) { } else if (enable_fallback) {
std::cout << "Resource::Helper: no pack at " << pack_file Log::info("Resource::Helper", "no pack at {} — using filesystem fallback", pack_file);
<< " — using filesystem fallback\n";
} else { } else {
std::cerr << "Resource::Helper: FATAL — no pack at " << pack_file Log::critical("Resource::Helper", "FATAL — no pack at {} and fallback disabled", pack_file);
<< " and fallback disabled\n";
return false; return false;
} }
return true; return true;
@@ -92,7 +90,7 @@ namespace Resource::Helper {
// carpeta `data/`). // carpeta `data/`).
const auto FROM_PACK = [&filepath]() -> std::vector<std::uint8_t> { const auto FROM_PACK = [&filepath]() -> std::vector<std::uint8_t> {
if (!g_pack_loaded) { return {}; } if (!g_pack_loaded) { return {}; }
constexpr std::string_view PREFIX = "data/"; const std::string_view PREFIX{Defaults::Paths::DATA_ROOT};
std::string key = filepath; std::string key = filepath;
if (key.starts_with(PREFIX)) { key.erase(0, PREFIX.size()); } if (key.starts_with(PREFIX)) { key.erase(0, PREFIX.size()); }
if (g_pack.hasResource(key)) { return g_pack.getResource(key); } if (g_pack.hasResource(key)) { return g_pack.getResource(key); }
+11 -10
View File
@@ -4,11 +4,12 @@
#include <array> #include <array>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream>
#include "utils/log.hpp"
namespace Resource { namespace Resource {
const std::string Pack::DEFAULT_ENCRYPT_KEY = "AEE_2026_PACK_KEY"; const std::string Pack::DEFAULT_ENCRYPT_KEY = "AEE_ARCADE_PACK_KEY";
namespace { namespace {
constexpr const char* MAGIC = "AE26"; constexpr const char* MAGIC = "AE26";
@@ -40,14 +41,14 @@ namespace Resource {
std::array<char, 4> header{}; std::array<char, 4> header{};
file.read(header.data(), 4); file.read(header.data(), 4);
if (std::string(header.data(), 4) != MAGIC) { if (std::string(header.data(), 4) != MAGIC) {
std::cerr << "Resource::Pack: invalid pack file format (bad magic): " << pack_file << '\n'; Log::error("Resource::Pack", "invalid pack file format (bad magic): {}", pack_file);
return false; return false;
} }
std::uint32_t version = 0; std::uint32_t version = 0;
file.read(reinterpret_cast<char*>(&version), sizeof(version)); file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != VERSION) { if (version != VERSION) {
std::cerr << "Resource::Pack: unsupported pack version: " << version << '\n'; Log::error("Resource::Pack", "unsupported pack version: {}", version);
return false; return false;
} }
@@ -86,7 +87,7 @@ namespace Resource {
auto Pack::savePack(const std::string& pack_file) const -> bool { auto Pack::savePack(const std::string& pack_file) const -> bool {
std::ofstream file(pack_file, std::ios::binary); std::ofstream file(pack_file, std::ios::binary);
if (!file) { if (!file) {
std::cerr << "Resource::Pack: could not create pack file: " << pack_file << '\n'; Log::error("Resource::Pack", "could not create pack file: {}", pack_file);
return false; return false;
} }
@@ -120,7 +121,7 @@ namespace Resource {
auto Pack::addFile(const std::string& filename, const std::string& filepath) -> bool { auto Pack::addFile(const std::string& filename, const std::string& filepath) -> bool {
std::ifstream file(filepath, std::ios::binary | std::ios::ate); std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) { if (!file) {
std::cerr << "Resource::Pack: could not open file: " << filepath << '\n'; Log::error("Resource::Pack", "could not open file: {}", filepath);
return false; return false;
} }
@@ -129,7 +130,7 @@ namespace Resource {
std::vector<std::uint8_t> file_data(file_size); std::vector<std::uint8_t> file_data(file_size);
if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) { if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) {
std::cerr << "Resource::Pack: could not read file: " << filepath << '\n'; Log::error("Resource::Pack", "could not read file: {}", filepath);
return false; return false;
} }
@@ -146,7 +147,7 @@ namespace Resource {
auto Pack::addDirectory(const std::string& directory) -> bool { auto Pack::addDirectory(const std::string& directory) -> bool {
if (!std::filesystem::exists(directory)) { if (!std::filesystem::exists(directory)) {
std::cerr << "Resource::Pack: directory does not exist: " << directory << '\n'; Log::error("Resource::Pack", "directory does not exist: {}", directory);
return false; return false;
} }
@@ -169,7 +170,7 @@ namespace Resource {
const PackEntry& entry = it->second; const PackEntry& entry = it->second;
if (entry.offset + entry.size > data_.size()) { if (entry.offset + entry.size > data_.size()) {
std::cerr << "Resource::Pack: invalid resource data: " << filename << '\n'; Log::error("Resource::Pack", "invalid resource data: {}", filename);
return {}; return {};
} }
@@ -179,7 +180,7 @@ namespace Resource {
std::uint32_t checksum = calculateChecksum(result); std::uint32_t checksum = calculateChecksum(result);
if (checksum != entry.checksum) { if (checksum != entry.checksum) {
std::cerr << "Resource::Pack: checksum mismatch for: " << filename << '\n'; Log::error("Resource::Pack", "checksum mismatch for: {}", filename);
} }
return result; return result;
} }
+81 -41
View File
@@ -4,10 +4,8 @@
#include <algorithm> #include <algorithm>
#include <any> #include <any>
#include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
#include <iostream>
#include <memory> #include <memory>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -20,9 +18,11 @@
#include "core/input/key_config.hpp" #include "core/input/key_config.hpp"
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/effects/shadow.hpp"
#include "core/rendering/notifier.hpp" #include "core/rendering/notifier.hpp"
#include "core/rendering/render_info.hpp" #include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/screenshot.hpp"
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
@@ -31,6 +31,7 @@
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp" #include "core/ui/ui_config.hpp"
#include "game/debug_config.hpp" #include "game/debug_config.hpp"
#include "game/gameplay_config.hpp"
#ifdef _DEBUG #ifdef _DEBUG
#include "game/editor/editor_config.hpp" #include "game/editor/editor_config.hpp"
#endif #endif
@@ -42,10 +43,11 @@
#include "game/scenes/title_scene.hpp" #include "game/scenes/title_scene.hpp"
#include "game/ui/service_menu.hpp" #include "game/ui/service_menu.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/log.hpp"
Director::Director() { Director::Director() {
if (!SDL_Init(0)) { if (!SDL_Init(0)) {
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); Log::critical("Director", "SDL_Init failed: {}", SDL_GetError());
} }
// Sistema de recursos: el pack viu al costat del binari (build/), el // Sistema de recursos: el pack viu al costat del binari (build/), el
@@ -66,12 +68,13 @@ Director::Director() {
constexpr bool ENABLE_FALLBACK = false; constexpr bool ENABLE_FALLBACK = false;
#endif #endif
if (!Resource::Helper::initializeResourceSystem(pack_file, ENABLE_FALLBACK)) { if (!Resource::Helper::initializeResourceSystem(pack_file, ENABLE_FALLBACK)) {
std::fprintf(stderr, "Director: sistema de recursos no disponible; avortem\n"); Log::critical("Director", "sistema de recursos no disponible; avortem");
std::exit(1); std::exit(1);
} }
// Prepara la carpeta de sistema (~/.config/jailgames/aee_2026/) i carrega // Prepara la carpeta de sistema (per plataforma: %APPDATA% a Windows,
// les opcions; si no existeix config.yaml el crea amb els defaults. // ~/Library/Application Support a macOS, ~/.config a Linux) i carrega les
// opcions; si no existeix config.yaml el crea amb els defaults.
createSystemFolder(); createSystemFolder();
// Gamepad: descobreix mandos presents silenciosament. Cal fer-ho ABANS // Gamepad: descobreix mandos presents silenciosament. Cal fer-ho ABANS
@@ -85,7 +88,7 @@ Director::Director() {
Options::setConfigFile(system_folder_ + "config.yaml"); Options::setConfigFile(system_folder_ + "config.yaml");
if (!Options::loadFromFile()) { if (!Options::loadFromFile()) {
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n'; Log::error("Director", "no s'ha pogut carregar ni desar config.yaml a {}", system_folder_);
} }
// Vinculacions de mando per dispositiu — fitxer aïllat de `config.yaml` // Vinculacions de mando per dispositiu — fitxer aïllat de `config.yaml`
@@ -93,16 +96,21 @@ Director::Director() {
// el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`). // el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`).
GamepadBindings::get().load(system_folder_ + "gamepad_configs.yaml"); GamepadBindings::get().load(system_folder_ + "gamepad_configs.yaml");
// Tweaks de jugabilitat (látigo travessa sarcs, friendly fire, col·lisió
// entre jugadors). Fitxer escrivible des del menú de servei (F9). Si
// no existeix, GameplayConfig::load el crea amb els defaults.
GameplayConfig::get().load(system_folder_ + "gameplay.yaml");
// Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix // Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix
// es crea amb presets per defecte (off, crt, ntsc). // es crea amb presets per defecte (off, crt, ntsc).
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE); Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
if (!Options::loadPostFXFromFile()) { if (!Options::loadPostFXFromFile()) {
std::cerr << "Director: no s'ha pogut carregar ni desar postfx.yaml.\n"; Log::error("Director", "no s'ha pogut carregar ni desar postfx.yaml.");
} }
// Presets del shader CRT-Pi (shaders/crtpi.yaml). // Presets del shader CRT-Pi (shaders/crtpi.yaml).
Options::setCrtPiFile(system_folder_ + Defaults::Video::CRTPI_FILE); Options::setCrtPiFile(system_folder_ + Defaults::Video::CRTPI_FILE);
if (!Options::loadCrtPiFromFile()) { if (!Options::loadCrtPiFromFile()) {
std::cerr << "Director: no s'ha pogut carregar ni desar crtpi.yaml.\n"; Log::error("Director", "no s'ha pogut carregar ni desar crtpi.yaml.");
} }
// Screen es configura ara (subsystem + finestra + GPU/renderer); la seua // Screen es configura ara (subsystem + finestra + GPU/renderer); la seua
@@ -118,22 +126,29 @@ Director::Director() {
Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale() + ".yaml"); Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale() + ".yaml");
// Hotkeys globals. GlobalInputs::update() les consumirà a cada frame. // Hotkeys globals. GlobalInputs::update() les consumirà a cada frame.
KeyConfig::get().load("data/input/keys.yaml"); KeyConfig::get().load(Defaults::Paths::INPUT_YAML);
// Tema d'UI (colors, timings, fonts, flags). Qualsevol clau absent a // Tema d'UI (colors, timings, fonts, flags). Qualsevol clau absent a
// data/ui.yaml cau al valor compilat a Defaults::UI. // data/config/ui.yaml cau al valor compilat a Defaults::UI.
UiConfig::get().load("data/ui.yaml"); UiConfig::get().load(Defaults::Paths::UI_YAML);
// Configuració específica de les escenes TITLE i LOGO. Es carreguen
// additivament sobre el mateix UiConfig: cada fitxer només conté la
// seua subclau (`title:` o `logo:`), i les claus absents a la base
// (ui.yaml) o als yamls d'escena cauen al valor compilat a
// Defaults::UI::Title / Defaults::UI::Logo.
UiConfig::get().load(Defaults::Paths::TITLE_YAML);
UiConfig::get().load(Defaults::Paths::LOGO_YAML);
#ifdef _DEBUG #ifdef _DEBUG
// Tema visual de l'editor de tilemap (Debug-only). Claus absents // Tema visual de l'editor de tilemap (Debug-only). Claus absents
// cauen als valors compilats a Defaults::Editor. // cauen als valors compilats a Defaults::Editor.
EditorConfig::get().load("data/editor/editor.yaml"); EditorConfig::get().load(Defaults::Paths::EDITOR_YAML);
#endif #endif
// Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els // Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els
// overlays perquè tots els reaprofiten sense duplicar la lògica. Els // overlays perquè tots els reaprofiten sense duplicar la lògica. Els
// noms de font viuen al fitxer data/ui.yaml via UiConfig. // noms de font viuen al fitxer data/config/ui.yaml via UiConfig.
Resource::Cache::get().loadPalette("ui", "data/palettes/uzebox.pal"); Resource::Cache::get().loadPalette("ui", Defaults::Paths::UI_PALETTE);
const std::string HUD_FONT_GIF_KEY = UiConfig::get().hudFont().name + ".gif"; const std::string HUD_FONT_GIF_KEY = UiConfig::get().hudFont().name + ".gif";
Resource::Cache::get().loadSurface(HUD_FONT_GIF_KEY, UiConfig::get().hudFont().gif); Resource::Cache::get().loadSurface(HUD_FONT_GIF_KEY, UiConfig::get().hudFont().gif);
Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui")); Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
@@ -149,36 +164,44 @@ Director::Director() {
Resource::Cache::get().loadSurface(ROOM_TEXT_FONT_GIF_KEY, UiConfig::get().roomTextFont().gif); Resource::Cache::get().loadSurface(ROOM_TEXT_FONT_GIF_KEY, UiConfig::get().roomTextFont().gif);
Resource::Cache::get().getSurface(ROOM_TEXT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui")); Resource::Cache::get().getSurface(ROOM_TEXT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
// Sprite compartit de la sombra (tira horitzontal de fotogrames de
// 16x16, de petit a gran). El consumeix `Effects::Shadow::render`,
// que el reaprofiten Player i qualsevol entitat futura compatible.
Resource::Cache::get().loadSurface(Effects::Shadow::CACHE_KEY, "data/entities/shadow/shadow.gif");
// Pistes de música del cicle de sala (intro/playing/outro). Es preloden // Pistes de música del cicle de sala (intro/playing/outro). Es preloden
// ací perquè formen part del preload d'assets, com la font i la paleta; // ací perquè formen part del preload d'assets, com la font i la paleta;
// la lògica de quina sona en cada estat viu a `GameScene`. // la lògica de quina sona en cada estat viu a `GameScene`.
Resource::Cache::get().loadMusic("room_intro.ogg", "data/music/room_intro.ogg"); Resource::Cache::get().loadMusic("room_intro.ogg", Defaults::Paths::MUSIC_ROOM_INTRO);
Resource::Cache::get().loadMusic("playing.ogg", "data/music/playing.ogg"); Resource::Cache::get().loadMusic("playing.ogg", Defaults::Paths::MUSIC_PLAYING);
Resource::Cache::get().loadMusic("room_outro.ogg", "data/music/room_outro.ogg"); Resource::Cache::get().loadMusic("room_outro.ogg", Defaults::Paths::MUSIC_ROOM_OUTRO);
// Pista de game over: pre-load igual que la resta perquè el playMusic
// del moment en què es dispara la sub-FSM no facti disc.
Resource::Cache::get().loadMusic("game_over.ogg", Defaults::Paths::MUSIC_GAME_OVER);
// Música de la TITLE: arrenca al constructor de l'escena i es fa // Música de la TITLE: arrenca al constructor de l'escena i es fa
// fade-out quan l'escena entra a STARTING. // fade-out quan l'escena entra a STARTING.
Resource::Cache::get().loadMusic("title.ogg", "data/music/title.ogg"); Resource::Cache::get().loadMusic("title.ogg", Defaults::Paths::MUSIC_TITLE);
// Sons del menú de servei. Els noms venen de `data/ui.yaml` (amb // Sons del menú de servei. Els noms venen de `data/config/ui.yaml` (amb
// fallback a `Defaults::UI::MENU_*_SOUND`); el ServiceMenu els // fallback a `Defaults::UI::MENU_*_SOUND`); el ServiceMenu els
// dispara via Audio::Group::INTERFACE. // dispara via Audio::Group::INTERFACE.
const auto& menu_select = UiConfig::get().menuSelectSound(); const auto& menu_select = UiConfig::get().menuSelectSound();
const auto& menu_accept = UiConfig::get().menuAcceptSound(); const auto& menu_accept = UiConfig::get().menuAcceptSound();
Resource::Cache::get().loadSound(menu_select, "data/sounds/" + menu_select); Resource::Cache::get().loadSound(menu_select, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + menu_select);
Resource::Cache::get().loadSound(menu_accept, "data/sounds/" + menu_accept); Resource::Cache::get().loadSound(menu_accept, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + menu_accept);
// So del START a la TITLE: el dispara TitleScene quan un jugador // So del START a la TITLE: el dispara TitleScene quan un jugador
// prem START (vore enterStarting i el reset del segon jugador). // prem START (vore enterStarting i el reset del segon jugador).
const auto& player_start = UiConfig::get().playerStartSound(); const auto& player_start = UiConfig::get().playerStartSound();
Resource::Cache::get().loadSound(player_start, "data/sounds/" + player_start); Resource::Cache::get().loadSound(player_start, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + player_start);
// Soroll de LogoScene (scanline reveal). El dispara LogoScene al // Soroll de LogoScene (scanline reveal). El dispara LogoScene al
// ctor; amb fallback al nom de Defaults si la clau YAML no hi és. // ctor; amb fallback al nom de Defaults si la clau YAML no hi és.
const auto& logo_glitch = UiConfig::get().logoGlitchSound(); const auto& logo_glitch = UiConfig::get().logoGlitchSound();
Resource::Cache::get().loadSound(logo_glitch, "data/sounds/" + logo_glitch); Resource::Cache::get().loadSound(logo_glitch, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + logo_glitch);
// Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio // Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio
// perquè els primers playSoundWith* (al començament del joc) ja // perquè els primers playSoundWith* (al començament del joc) ja
// troben els presets disponibles. // troben els presets disponibles.
SoundEffectsConfig::get().load("data/sounds.yaml"); SoundEffectsConfig::get().load(Defaults::Paths::SOUNDS_YAML);
// Audio: singleton manual propietari d'un SDL audio device. Es destrueix // Audio: singleton manual propietari d'un SDL audio device. Es destrueix
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els // explícitament a ~Director abans que Resource::Cache (Meyers) buide els
@@ -216,7 +239,7 @@ Director::Director() {
// Font del menú de servei (F9). El títol va amb la variant `display` // Font del menú de servei (F9). El títol va amb la variant `display`
// i els items amb `condensed-display` (més compacte perquè caben més // i els items amb `condensed-display` (més compacte perquè caben més
// files). Els dos noms i kernings viuen a data/ui.yaml via UiConfig. // files). Els dos noms i kernings viuen a data/config/ui.yaml via UiConfig.
auto load_menu_font = [&](const UiConfig::Font& font) { auto load_menu_font = [&](const UiConfig::Font& font) {
const std::string KEY = font.name + ".gif"; const std::string KEY = font.name + ".gif";
Resource::Cache::get().loadSurface(KEY, font.gif); Resource::Cache::get().loadSurface(KEY, font.gif);
@@ -233,7 +256,7 @@ Director::Director() {
// sobreescrivim amb la "ui" perquè el GIF ja porta els colors finals // sobreescrivim amb la "ui" perquè el GIF ja porta els colors finals
// del logo). // del logo).
constexpr const char* MINI_LOGO_KEY = "logo_jailgames_mini.gif"; constexpr const char* MINI_LOGO_KEY = "logo_jailgames_mini.gif";
Resource::Cache::get().loadSurface(MINI_LOGO_KEY, "data/logo/logo_jailgames_mini.gif"); Resource::Cache::get().loadSurface(MINI_LOGO_KEY, Defaults::Paths::LOGO_MINI_GIF);
auto mini_logo_surface = Resource::Cache::get().getSurface(MINI_LOGO_KEY); auto mini_logo_surface = Resource::Cache::get().getSurface(MINI_LOGO_KEY);
mini_logo_surface_ = mini_logo_surface; mini_logo_surface_ = mini_logo_surface;
@@ -241,20 +264,20 @@ Director::Director() {
// conserva la seua paleta nadiua: els colors finals del logo viuen // conserva la seua paleta nadiua: els colors finals del logo viuen
// dins del propi GIF i no han de passar per la paleta UI. // dins del propi GIF i no han de passar per la paleta UI.
constexpr const char* LOGO_FULL_KEY = "logo_jailgames.gif"; constexpr const char* LOGO_FULL_KEY = "logo_jailgames.gif";
Resource::Cache::get().loadSurface(LOGO_FULL_KEY, "data/logo/logo_jailgames.gif"); Resource::Cache::get().loadSurface(LOGO_FULL_KEY, Defaults::Paths::LOGO_FULL_GIF);
logo_full_surface_ = Resource::Cache::get().getSurface(LOGO_FULL_KEY); logo_full_surface_ = Resource::Cache::get().getSurface(LOGO_FULL_KEY);
// Logotip del joc per a la TITLE (substitueix el text "Aventures en // Logotip del joc per a la TITLE (substitueix el text "Aventures en
// Egipte ARCADE"). Igual que la resta de logos, paleta nadiua — // Egipte ARCADE"). Igual que la resta de logos, paleta nadiua —
// l'art ja porta els colors finals i no passa per la UI. // l'art ja porta els colors finals i no passa per la UI.
constexpr const char* GAME_LOGO_KEY = "game_logo.gif"; constexpr const char* GAME_LOGO_KEY = "game_logo.gif";
Resource::Cache::get().loadSurface(GAME_LOGO_KEY, "data/title/game_logo.gif"); Resource::Cache::get().loadSurface(GAME_LOGO_KEY, Defaults::Paths::TITLE_GAME_LOGO_GIF);
game_logo_surface_ = Resource::Cache::get().getSurface(GAME_LOGO_KEY); game_logo_surface_ = Resource::Cache::get().getSurface(GAME_LOGO_KEY);
// Fons sencer de la TITLE (320×240). Igual que el minilogo, conserva // Fons sencer de la TITLE (320×240). Igual que el minilogo, conserva
// la seua paleta nadiua perquè el GIF ja porta els colors finals. // la seua paleta nadiua perquè el GIF ja porta els colors finals.
constexpr const char* TITLE_BG_KEY = "title_bg.gif"; constexpr const char* TITLE_BG_KEY = "title_bg.gif";
Resource::Cache::get().loadSurface(TITLE_BG_KEY, "data/title/title_bg.gif"); Resource::Cache::get().loadSurface(TITLE_BG_KEY, Defaults::Paths::TITLE_BG_GIF);
auto title_bg_surface = Resource::Cache::get().getSurface(TITLE_BG_KEY); auto title_bg_surface = Resource::Cache::get().getSurface(TITLE_BG_KEY);
title_bg_surface_ = title_bg_surface; title_bg_surface_ = title_bg_surface;
ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette); ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette);
@@ -288,8 +311,7 @@ Director::Director() {
const auto START = DebugConfig::StartScene::LOGO; const auto START = DebugConfig::StartScene::LOGO;
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH; const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
#endif #endif
std::cout << "Director: start_scene=" << DebugConfig::toString(START) Log::info("Director", "start_scene={} starting_players={}", DebugConfig::toString(START), DebugConfig::toString(STARTING_PLAYERS));
<< " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n';
switch (START) { switch (START) {
case DebugConfig::StartScene::LOGO: case DebugConfig::StartScene::LOGO:
@@ -326,26 +348,44 @@ Director::~Director() {
} }
void Director::createSystemFolder() { void Director::createSystemFolder() {
// Volem la config a $XDG_CONFIG_HOME/jailgames/aee_2026/ (defecte // Carpeta de configuració per plataforma:
// ~/.config/jailgames/aee_2026/). SDL_GetPrefPath apunta a XDG_DATA_HOME // - Windows: %APPDATA%\jailgames\aee_arcade\
// (.local/share), que és el lloc per a dades, no per a configuració. // - macOS: ~/Library/Application Support/jailgames/aee_arcade/
const char* xdg = std::getenv("XDG_CONFIG_HOME"); // - Linux: $XDG_CONFIG_HOME/jailgames/aee_arcade/ (defecte
// ~/.config/jailgames/aee_arcade/)
// No fem servir SDL_GetPrefPath: a Linux apunta a XDG_DATA_HOME
// (~/.local/share), que és per a dades, no per a configuració.
std::string base; std::string base;
#ifdef _WIN32
const char* appdata = std::getenv("APPDATA");
base = (appdata != nullptr && *appdata != '\0') ? std::string(appdata) : ".";
#elif defined(__APPLE__)
const char* home = std::getenv("HOME");
base = (home != nullptr) ? std::string(home) + "/Library/Application Support" : ".";
#else
const char* xdg = std::getenv("XDG_CONFIG_HOME");
if (xdg != nullptr && *xdg != '\0') { if (xdg != nullptr && *xdg != '\0') {
base = xdg; base = xdg;
} else { } else {
const char* home = std::getenv("HOME"); const char* home = std::getenv("HOME");
base = (home != nullptr) ? std::string(home) + "/.config" : "."; base = (home != nullptr) ? std::string(home) + "/.config" : ".";
} }
system_folder_ = base + "/jailgames/aee_2026/"; #endif
system_folder_ = base + "/jailgames/aee_arcade/";
std::error_code error; std::error_code error;
std::filesystem::create_directories(system_folder_, error); std::filesystem::create_directories(system_folder_, error);
// Subcarpeta per als presets de shaders. std::filesystem és tolerant si ja existeix. // Subcarpeta per als presets de shaders. std::filesystem és tolerant si ja existeix.
std::filesystem::create_directories(system_folder_ + "shaders/", error); std::filesystem::create_directories(system_folder_ + "shaders/", error);
if (error) { if (error) {
std::cerr << "Director: no s'ha pogut crear " << system_folder_ << ": " << error.message() << '\n'; Log::error("Director", "no s'ha pogut crear {}: {}", system_folder_, error.message());
} }
// Les captures de pantalla viuen sota la mateixa carpeta per
// plataforma. Sense aquesta crida, `Screenshot::save` cauria al
// CWD del procés (raïz del projecte si llances `build/aee_arcade`
// a mà, lloc imprevisible en una release de macOS/Windows).
Screenshot::setBaseDir(system_folder_);
} }
void Director::processEvents() { void Director::processEvents() {
@@ -426,7 +466,7 @@ void Director::switchSceneIfRequested() {
// l'escena en aquest mateix frame — l'usuari ha cancel·lat l'estat // l'escena en aquest mateix frame — l'usuari ha cancel·lat l'estat
// de joc explícitament i no ens interessa mantenir-ne la sortida. // de joc explícitament i no ens interessa mantenir-ne la sortida.
if (auto menu_req = ServiceMenu::get().takePendingSceneRequest(); menu_req.has_value()) { if (auto menu_req = ServiceMenu::get().takePendingSceneRequest(); menu_req.has_value()) {
std::cout << "Director: scene transition (menu) → " << toString(menu_req->next) << '\n'; Log::info("Director", "scene transition (menu) → {}", toString(menu_req->next));
current_scene_ = buildScene(menu_req->next, menu_req->params); current_scene_ = buildScene(menu_req->next, menu_req->params);
return; return;
} }
@@ -438,10 +478,10 @@ void Director::switchSceneIfRequested() {
if (!request.has_value()) { return; } if (!request.has_value()) { return; }
try { try {
const auto& req = std::any_cast<const ExitRequest&>(request); const auto& req = std::any_cast<const ExitRequest&>(request);
std::cout << "Director: scene transition → " << toString(req.next) << '\n'; Log::info("Director", "scene transition → {}", toString(req.next));
current_scene_ = buildScene(req.next, req.params); current_scene_ = buildScene(req.next, req.params);
} catch (const std::bad_any_cast&) { } catch (const std::bad_any_cast&) {
std::cerr << "Director: exit_request_ amb tipus inesperat — ignorant\n"; Log::error("Director", "exit_request_ amb tipus inesperat — ignorant");
} }
} }
+30 -10
View File
@@ -1,6 +1,5 @@
#include "core/ui/ui_config.hpp" #include "core/ui/ui_config.hpp"
#include <iostream>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <unordered_map> #include <unordered_map>
@@ -9,6 +8,7 @@
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/easing.hpp" #include "utils/easing.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -27,7 +27,7 @@ namespace {
if (!node.contains(key)) { return; } if (!node.contains(key)) { return; }
const int VALUE = node[key].get_value<int>(); const int VALUE = node[key].get_value<int>();
if (VALUE < 0 || VALUE > 255) { if (VALUE < 0 || VALUE > 255) {
std::cerr << "UiConfig: índex '" << key << "'=" << VALUE << " fora de rang 0..255 — ignorat\n"; Log::warn("UiConfig", "índex '{}'={} fora de rang 0..255 — ignorat", key, VALUE);
return; return;
} }
dst = static_cast<Uint8>(VALUE); dst = static_cast<Uint8>(VALUE);
@@ -82,8 +82,15 @@ UiConfig::UiConfig()
menu_cursor_(Defaults::UI::MENU_CURSOR), menu_cursor_(Defaults::UI::MENU_CURSOR),
menu_disabled_(Defaults::UI::MENU_DISABLED), menu_disabled_(Defaults::UI::MENU_DISABLED),
hud_bg_(Defaults::UI::HUD_BG), hud_bg_(Defaults::UI::HUD_BG),
hud_label_(Defaults::UI::HUD_LABEL), hud_p1_label_(Defaults::UI::HUD_P1_LABEL),
hud_value_(Defaults::UI::HUD_VALUE), hud_p1_value_(Defaults::UI::HUD_P1_VALUE),
hud_p1_shadow_(Defaults::UI::HUD_P1_SHADOW),
hud_center_label_(Defaults::UI::HUD_CENTER_LABEL),
hud_center_value_(Defaults::UI::HUD_CENTER_VALUE),
hud_center_shadow_(Defaults::UI::HUD_CENTER_SHADOW),
hud_p2_label_(Defaults::UI::HUD_P2_LABEL),
hud_p2_value_(Defaults::UI::HUD_P2_VALUE),
hud_p2_shadow_(Defaults::UI::HUD_P2_SHADOW),
hud_flash_(Defaults::UI::HUD_FLASH), hud_flash_(Defaults::UI::HUD_FLASH),
room_text_(Defaults::UI::ROOM_TEXT), room_text_(Defaults::UI::ROOM_TEXT),
room_text_outline_(Defaults::UI::ROOM_TEXT_OUTLINE), room_text_outline_(Defaults::UI::ROOM_TEXT_OUTLINE),
@@ -183,7 +190,7 @@ auto UiConfig::curveByName(const std::string& name) -> CurveFn {
const auto& reg = curveRegistry(); const auto& reg = curveRegistry();
const auto IT = reg.find(name); const auto IT = reg.find(name);
if (IT == reg.end()) { if (IT == reg.end()) {
std::cerr << "UiConfig: corba '" << name << "' desconeguda — s'usa linear\n"; Log::warn("UiConfig", "corba '{}' desconeguda — s'usa linear", name);
return &Easing::linear; return &Easing::linear;
} }
return IT->second; return IT->second;
@@ -192,7 +199,7 @@ auto UiConfig::curveByName(const std::string& name) -> CurveFn {
void UiConfig::load(const std::string& file_path) { void UiConfig::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 << "UiConfig: no s'ha pogut obrir " << file_path << " — s'usaran els defaults compilats\n"; Log::warn("UiConfig", "no s'ha pogut obrir {} — s'usaran els defaults compilats", file_path);
return; return;
} }
@@ -218,13 +225,26 @@ void UiConfig::load(const std::string& file_path) {
readColorIndex(c, "menu_cursor", menu_cursor_); readColorIndex(c, "menu_cursor", menu_cursor_);
readColorIndex(c, "menu_disabled", menu_disabled_); readColorIndex(c, "menu_disabled", menu_disabled_);
readColorIndex(c, "hud_bg", hud_bg_); readColorIndex(c, "hud_bg", hud_bg_);
readColorIndex(c, "hud_label", hud_label_);
readColorIndex(c, "hud_value", hud_value_);
readColorIndex(c, "hud_flash", hud_flash_); readColorIndex(c, "hud_flash", hud_flash_);
readColorIndex(c, "room_text", room_text_); readColorIndex(c, "room_text", room_text_);
readColorIndex(c, "room_text_outline", room_text_outline_); readColorIndex(c, "room_text_outline", room_text_outline_);
} }
// Famílies de color del HUD: blocs `hud_p1`, `hud_center`,
// `hud_p2` amb claus `label`, `value`, `shadow`. Cada bloc i
// cada clau són opcionals — el que manqui cau al default
// compilat a `Defaults::UI`.
auto read_hud_family = [&](const std::string& key, Uint8& label, Uint8& value, Uint8& shadow) {
if (!yaml.contains(key)) { return; }
const auto& f = yaml[key];
readColorIndex(f, "label", label);
readColorIndex(f, "value", value);
readColorIndex(f, "shadow", shadow);
};
read_hud_family("hud_p1", hud_p1_label_, hud_p1_value_, hud_p1_shadow_);
read_hud_family("hud_center", hud_center_label_, hud_center_value_, hud_center_shadow_);
read_hud_family("hud_p2", hud_p2_label_, hud_p2_value_, hud_p2_shadow_);
if (yaml.contains("timings")) { if (yaml.contains("timings")) {
const auto& t = yaml["timings"]; const auto& t = yaml["timings"];
readField(t, "notification_ms", notification_ms_); readField(t, "notification_ms", notification_ms_);
@@ -345,8 +365,8 @@ void UiConfig::load(const std::string& file_path) {
readCurve(n, "slide_out_curve", notifier_.slide_out_curve); readCurve(n, "slide_out_curve", notifier_.slide_out_curve);
} }
std::cout << "UiConfig: carregat des de " << file_path << '\n'; Log::info("UiConfig", "carregat des de {}", file_path);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "UiConfig: error parsejant " << file_path << ": " << e.what() << '\n'; Log::error("UiConfig", "error parsejant {}: {}", file_path, e.what());
} }
} }
+26 -8
View File
@@ -5,7 +5,7 @@
#include <string> #include <string>
// Configuració del tema UI (índexs de paleta, timings, fonts i flags) // Configuració del tema UI (índexs de paleta, timings, fonts i flags)
// carregada des de data/ui.yaml. Cada camp s'inicialitza amb el valor // carregada des de data/config/ui.yaml. Cada camp s'inicialitza amb el valor
// de Defaults::UI com a fallback: si el fitxer falta o li manca una // de Defaults::UI com a fallback: si el fitxer falta o li manca una
// clau, el joc s'executa amb el tema compilat. // clau, el joc s'executa amb el tema compilat.
// //
@@ -21,12 +21,12 @@ class UiConfig {
}; };
// Signatura d'una funció d'easing: t ∈ [0, 1] → valor ∈ [0, 1]. // Signatura d'una funció d'easing: t ∈ [0, 1] → valor ∈ [0, 1].
// Permet triar corbes des de data/ui.yaml per nom (cadena) i guardar // Permet triar corbes des de data/config/ui.yaml per nom (cadena) i guardar
// el punter a la corresponent funció de `utils/easing.hpp`. // el punter a la corresponent funció de `utils/easing.hpp`.
using CurveFn = auto (*)(float) -> float; using CurveFn = auto (*)(float) -> float;
// Geometria del Notifier (popup superior). Carregada des de // Geometria del Notifier (popup superior). Carregada des de
// data/ui.yaml secció `notifier:`; els valors per defecte vénen de // data/config/ui.yaml secció `notifier:`; els valors per defecte vénen de
// Defaults::UI::Notifier. El moviment és per temps (slide_ms) i no per // Defaults::UI::Notifier. El moviment és per temps (slide_ms) i no per
// velocitat, perquè una caixa més gran no triga més a entrar/eixir. // velocitat, perquè una caixa més gran no triga més a entrar/eixir.
struct Notifier { struct Notifier {
@@ -66,8 +66,19 @@ class UiConfig {
[[nodiscard]] auto menuCursor() const -> Uint8 { return menu_cursor_; } [[nodiscard]] auto menuCursor() const -> Uint8 { return menu_cursor_; }
[[nodiscard]] auto menuDisabled() const -> Uint8 { return menu_disabled_; } [[nodiscard]] auto menuDisabled() const -> Uint8 { return menu_disabled_; }
[[nodiscard]] auto hudBg() const -> Uint8 { return hud_bg_; } [[nodiscard]] auto hudBg() const -> Uint8 { return hud_bg_; }
[[nodiscard]] auto hudLabel() const -> Uint8 { return hud_label_; } // Famílies de color del HUD per bloc (LEFT/CENTER/RIGHT). Cada
[[nodiscard]] auto hudValue() const -> Uint8 { return hud_value_; } // família té label (etiqueta apagada), value (valor destacat) i
// shadow (to fosc per a la sombra-embossing). El flash és comú a
// les tres famílies (groc neó al guanyar punts).
[[nodiscard]] auto hudP1Label() const -> Uint8 { return hud_p1_label_; }
[[nodiscard]] auto hudP1Value() const -> Uint8 { return hud_p1_value_; }
[[nodiscard]] auto hudP1Shadow() const -> Uint8 { return hud_p1_shadow_; }
[[nodiscard]] auto hudCenterLabel() const -> Uint8 { return hud_center_label_; }
[[nodiscard]] auto hudCenterValue() const -> Uint8 { return hud_center_value_; }
[[nodiscard]] auto hudCenterShadow() const -> Uint8 { return hud_center_shadow_; }
[[nodiscard]] auto hudP2Label() const -> Uint8 { return hud_p2_label_; }
[[nodiscard]] auto hudP2Value() const -> Uint8 { return hud_p2_value_; }
[[nodiscard]] auto hudP2Shadow() const -> Uint8 { return hud_p2_shadow_; }
[[nodiscard]] auto hudFlash() const -> Uint8 { return hud_flash_; } [[nodiscard]] auto hudFlash() const -> Uint8 { return hud_flash_; }
[[nodiscard]] auto roomText() const -> Uint8 { return room_text_; } [[nodiscard]] auto roomText() const -> Uint8 { return room_text_; }
[[nodiscard]] auto roomTextOutline() const -> Uint8 { return room_text_outline_; } [[nodiscard]] auto roomTextOutline() const -> Uint8 { return room_text_outline_; }
@@ -170,7 +181,7 @@ class UiConfig {
[[nodiscard]] static auto curveByName(const std::string& name) -> CurveFn; [[nodiscard]] static auto curveByName(const std::string& name) -> CurveFn;
// Sons del menú de servei. Claus de Resource::Cache::loadSound (els // Sons del menú de servei. Claus de Resource::Cache::loadSound (els
// fitxers viuen a `data/sounds/`). // fitxers viuen a `data/audio/sounds/`).
[[nodiscard]] auto menuSelectSound() const -> const std::string& { return menu_select_sound_; } [[nodiscard]] auto menuSelectSound() const -> const std::string& { return menu_select_sound_; }
[[nodiscard]] auto menuAcceptSound() const -> const std::string& { return menu_accept_sound_; } [[nodiscard]] auto menuAcceptSound() const -> const std::string& { return menu_accept_sound_; }
[[nodiscard]] auto playerStartSound() const -> const std::string& { return player_start_sound_; } [[nodiscard]] auto playerStartSound() const -> const std::string& { return player_start_sound_; }
@@ -209,8 +220,15 @@ class UiConfig {
Uint8 menu_cursor_; Uint8 menu_cursor_;
Uint8 menu_disabled_; Uint8 menu_disabled_;
Uint8 hud_bg_; Uint8 hud_bg_;
Uint8 hud_label_; Uint8 hud_p1_label_;
Uint8 hud_value_; Uint8 hud_p1_value_;
Uint8 hud_p1_shadow_;
Uint8 hud_center_label_;
Uint8 hud_center_value_;
Uint8 hud_center_shadow_;
Uint8 hud_p2_label_;
Uint8 hud_p2_value_;
Uint8 hud_p2_shadow_;
Uint8 hud_flash_; Uint8 hud_flash_;
Uint8 room_text_; Uint8 room_text_;
Uint8 room_text_outline_; Uint8 room_text_outline_;
+10 -10
View File
@@ -3,11 +3,11 @@
#include <exception> #include <exception>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace DebugConfig { namespace DebugConfig {
@@ -53,12 +53,12 @@ namespace DebugConfig {
if (!yaml.contains("start_scene")) { return StartScene::GAME; } if (!yaml.contains("start_scene")) { return StartScene::GAME; }
const auto RAW = yaml["start_scene"].get_value<std::string>(); const auto RAW = yaml["start_scene"].get_value<std::string>();
if (!isKnown(RAW)) { if (!isKnown(RAW)) {
std::cerr << "debug.yaml: start_scene \"" << RAW << "\" invàlid, arrancant a GAME\n"; Log::warn("debug.yaml", "start_scene \"{}\" invàlid, arrancant a GAME", RAW);
return StartScene::GAME; return StartScene::GAME;
} }
return fromName(RAW); return fromName(RAW);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "debug.yaml: error parsejant: " << e.what() << " — arrancant a GAME\n"; Log::error("debug.yaml", "error parsejant: {} — arrancant a GAME", e.what());
return StartScene::GAME; return StartScene::GAME;
} }
} }
@@ -72,7 +72,7 @@ namespace DebugConfig {
} }
const auto CONTENT = readFile(file_path); const auto CONTENT = readFile(file_path);
if (CONTENT.empty()) { if (CONTENT.empty()) {
std::cerr << "debug.yaml: fitxer buit a " << file_path << ", arrancant a GAME\n"; Log::warn("debug.yaml", "fitxer buit a {}, arrancant a GAME", file_path);
return StartScene::GAME; return StartScene::GAME;
} }
return parseStartScene(CONTENT); return parseStartScene(CONTENT);
@@ -85,12 +85,12 @@ namespace DebugConfig {
if (!yaml.contains("starting_players")) { return StartingPlayers::BOTH; } if (!yaml.contains("starting_players")) { return StartingPlayers::BOTH; }
const auto RAW = yaml["starting_players"].get_value<std::string>(); const auto RAW = yaml["starting_players"].get_value<std::string>();
if (!isKnownPlayers(RAW)) { if (!isKnownPlayers(RAW)) {
std::cerr << "debug.yaml: starting_players \"" << RAW << "\" invàlid, arrancant amb BOTH\n"; Log::warn("debug.yaml", "starting_players \"{}\" invàlid, arrancant amb BOTH", RAW);
return StartingPlayers::BOTH; return StartingPlayers::BOTH;
} }
return playersFromName(RAW); return playersFromName(RAW);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "debug.yaml: error parsejant starting_players: " << e.what() << " — arrancant amb BOTH\n"; Log::error("debug.yaml", "error parsejant starting_players: {} — arrancant amb BOTH", e.what());
return StartingPlayers::BOTH; return StartingPlayers::BOTH;
} }
} }
@@ -110,7 +110,7 @@ namespace DebugConfig {
if (!yaml.contains("starting_level")) { return std::nullopt; } if (!yaml.contains("starting_level")) { return std::nullopt; }
const auto& sl = yaml["starting_level"]; const auto& sl = yaml["starting_level"];
if (!sl.contains("pyramid") || !sl.contains("room")) { if (!sl.contains("pyramid") || !sl.contains("room")) {
std::cerr << "debug.yaml: starting_level necessita els camps `pyramid` i `room` — ignorat\n"; Log::warn("debug.yaml", "starting_level necessita els camps `pyramid` i `room` — ignorat");
return std::nullopt; return std::nullopt;
} }
return StartingLevel{ return StartingLevel{
@@ -118,7 +118,7 @@ namespace DebugConfig {
.room = sl["room"].get_value<int>(), .room = sl["room"].get_value<int>(),
}; };
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "debug.yaml: error parsejant starting_level: " << e.what() << " — ignorat\n"; Log::error("debug.yaml", "error parsejant starting_level: {} — ignorat", e.what());
return std::nullopt; return std::nullopt;
} }
} }
@@ -162,7 +162,7 @@ namespace DebugConfig {
if (std::filesystem::exists(file_path, error)) { return; } if (std::filesystem::exists(file_path, error)) { return; }
std::ofstream file(file_path); std::ofstream file(file_path);
if (!file) { if (!file) {
std::cerr << "debug.yaml: no s'ha pogut crear " << file_path << '\n'; Log::error("debug.yaml", "no s'ha pogut crear {}", file_path);
return; return;
} }
file << "# debug.yaml — ajust personal de dev (no versionat).\n"; file << "# debug.yaml — ajust personal de dev (no versionat).\n";
@@ -173,7 +173,7 @@ namespace DebugConfig {
file << "# clau, comença a la primera (P1-R1).\n"; file << "# clau, comença a la primera (P1-R1).\n";
file << "start_scene: GAME\n"; file << "start_scene: GAME\n";
file << "starting_players: BOTH\n"; file << "starting_players: BOTH\n";
std::cout << "debug.yaml: creat a " << file_path << " amb start_scene=GAME starting_players=BOTH\n"; Log::info("debug.yaml", "creat a {} amb start_scene=GAME starting_players=BOTH", file_path);
} }
} // namespace DebugConfig } // namespace DebugConfig
+4 -3
View File
@@ -5,9 +5,10 @@
#include <string> #include <string>
// Configuració personal de desenvolupament, carregada de `debug.yaml` dins la // Configuració personal de desenvolupament, carregada de `debug.yaml` dins la
// carpeta de sistema (`$XDG_CONFIG_HOME/jailgames/aee_2026/debug.yaml`). No es // carpeta de sistema del joc (vegeu `Director::createSystemFolder` per la
// versiona: cada dev el crea a la seua màquina. Si no existeix, s'aplica el // ubicació real per plataforma). No es versiona: cada dev el crea a la seua
// valor per defecte (GAME) sense warning de nivell error. // màquina. Si no existeix, s'aplica el valor per defecte (GAME) sense warning
// de nivell error.
namespace DebugConfig { namespace DebugConfig {
enum class StartScene : std::uint8_t { LOGO, enum class StartScene : std::uint8_t { LOGO,
+5 -3
View File
@@ -234,7 +234,7 @@ void Editor::ensureEditableEntities() {
// llança si no troba — provem primer i, en cas de miss, carreguem. // llança si no troba — provem primer i, en cas de miss, carreguem.
const auto REL = sp["path"].get_value<std::string>(); const auto REL = sp["path"].get_value<std::string>();
const std::string KEY = std::filesystem::path(REL).filename().string(); const std::string KEY = std::filesystem::path(REL).filename().string();
std::string full = "data/entities/"; std::string full = Defaults::Paths::ENTITIES_DIR;
full += name; full += name;
full += '/'; full += '/';
full += REL; full += REL;
@@ -739,8 +739,10 @@ void Editor::renderEditorHud(const EntityManager& entities) const {
? "(" + std::to_string(cursor_col_) + "," + std::to_string(cursor_row_) + ")" ? "(" + std::to_string(cursor_col_) + "," + std::to_string(cursor_row_) + ")"
: "(--,--)"; : "(--,--)";
const auto LBL = UiConfig::get().hudLabel(); // El HUD ara té tres famílies (P1/CENTER/P2); l'editor n'agafa la
const auto VAL = UiConfig::get().hudValue(); // del CENTER (ambre/or) per ser neutra i contrastar amb el mapa.
const auto LBL = UiConfig::get().hudCenterLabel();
const auto VAL = UiConfig::get().hudCenterValue();
const auto FLASH = UiConfig::get().hudFlash(); const auto FLASH = UiConfig::get().hudFlash();
const int KERN = UiConfig::get().hudFont().kerning; const int KERN = UiConfig::get().hudFont().kerning;
// Mateixos Y que el HUD del joc per simetria visual: línia 1 = 4, // Mateixos Y que el HUD del joc per simetria visual: línia 1 = 4,
+5 -5
View File
@@ -3,13 +3,13 @@
#ifdef _DEBUG #ifdef _DEBUG
#include <cstdint> #include <cstdint>
#include <iostream>
#include <stdexcept> #include <stdexcept>
#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/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -34,7 +34,7 @@ namespace {
if (!s.empty() && s.front() == '#') { s.erase(0, 1); } if (!s.empty() && s.front() == '#') { s.erase(0, 1); }
dst = static_cast<std::uint32_t>(std::stoul(s, nullptr, 0)); dst = static_cast<std::uint32_t>(std::stoul(s, nullptr, 0));
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "EditorConfig: color '" << key << "' invàlid (" << e.what() << ") — ignorat\n"; Log::warn("EditorConfig", "color '{}' invàlid ({}) — ignorat", key, e.what());
} }
} }
@@ -56,7 +56,7 @@ auto EditorConfig::get() -> EditorConfig& {
void EditorConfig::load(const std::string& file_path) { void EditorConfig::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 << "EditorConfig: no s'ha pogut obrir " << file_path << " — s'usaran els defaults compilats\n"; Log::warn("EditorConfig", "no s'ha pogut obrir {} — s'usaran els defaults compilats", file_path);
return; return;
} }
try { try {
@@ -76,9 +76,9 @@ void EditorConfig::load(const std::string& file_path) {
readArgb(c, "picker_hover", picker_hover_argb_); readArgb(c, "picker_hover", picker_hover_argb_);
} }
std::cout << "EditorConfig: carregat des de " << file_path << '\n'; Log::info("EditorConfig", "carregat des de {}", file_path);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "EditorConfig: error parsejant " << file_path << ": " << e.what() << '\n'; Log::error("EditorConfig", "error parsejant {}: {}", file_path, e.what());
} }
} }
+1 -1
View File
@@ -6,7 +6,7 @@
#include <string> #include <string>
// Configuració visual de l'editor de tilemap (Debug-only). Carregada des // Configuració visual de l'editor de tilemap (Debug-only). Carregada des
// de data/editor/editor.yaml; les claus absents cauen als valors compilats // de data/config/editor.yaml; les claus absents cauen als valors compilats
// a Defaults::Editor. Mateix patró que UiConfig (Meyers singleton + load). // a Defaults::Editor. Mateix patró que UiConfig (Meyers singleton + load).
class EditorConfig { class EditorConfig {
public: public:
+111 -6
View File
@@ -8,11 +8,15 @@
#include <string> #include <string>
#include <utility> #include <utility>
#include "core/audio/audio.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/entities/concrete/whip_attack.hpp"
#include "game/entities/entity_factory.hpp" #include "game/entities/entity_factory.hpp"
#include "game/entities/entity_manager.hpp" #include "game/entities/entity_manager.hpp"
#include "game/entities/event_bus.hpp" #include "game/entities/event_bus.hpp"
#include "game/entities/player.hpp" #include "game/entities/player.hpp"
#include "game/map/map.hpp" #include "game/map/map.hpp"
#include "utils/defaults.hpp"
namespace { namespace {
@@ -62,6 +66,24 @@ namespace {
// més d'una lane (cas marginal a velocitats típiques, però pot passar // més d'una lane (cas marginal a velocitats típiques, però pot passar
// amb dt grans). Comparació estricta a un costat: no dispara quan la // amb dt grans). Comparació estricta a un costat: no dispara quan la
// caixa ja es troba alineada a la lane (hi està després del snap). // caixa ja es troba alineada a la lane (hi està després del snap).
// Mapatge WhipAttack::Direction → Mummy::Direction. Viuen amb noms i
// valors paral·lels però són dos enums distints (cada classe modela el
// seu propi domini). Conversió 1-a-1; `NONE` no apareix com a possible
// sortida del whip, però la deixem per coherència defensiva.
auto whipDirToMummyDir(WhipAttack::Direction d) -> Mummy::Direction {
switch (d) {
case WhipAttack::Direction::NORTH:
return Mummy::Direction::NORTH;
case WhipAttack::Direction::SOUTH:
return Mummy::Direction::SOUTH;
case WhipAttack::Direction::EAST:
return Mummy::Direction::EAST;
case WhipAttack::Direction::WEST:
return Mummy::Direction::WEST;
}
return Mummy::Direction::NONE;
}
auto findLaneCross(float prev, float delta, float base, float pitch, int max_idx) -> std::optional<float> { auto findLaneCross(float prev, float delta, float base, float pitch, int max_idx) -> std::optional<float> {
const float NEXT = prev + delta; const float NEXT = prev + delta;
if (delta > 0.0F) { if (delta > 0.0F) {
@@ -84,7 +106,8 @@ Mummy::Mummy(const fkyaml::node& yaml)
: anim_walk_south_(buildAnimation(yaml, "walk_south")), : anim_walk_south_(buildAnimation(yaml, "walk_south")),
anim_walk_north_(buildAnimation(yaml, "walk_north")), anim_walk_north_(buildAnimation(yaml, "walk_north")),
anim_walk_east_(buildAnimation(yaml, "walk_east")), anim_walk_east_(buildAnimation(yaml, "walk_east")),
anim_walk_west_(buildAnimation(yaml, "walk_west")) { anim_walk_west_(buildAnimation(yaml, "walk_west")),
anim_raise_(buildAnimation(yaml, "raise")) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy"); name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
collides_ = true; collides_ = true;
interactable_ = false; interactable_ = false;
@@ -119,6 +142,19 @@ Mummy::Mummy(const fkyaml::node& yaml)
changeDirection(0.0F, 1.0F); changeDirection(0.0F, 1.0F);
} }
// Estat SPAWN inicial: la mòmia naix en SPAWNING amb l'animació
// `raise` activa (16 frames @ 8fps, no-loop). La duració del SPAWN
// la marca la pròpia animació — `Mummy::update` la tickeja, i quan
// `completed()`, crida `endSpawn()` i restaura la `walk_*` per la
// direcció actual. Passem 0 al countdown del DynamicHazard perquè
// no s'utilitza en aquest cas (el nostre `update` retorna abans).
enterSpawn(0.0F);
anim_ = anim_raise_;
if (anim_.has_value()) {
anim_->reset();
flip_ = anim_->flipMode();
}
// Configuració de l'estat HURT: opcional. Si el bloc no apareix, la // Configuració de l'estat HURT: opcional. Si el bloc no apareix, la
// mòmia no entrarà mai a HURT efectiu (defensiu); el WhipAttack la // mòmia no entrarà mai a HURT efectiu (defensiu); el WhipAttack la
// pot ignorar amb `isWhippable() == false` si volem que no reacció. // pot ignorar amb `isWhippable() == false` si volem que no reacció.
@@ -133,6 +169,14 @@ Mummy::Mummy(const fkyaml::node& yaml)
if (h.contains("whippable")) { if (h.contains("whippable")) {
whippable_ = h["whippable"].get_value<bool>(); whippable_ = h["whippable"].get_value<bool>();
} }
// So opcional: el basename val tant de clau al Resource::Cache com de
// path (`Defaults::Paths::AUDIO_SOUNDS_DIR + key`). Mateix patró que
// els sons del Player. Si el camp manca, la clau queda buida i
// `enterHurt` no dispara playSound.
if (h.contains("sound")) {
hurt_config_.sound_key = h["sound"].get_value<std::string>();
Resource::Cache::get().loadSound(hurt_config_.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + hurt_config_.sound_key);
}
} }
} }
@@ -211,9 +255,24 @@ void Mummy::step(float delta_time) {
const float DX = dir_x_ * speed_ * delta_time; const float DX = dir_x_ * speed_ * delta_time;
const float DY = dir_y_ * speed_ * delta_time; const float DY = dir_y_ * speed_ * delta_time;
// Si no té direcció (p.ex. YAML sense `initial_direction`), el frame és // Sense direcció: pot ser un setup degenerat (YAML sense
// no-op fins que algú la injecte amb `setDirection`. // `initial_direction`) o el residu d'un HURT que va sortir amb
if (DX == 0.0F && DY == 0.0F) { return; } // `chooseHurtDirection() == NONE` (totes les direccions provades
// bloquejades). En aquest segon cas la mòmia quedaria eternament quieta
// — `setDirection(0,0)` a `enterHurt` no toca `anim_`, així que es
// veuria animant-se sense moure's. Re-decidim com a una intersecció:
// si trobem alguna perpendicular vàlida, la prenem; si no, mitja volta.
// Si tampoc, mantenim (0,0) i tornem a provar el proper frame —
// pràcticament impossible amb la geometria actual, però no enganxa.
if (DX == 0.0F && DY == 0.0F) {
auto pools = collectDirectionPools(speed_ * delta_time, /*avoid_blocked=*/true);
if (!pools.perp.empty()) {
applyChoice(chooseDirection(pools.perp));
} else if (!pools.uturn.empty()) {
applyChoice(chooseDirection(pools.uturn));
}
return;
}
// Branca creuament: si aquest frame travessa la línia central d'un // Branca creuament: si aquest frame travessa la línia central d'un
// corredor perpendicular, amarra la caixa al centre del creuament i // corredor perpendicular, amarra la caixa al centre del creuament i
@@ -483,7 +542,19 @@ void Mummy::applyChoice(Direction dir) {
} }
} }
auto Mummy::onWhipHit(WhipAttack* attack) -> bool {
// Defensiu: el filtre `shouldReceiveContact` ja salta DynamicHazards en
// SPAWN, però blindem aquesta crida si arribara per un camí no central.
if (isSpawning()) { return false; }
if (!whippable_ || attack == nullptr) { return false; }
enterHurt(whipDirToMummyDir(attack->getDirection()));
return true;
}
void Mummy::onPlayerContact(Player* player) { void Mummy::onPlayerContact(Player* player) {
// Defensiu: el filtre central a EntityManager::checkPlayerCollisions ja
// salta DynamicHazards en SPAWN, però blindem la crida directa.
if (isSpawning()) { return; }
if (bus_ == nullptr) { return; } if (bus_ == nullptr) { return; }
const int PID = (player != nullptr) ? player->getId() : -1; const int PID = (player != nullptr) ? player->getId() : -1;
bus_->publish(Event{ bus_->publish(Event{
@@ -508,9 +579,16 @@ void Mummy::onPlayerContact(Player* player) {
} }
void Mummy::enterHurt(Direction flee_dir) { void Mummy::enterHurt(Direction flee_dir) {
// Defensiu: el whip no hauria de reaccionar contra una mòmia en SPAWN
// (el filtre de col·lisió la salta), però blindem per si algun camí
// futur crida directament.
if (isSpawning()) { return; }
state_ = State::HURT; state_ = State::HURT;
hurt_remaining_s_ = hurt_config_.duration_s; hurt_remaining_s_ = hurt_config_.duration_s;
hurt_speed_ = (hurt_config_.speed_px_per_s > 0.0F) ? hurt_config_.speed_px_per_s : speed_; hurt_speed_ = (hurt_config_.speed_px_per_s > 0.0F) ? hurt_config_.speed_px_per_s : speed_;
if (!hurt_config_.sound_key.empty() && Audio::get() != nullptr) {
Audio::get()->playSound(hurt_config_.sound_key, Audio::Group::GAME);
}
const Direction CHOSEN = chooseHurtDirection(flee_dir); const Direction CHOSEN = chooseHurtDirection(flee_dir);
if (CHOSEN == Direction::NONE) { if (CHOSEN == Direction::NONE) {
@@ -598,13 +676,40 @@ void Mummy::hurtStep(float delta_time) {
return; return;
} }
// Topa amb mur o sarc: surt del HURT immediatament. La IA normal // Topa amb mur del playfield: amarra la BOX a la vora exacta abans de
// re-decideix al pròxim step (no fem U-turn ni re-orientació aquí). // transitar a ROAMING. Sense aquest snap, el drift sub-pixel acumulat a
// la cursa (DX = -hurt_speed·dt rarament és enter al joc real, on dt ve
// de SDL_GetTicks) deixaria la BOX a un offset de 0.x px dins de
// l'última cel·la. Com que la BOX és 16×16 i els passadissos també ho
// són, qualsevol drift fa que la cantonada sobresurti cap als sarcs
// adjacents — i el `chooseHurtDirection` del proper fueteig classifica
// les perpendiculars com a bloquejades per micro-overlap, retornant
// NONE i deixant la mòmia eternament quieta. Sarc-collision durant HURT
// és un cas molt menys habitual (la cursa va sempre per corredor recte);
// si arriba a passar, l'snap al playfield no l'arregla però la branca
// de recovery a `step()` el cobreix.
if (!Map::insidePlayfield(CANDIDATE)) { snapToPlayfieldEdge(DX, DY); }
state_ = State::ROAMING; state_ = State::ROAMING;
hurt_remaining_s_ = 0.0F; hurt_remaining_s_ = 0.0F;
} }
void Mummy::update(float delta_time) { void Mummy::update(float delta_time) {
if (isSpawning()) {
// SPAWN dirigit per animació: la `raise` és no-loop, així que
// `completed()` ens marca el final. Mentre dura, NO cridem
// `DynamicHazard::update`: la base hi té el countdown i ja
// hauria saltat a ACTIVE (passem `enterSpawn(0)`), i el `step()`
// d'IA mouria la mòmia abans de "aixecar-se". En completar,
// tornem a la `walk_*` que toca segons la direcció ja triada
// al ctor (initial_direction de l'IA).
if (anim_.has_value()) { anim_->tick(delta_time); }
if (anim_.has_value() && anim_->completed()) {
endSpawn();
changeDirection(getDirX(), getDirY());
}
return;
}
if (state_ == State::HURT) { if (state_ == State::HURT) {
// Avancem l'animació inline (no podem cridar `Entity::update` ni // Avancem l'animació inline (no podem cridar `Entity::update` ni
// `DynamicHazard::update`: el primer salta una capa virtual i el // `DynamicHazard::update`: el primer salta una capa virtual i el
+14
View File
@@ -3,6 +3,7 @@
#include <climits> #include <climits>
#include <cstdint> #include <cstdint>
#include <optional> #include <optional>
#include <string>
#include <vector> #include <vector>
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
@@ -35,6 +36,7 @@ class Mummy : public DynamicHazard {
struct HurtConfig { struct HurtConfig {
float duration_s{0.0F}; float duration_s{0.0F};
float speed_px_per_s{0.0F}; float speed_px_per_s{0.0F};
std::string sound_key; // Buit = sense so. El fitxer viu a Defaults::Paths::AUDIO_SOUNDS_DIR.
}; };
explicit Mummy(const fkyaml::node& yaml); explicit Mummy(const fkyaml::node& yaml);
@@ -62,6 +64,12 @@ class Mummy : public DynamicHazard {
// Contacte amb el jugador: publica `player_hit` amb el dany. // Contacte amb el jugador: publica `player_hit` amb el dany.
void onPlayerContact(Player* player) override; void onPlayerContact(Player* player) override;
// Contacte amb el fueteig: si la mòmia és whippable, entra a HURT amb
// la direcció derivada del whip. Retorna true si efectivament ha
// rebut dany — el WhipAttack ho usa per al `already_hit_`. Substitueix
// el `dynamic_cast<Mummy*>` que feia el whip abans per discriminar.
auto onWhipHit(WhipAttack* attack) -> bool override;
// Activa l'estat HURT amb la direcció de fuga preferida `flee_dir`. // Activa l'estat HURT amb la direcció de fuga preferida `flee_dir`.
// Si està bloquejada, prova perpendiculars; si totes estan bloquejades, // Si està bloquejada, prova perpendiculars; si totes estan bloquejades,
// la mòmia queda quieta però en HURT durant tot el timer. // la mòmia queda quieta però en HURT durant tot el timer.
@@ -169,6 +177,12 @@ class Mummy : public DynamicHazard {
AnimationComponent anim_walk_east_; AnimationComponent anim_walk_east_;
AnimationComponent anim_walk_west_; AnimationComponent anim_walk_west_;
// Animació d'aparició (no-loop). Mentre `isSpawning()`, `update()`
// la tickeja sense moure la mòmia; quan `completed()`, la mòmia
// transita a ACTIVE via `endSpawn()` i conmuta a la walk_* que
// toca segons direcció.
AnimationComponent anim_raise_;
// Estat HURT i la seua configuració. `hurt_speed_` és l'efectiva // Estat HURT i la seua configuració. `hurt_speed_` és l'efectiva
// (resultat d'aplicar el `speed` del bloc `hurt:` o, si val 0, la // (resultat d'aplicar el `speed` del bloc `hurt:` o, si val 0, la
// velocitat normal de la mòmia). // velocitat normal de la mòmia).
@@ -1,13 +1,26 @@
#include "game/entities/concrete/sarcophagus.hpp" #include "game/entities/concrete/sarcophagus.hpp"
#include <random>
#include <string> #include <string>
#include <vector> #include <vector>
#include "core/audio/audio.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/entities/entity_factory.hpp" #include "game/entities/entity_factory.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/entities/event_bus.hpp" #include "game/entities/event_bus.hpp"
#include "utils/defaults.hpp"
namespace { namespace {
// RNG local al mòdul, mateix patró que `mummy.cpp`: llavor fixa per
// facilitar la depuració. Si algun dia volem aleatoritat real entre
// partides, prou amb llegir-la de std::random_device.
auto rng() -> std::mt19937& {
static std::mt19937 k_rng_{0xCAFEU};
return k_rng_;
}
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent { auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
std::vector<int> frames = {0}; std::vector<int> frames = {0};
float fps = 1.0F; float fps = 1.0F;
@@ -56,6 +69,20 @@ Sarcophagus::Sarcophagus(const fkyaml::node& yaml)
interactable_ = true; interactable_ = true;
readTileSize(yaml, tile_size_w_, tile_size_h_); readTileSize(yaml, tile_size_w_, tile_size_h_);
anim_ = anim_closed_; anim_ = anim_closed_;
// Sons opcionals (bloc `sound:` del YAML). El basename val tant de
// clau al Resource::Cache com de path dins `Defaults::Paths::AUDIO_SOUNDS_DIR`.
// Mateix patró que `mummy.hurt.sound` i `player.jump.sound`.
if (yaml.contains("sound")) {
const auto& s = yaml["sound"];
if (s.contains("opening")) {
opening_sound_key_ = s["opening"].get_value<std::string>();
Resource::Cache::get().loadSound(opening_sound_key_, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + opening_sound_key_);
}
if (s.contains("opening_pitch_spread")) {
opening_pitch_spread_ = s["opening_pitch_spread"].get_value<float>();
}
}
} }
void Sarcophagus::place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked) { void Sarcophagus::place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked) {
@@ -121,6 +148,30 @@ void Sarcophagus::notifyTilePainted(int col, int row) {
state_ = State::OPENING; state_ = State::OPENING;
anim_ = anim_opening_; anim_ = anim_opening_;
if (anim_.has_value()) { anim_->reset(); } if (anim_.has_value()) { anim_->reset(); }
if (!opening_sound_key_.empty() && Audio::get() != nullptr) {
// Variació de pitch/velocitat per evitar la fatiga d'escoltar
// el mateix sample idèntic a cada obertura. Si l'spread és 0
// (default), reproduïm amb ratio 1.0; en cas contrari sortegem
// uniformement [1 - spread, 1 + spread].
if (opening_pitch_spread_ > 0.0F) {
std::uniform_real_distribution<float> dist(1.0F - opening_pitch_spread_, 1.0F + opening_pitch_spread_);
Audio::get()->playSound(opening_sound_key_, Audio::Group::GAME, dist(rng()));
} else {
Audio::get()->playSound(opening_sound_key_, Audio::Group::GAME);
}
}
}
}
void Sarcophagus::onRemoved() {
// El manager comptabilitza els sarcòfags actius en `total_sarcs_` per
// saber quan ha d'obrir la porta automàticament. Quan el editor
// n'esborra un, cal informar-lo: sense aquesta crida, `total_sarcs_`
// quedaria descompassat i la porta no s'obriria mai. Substitueix el
// `getName() == "sarcophagus"` que el manager feia abans —
// polimorfisme net, no comparació de strings.
if (entity_manager_ != nullptr) {
entity_manager_->onSarcophagusRemoved();
} }
} }
@@ -2,6 +2,7 @@
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
#include <string>
#include <unordered_set> #include <unordered_set>
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
@@ -43,6 +44,7 @@ class Sarcophagus : public Interactable {
void update(float delta_time) override; void update(float delta_time) override;
void notifyTilePainted(int col, int row) override; void notifyTilePainted(int col, int row) override;
void onRemoved() override;
[[nodiscard]] auto getState() const -> State { return state_; } [[nodiscard]] auto getState() const -> State { return state_; }
[[nodiscard]] auto getPerimeterSize() const -> std::size_t { return perimeter_tiles_.size(); } [[nodiscard]] auto getPerimeterSize() const -> std::size_t { return perimeter_tiles_.size(); }
@@ -82,5 +84,12 @@ class Sarcophagus : public Interactable {
AnimationComponent anim_opening_; AnimationComponent anim_opening_;
AnimationComponent anim_open_; AnimationComponent anim_open_;
// Clau (basename del fitxer) al Resource::Cache del so que es dispara
// a la transició CLOSED → OPENING. Buit ⇒ no sona res.
std::string opening_sound_key_;
// Variació uniforme del ratio de reproducció al voltant de 1.0. Cada
// obertura sorteja [1 - spread, 1 + spread]. 0 ⇒ ratio fix a 1.0.
float opening_pitch_spread_{0.0F};
bool opened_event_emitted_{false}; bool opened_event_emitted_{false};
}; };
+42 -29
View File
@@ -9,6 +9,7 @@
#include "game/entities/entity_factory.hpp" #include "game/entities/entity_factory.hpp"
#include "game/entities/entity_manager.hpp" #include "game/entities/entity_manager.hpp"
#include "game/entities/player.hpp" #include "game/entities/player.hpp"
#include "game/gameplay_config.hpp"
#include "game/map/map.hpp" #include "game/map/map.hpp"
namespace { namespace {
@@ -174,9 +175,16 @@ void WhipAttack::computeDamageRect(Direction dir, float spawn_x, float spawn_y,
// d'atac, fins a topar amb un mur o sarc, o esgotar `range_px`. // d'atac, fins a topar amb un mur o sarc, o esgotar `range_px`.
// L'amplada perpendicular és `tile_size` (els passadissos del joc // L'amplada perpendicular és `tile_size` (els passadissos del joc
// tenen exactament aquesta mida i l'atac és lateral). // tenen exactament aquesta mida i l'atac és lateral).
//
// Si el tweak `whip.damage_pierces_walls` està actiu, la hitbox
// ignora els sarcòfags (passa a través del bloc complet) però
// continua respectant el `insidePlayfield`: fora de la sala no hi
// ha res a colpejar i ampliar-ho generaria visuals/edge cases
// estranys. El mur extern, doncs, sempre retalla.
const auto TILE = static_cast<float>(Map::tile_size); const auto TILE = static_cast<float>(Map::tile_size);
const int MAX_PX = static_cast<int>(std::max(0.0F, range_px)); const int MAX_PX = static_cast<int>(std::max(0.0F, range_px));
const auto MAX = static_cast<float>(MAX_PX); const auto MAX = static_cast<float>(MAX_PX);
const bool PIERCES = GameplayConfig::get().whipDamagePiercesWalls();
float length = 0.0F; float length = 0.0F;
auto try_probe = [&](float l) -> bool { auto try_probe = [&](float l) -> bool {
@@ -198,7 +206,9 @@ void WhipAttack::computeDamageRect(Direction dir, float spawn_x, float spawn_y,
probe = {.x = spawn_x, .y = spawn_y + (MAX - l), .w = TILE, .h = l}; probe = {.x = spawn_x, .y = spawn_y + (MAX - l), .w = TILE, .h = l};
break; break;
} }
return Map::insidePlayfield(probe) && !Map::collides(probe); if (!Map::insidePlayfield(probe)) { return false; }
if (PIERCES) { return true; }
return !Map::collides(probe);
}; };
for (int i = 1; i <= MAX_PX; ++i) { for (int i = 1; i <= MAX_PX; ++i) {
@@ -252,19 +262,10 @@ void WhipAttack::update(float delta_time) {
namespace { namespace {
auto whipDirToMummyDir(WhipAttack::Direction d) -> Mummy::Direction { // (El mapping WhipAttack::Direction → Mummy::Direction viu ara a
switch (d) { // mummy.cpp, on s'usa des de `Mummy::onWhipHit`. Aquí només queda el
case WhipAttack::Direction::NORTH: // mapping a Player::Cardinal — usat per al friendly-fire entre
return Mummy::Direction::NORTH; // jugadors, que no és polimòrfic via Entity.)
case WhipAttack::Direction::SOUTH:
return Mummy::Direction::SOUTH;
case WhipAttack::Direction::EAST:
return Mummy::Direction::EAST;
case WhipAttack::Direction::WEST:
return Mummy::Direction::WEST;
}
return Mummy::Direction::NONE;
}
auto whipDirToPlayerCardinal(WhipAttack::Direction d) -> Player::Cardinal { auto whipDirToPlayerCardinal(WhipAttack::Direction d) -> Player::Cardinal {
switch (d) { switch (d) {
@@ -286,22 +287,24 @@ namespace {
return STATE != Player::State::HURT && STATE != Player::State::EXITED && STATE != Player::State::DISABLED; return STATE != Player::State::HURT && STATE != Player::State::EXITED && STATE != Player::State::DISABLED;
} }
// Tracta una mòmia candidata: si és whippable i la seua caja solapa // Tracta una entitat candidata: si la seva caixa solapa amb
// amb `damage_rect`, la posa a HURT i la marca al set. Extret per // `damage_rect`, delega la decisió a `entity->onWhipHit(attack)`.
// acotar la complexitat cognitiva d'`applyDamage` (clang-tidy llindar). // Cada entitat decideix què fer (Mummy entra HURT, Torch retorna
void hitMummyIfApplicable( // false sense efecte, RollingRock no override → no-op). El retorn
Entity* e, // true marca el set `already_hit` per evitar segons impactes dins del
// mateix swing. Polimorfisme net — sense dynamic_cast.
void hitEntityIfApplicable(
Entity* entity,
const SDL_FRect& damage_rect, const SDL_FRect& damage_rect,
WhipAttack::Direction dir, WhipAttack* attack,
std::unordered_set<void*>& already_hit) { std::unordered_set<void*>& already_hit) {
if (e == nullptr || e->isDead()) { return; } if (entity == nullptr || entity->isDead()) { return; }
if (already_hit.contains(static_cast<void*>(e))) { return; } if (already_hit.contains(static_cast<void*>(entity))) { return; }
auto* mummy = dynamic_cast<Mummy*>(e); const auto BOX = entity->getCollisionRect();
if (mummy == nullptr || !mummy->isWhippable()) { return; }
const auto BOX = mummy->getCollisionRect();
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; } if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
mummy->enterHurt(whipDirToMummyDir(dir)); if (entity->onWhipHit(attack)) {
already_hit.insert(static_cast<void*>(e)); already_hit.insert(static_cast<void*>(entity));
}
} }
void hitPlayerIfApplicable( void hitPlayerIfApplicable(
@@ -311,6 +314,10 @@ namespace {
WhipAttack::Direction dir, WhipAttack::Direction dir,
std::unordered_set<void*>& already_hit) { std::unordered_set<void*>& already_hit) {
if (p == owner || !playerIsTargetable(p)) { return; } if (p == owner || !playerIsTargetable(p)) { return; }
// Tweak `combat.friendly_fire`: si està desactivat, els altres
// jugadors són immunes al látigo aliat. L'owner ja queda exclòs
// per la primera guarda; aquest filtre cobreix només cross-player.
if (!GameplayConfig::get().friendlyFire()) { return; }
if (already_hit.contains(static_cast<void*>(p))) { return; } if (already_hit.contains(static_cast<void*>(p))) { return; }
const auto BOX = p->getBaseBox(); const auto BOX = p->getBaseBox();
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; } if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
@@ -322,8 +329,14 @@ namespace {
void WhipAttack::applyDamage() { void WhipAttack::applyDamage() {
if (entity_manager_ != nullptr) { if (entity_manager_ != nullptr) {
for (auto* e : entity_manager_->getEntities()) { // Iterem totes les entitats. La discriminació la fa cada una via
hitMummyIfApplicable(e, damage_rect_, direction_, already_hit_); // `onWhipHit`: Mummy entra HURT si és whippable, la resta retorna
// false (no-op). El cost d'iterar antorxes/sarcs/forats és una
// crida virtual buida cada una — menyspreable comparat amb la
// claredat del callsite. Si demà un sarc vol trencar-se al
// fueteig, només cal override de `onWhipHit` allà.
for (auto* entity : entity_manager_->getEntities()) {
hitEntityIfApplicable(entity, damage_rect_, this, already_hit_);
} }
} }
if (players_ != nullptr) { if (players_ != nullptr) {
+25
View File
@@ -28,6 +28,31 @@ void Entity::notifyTilePainted(int /*col*/, int /*row*/) {
// No-op per defecte; només `Sarcophagus` l'usa. // No-op per defecte; només `Sarcophagus` l'usa.
} }
auto Entity::onWhipHit(WhipAttack* /*attack*/) -> bool {
// No-op per defecte; les entitats no-whippables (Torch, Hole, Sarc,
// Door, RollingRock...) retornen `false`. `Mummy` n'és l'override
// efectiu. El caller usa el retorn per marcar `already_hit_` —
// entitats que no han rebut dany NO bloquegen futurs frames del whip.
return false;
}
void Entity::onJustSpawned() {
// No-op per defecte. `DynamicHazard` override per disparar el seu
// `triggerSpawnExit` (anima la fase EMERGING just després de néixer).
}
void Entity::onRemoved() {
// No-op per defecte. `Sarcophagus` override per decrementar el
// comptador `total_sarcs_` del manager.
}
auto Entity::shouldReceiveContact() const -> bool {
// Default: l'entitat rep contacte si és sòlida (`collides_`) o si és
// un trigger d'àrea (`triggers_on_overlap_`). `DynamicHazard` afegeix
// el filtre de SPAWN — mentre està emergint, ignora contactes.
return collides_ || triggers_on_overlap_;
}
auto Entity::getCollisionRect() const -> SDL_FRect { auto Entity::getCollisionRect() const -> SDL_FRect {
if (!collision_box_.has_value()) { return pixel_rect_; } if (!collision_box_.has_value()) { return pixel_rect_; }
return { return {
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff. Show more