Commit Graph
470 Commits
Author SHA1 Message Date
JailDesigner db2e5d66e0 PATCH-04: backup <path>.bak abans de reescriure config/presets per fallada
Helper `backupFile(path)` que copia el YAML a `<path>.bak` si existeix.
Es crida a `loadFromFile` (versió mismatch i catch) i a
`loadPresetsFromFile<T>` (manca `presets:`, llista buida, catch).
La primera execució no genera .bak. Cada reescriptura forçada registra el
motiu per stderr (INCONS-02 migrarà a std::cout).

Prova manual: config.yaml amb contingut \"===broken===\" genera
config.yaml.bak amb el contingut previ i un nou config.yaml amb defaults.
2026-04-20 17:40:39 +02:00
JailDesigner 505c20dabc DUP-01: pipeline unificat de presets PostFX i CrtPi a options.cpp
Afegits `loadPresetsFromFile<T>` i `savePresetsToFile<T>` a l'anonymous
namespace; els quatre entry-points passen a ser one-liners que injecten
parse/write/defaults per tipus. Abans eren dues còpies del mateix codi
diferenciades només pel tipus concret de preset. Els YAML regenerats són
byte-a-byte idèntics als anteriors (el formatter i els comentaris es
preserven).
2026-04-20 17:37:58 +02:00
JailDesigner ab7a79dc50 COUP-01: notification_presets.hpp mogut de game/ui/ a core/rendering/
El fitxer definia presets del Notifier (que ja viu a core/rendering/), però
estava a game/ i era consumit únicament per core/input/global_inputs.cpp
— una dependència invertida core→game. El mogueu al costat del component
que consumeix; el comentari de `notifier.hpp` hi apunta. `game/ui/` es
queda només amb `ui_palette.hpp`.
2026-04-20 17:35:03 +02:00
JailDesigner e6c7f0dcfb HC-03: geometria del mapa traslladada de map.hpp al YAML de la piràmide
El bloc `geometry:` de pyramid.yaml porta les 9 constants base (grid, sarc,
pitch, HUD, murs); els derivats (corridor, board, origin, playfield) es
recalculen al `Map::loadFrom()`. `constexpr` passa a `const` on calia i
els membres estàtics adopten snake_case. Desbloqueja la iteració
"pirámide a pirámide" sense recompilar.
2026-04-20 17:32:56 +02:00
JailDesigner b895475716 Auditoria game/: llista completa de 38 troballes en docs/audit-game.md
Inventari de duplicació, hardcoding, parxes, acoplament, inconsistències,
codi mort i violacions de convencions. Tots els ítems PENDING; els commits
següents en tancaran un a un.
2026-04-20 17:25:04 +02:00
JailDesigner bad71ec4a9 Refactor core/audio: Ja::Engine encapsula l'estat del motor (rama refactor/audio-engine-class)
Tanca els DEFERRED de l'auditoria: COUP-01 (engine sense globals),
COUP-02 (header-only → .hpp/.cpp), PATCH-03 (stb_vorbis macros aïllades),
CONV-02 (engine ja és classe), CONV-03 (callback music-ended),
INCONS-05 complet (silenciar per gain, sense flags de motor).

5 commits atòmics amb build + xvfb entre cadascun. Detalls: plan a
/home/sergio/.claude/plans/crea-un-plan-para-twinkling-bubble.md i
docs/audit-core-audio.md actualitzat.

Únic DEFERRED restant de l'auditoria: PATCH-05 (voice-stealing policy)
— és feature, no deute.
2026-04-20 13:39:50 +02:00
JailDesigner 17301aff66 Auditoria de core/audio: neteja general aplicada (rama audit/core-audio)
17 ítems DONE (HC-01..HC-05 exc. HC-04 WONTFIX, DUP-01..DUP-05, PATCH-01/02/04,
INCONS-01/02/03/05-parcial, COUP-03, DEAD-01..05) + 1 parcial (INCONS-05).

Detalls i resolucions: docs/audit-core-audio.md.
2026-04-20 13:37:45 +02:00
JailDesignerandClaude Opus 4.7 11473588de docs(audit): tancar COUP-01/02, PATCH-03, CONV-02/03, INCONS-05 complet
Actualitza audit-core-audio.md amb els estats finals després del refactor
a refactor/audio-engine-class:

  - COUP-01 (encapsular Ja::* globals): DONE — Ja::Engine classe RAII
  - COUP-02 (header-only): DONE — jail_audio.cpp nou amb stb_vorbis.c
  - PATCH-03 (#undef macros): DONE — migrats a jail_audio.cpp
  - CONV-02 (engine no és classe): DONE — resolt amb COUP-01
  - CONV-03 (callback music-ended): DONE — setOnMusicEnded
  - INCONS-05 (enable/disable divergent): DONE complet — Audio silencia
    per gain, flags de motor eliminats

Queda un sol ítem DEFERRED: PATCH-05 (voice-stealing policy), que és una
feature, no deute.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:33:59 +02:00
JailDesignerandClaude Opus 4.7 2ec6fd23b6 feat(core/audio): music-ended callback (CONV-03)
Ja::Engine exposa setOnMusicEnded(std::function<void()>). Quan
updateCurrentMusic detecta que la pista actual ha acabat de drenar
(times == 0 && stream buit) s'atura primer via stopMusic i després
s'invoca el callback — així un eventual playMusic des del callback pot
començar amb l'engine en estat net.

Audio proxeia amb setOnMusicEnded. Contracte documentat: s'executa al
mateix thread que Audio::update (render loop); no fer operacions
blocants.

Cap consumidor actualment; només exposa l'API per quan calgui.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:31:47 +02:00
JailDesignerandClaude Opus 4.7 4e0dcfe739 refactor(core/audio): remove engine enable flags (INCONS-05)
Ja::Engine tenia music_enabled_ i sound_enabled_ com a membres amb el
seu propi enableMusic/enableSound setter, i disseminava checks
`if (!music_enabled_)` / `if (!sound_enabled_)` per gairebé totes les
operacions. Després del commit 3, ningú crida aquests setters: Audio
gestiona el gating per gain (effectiveVolume retorna 0 quan
music_enabled_/sound_enabled_ d'Audio són false). L'usuari ha confirmat
que vol mantenir aquesta semàntica (silenciar per gain, no parar).

Es retira tot el camí mort:

  - Membres music_enabled_ / sound_enabled_ eliminats.
  - Mètodes Engine::enableMusic(bool) / Engine::enableSound(bool)
    eliminats.
  - Checks `if (!music_enabled_)` / `if (!sound_enabled_)` eliminats de
    playMusic, pauseMusic, resumeMusic, fadeOutMusic, crossfadeMusic,
    updateCurrentMusic, playSound, playSoundOnChannel, pauseChannel,
    resumeChannel, updateSoundChannels.
  - Enum MusicState::DISABLED eliminat (inabastable sense el flag).
  - Enum ChannelState::INVALID i SOUND_DISABLED eliminats (no usats
    després de DEAD-04).
  - Audio::getMusicState treu el case DISABLED del switch.

Audio::enableMusic/enableSound i els seus flags music_enabled_/
sound_enabled_ d'Audio romanen — són ells els qui porten la decisió al
motor via setXxxVolume(config_.X_volume) → effectiveVolume → gain.
Tanca la meitat restant d'INCONS-05.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:29:00 +02:00
JailDesignerandClaude Opus 4.7 961a59c460 refactor(core/audio): Audio owns the engine (migration)
Audio guanya un std::unique_ptr<Ja::Engine> engine_ com a membre privat.
Es construeix a initSDLAudio amb els defaults de Defaults::Audio; es
destrueix sol al ~Audio (cap Ja::quit() explícit).

Totes les crides Ja::funcióN(...) d'audio.cpp passen a engine_->funcióN:
  - playMusicInternal: crossfadeMusic/stopMusic/playMusic
  - pauseMusic, resumeMusic, stopMusic, fadeOutMusic
  - playSound x2, stopAllSounds
  - setSoundVolume, setMusicVolume
  - getMusicState (via instance->engine_ — és static però pot arribar-hi)

Les free functions forwarder del commit 2 (Ja::init/quit/update/playMusic/
pauseMusic/resumeMusic/stopMusic/fadeOutMusic/crossfadeMusic/
getMusicState/setMusicVolume/enableMusic i els equivalents de so) es
retiren del .hpp i del .cpp: ja no les crida ningú.

Queden com a free functions permanents (les usa Resource::Cache):
loadMusic (x2), loadSound, deleteMusic, deleteSound. Aquests darrers
consulten Engine::active() per notificar el motor abans d'alliberar.

audio.hpp forward-declara també `class Ja::Engine` perquè engine_ és
un std::unique_ptr i el dtor es defineix out-of-line al .cpp (= default)
— així no cal incloure jail_audio.hpp al header.

Els NOLINT(readability-make-member-function-const) d'audio.cpp es
mantenen amb raó actualitzada: std::unique_ptr::operator-> retorna T*
(no const T*), per tant clang-tidy veu un mètode d'Audio que podria ser
const encara que muti l'Engine via el punter. La mutació és real; el
NOLINT segueix sent honest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:25:03 +02:00
JailDesignerandClaude Opus 4.7 1d124e967d refactor(core/audio): introduce Ja::Engine class (COUP-01, CONV-02)
Encapsula l'estat del motor a Ja::Engine — classe amb RAII (ctor obre
SDL_AudioDevice, dtor el tanca) i `assert` al ctor contra instàncies
concurrents. Tots els globals del namespace anònim del commit 1
(current_music, channels[], audio_spec, music_volume, sound_volume[],
music_enabled, sound_enabled, sdl_audio_device, outgoing_music,
incoming_fade) es converteixen en membres privats amb sufix `_`. Els
helpers que toquen estat (stealCurrentIntoOutgoing,
updateOutgoing/IncomingFade, updateCurrent*, forEachTargetChannel)
passen a mètodes privats. Els stateless (feedMusicChunk, pumpMusic,
preFillOutgoing, fadeProgress) es queden al namespace anònim del .cpp.

Engine::active() static accessor: registrat al ctor, desregistrat al
dtor. Els deleters Ja::deleteMusic/deleteSound consulten Engine::active()
per parar canals/música abans d'alliberar recursos; si active() és
nullptr (shutdown-order invertit), fan cleanup local silenciós. Això
resol CONV-02 (engine ja és classe) i tanca COUP-01.

Compat amb els callers: les free functions Ja::init/quit/update,
Ja::playMusic, Ja::pauseMusic, Ja::resumeMusic, Ja::stopMusic,
Ja::fadeOutMusic, Ja::crossfadeMusic, Ja::getMusicState,
Ja::setMusicVolume, Ja::enableMusic, Ja::playSound, Ja::playSoundOnChannel,
Ja::pauseChannel, Ja::resumeChannel, Ja::stopChannel, Ja::setSoundVolume
i Ja::enableSound es mantenen com a thin forwarders contra Engine::active().
audio.cpp no canvia en aquest commit; les seves crides Ja:: continuen
funcionant. Al commit 3 Audio agafarà ownership directe via
std::unique_ptr<Engine> i aquests forwarders desapareixeran.

Ja::init() crea un std::unique_ptr<Engine> propietari dins d'un namespace
anònim del .cpp (encara no és Audio qui posseeix — això és el commit 3).
Ja::loadMusic/loadSound/deleteMusic/deleteSound es queden com a free
functions permanents (no depenen del motor per construir).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:21:01 +02:00
JailDesignerandClaude Opus 4.7 bc53174f2b refactor(core/audio): split jail_audio.hpp/.cpp (COUP-02, PATCH-03)
Preparatori per COUP-01. No canvia comportament: només mou les
definicions inline de jail_audio.hpp a un jail_audio.cpp nou, deixant
el .hpp amb declaracions + tipus + constants.

Efectes col·laterals que es tanquen gratis:

  - COUP-02: jail_audio deixa de ser header-only. Abans cada TU que
    l'incloïa compilava 650 línies inline (amb stb_vorbis.c en mode
    HEADER_ONLY). Ara el .hpp té ~130 línies de declaracions.
  - PATCH-03: #include "external/stb_vorbis.c" + #undef L/C/R/PLAYBACK_*
    viu només al final de jail_audio.cpp. audio.cpp perd el workaround
    dels undefs.

Estat intern (current_music, channels[], audio_spec, music_volume,
sound_volume[], music_enabled, sound_enabled, sdl_audio_device,
outgoing_music, incoming_fade) baixa al .cpp dins d'un namespace
anònim (internal linkage). Els helpers privats (feedMusicChunk,
pumpMusic, preFillOutgoing, fadeProgress, update*Fade, updateCurrent*,
stealCurrentIntoOutgoing, forEachTargetChannel) també viuen a
l'anonymous namespace.

Ja::Music i Ja::Sound són públics; el .hpp forward-declara
`struct stb_vorbis` perquè els callers només els agafen per punter.
NOLINT(readability-identifier-naming) al forward-decl perquè el nom
l'imposa stb_vorbis.

jail_audio.cpp s'afegeix a APP_SOURCES al CMakeLists.txt.

.clang-tidy: desactivat clang-analyzer-core.UndefinedBinaryOperatorResult
globalment. Fals positiu conegut a external/stb_vorbis.c que abans no es
disparava (no hi havia TU que cridés directament stb_vorbis_seek_start
des del mateix fitxer on la funció està inlined); ara que jail_audio.cpp
sí que fa aquestes crides directes, el path d'anàlisi passa pel .c i el
check dispara. HeaderFilterRegex no aplica a clang-analyzer-*.

Preparació per COUP-01 (introduir Ja::Engine al commit següent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:11:15 +02:00
JailDesignerandClaude Opus 4.7 5610814c9f docs(audit): tancar audit-core-audio.md amb estat final
17 ítems DONE (+1 parcial INCONS-05), 4 WONTFIX (HC-04, PATCH-03,
COUP-04, COUP-05, INCONS-04), 4 DEFERRED (COUP-01/02, PATCH-05,
CONV-02/03). CONV-01 passa a DONE parcial — la part del motor reutilizable
no es mou a Defaults per disseny.

Tots els DEFERRED pengen del refactor COUP-01 (encapsular Ja::* globals
en una classe), que tancarà també COUP-02, PATCH-03, PATCH-05, CONV-02,
CONV-03 i la meitat restant d'INCONS-05 (la divergència Ja::enable* vs
Audio::enable*).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:29:37 +02:00
JailDesignerandClaude Opus 4.7 0229468cc5 docs(core/audio): unificar comentaris a català (INCONS-03)
Els comentaris d'audio.hpp i audio.cpp barrejaven castellà i català —
"Reproduce la música", "Detener", "Estado de la música"... conviven amb
"Reproduir", "Aturar", "Estat". Unifico tot a català, incloent-hi:

  - Comentaris d'includes ("Para X" → "Per a X").
  - Secció headers ("// --- Configuración general ---" → "// --- Configuració
    general ---").
  - Descripcions de mètodes i membres.
  - Comentaris de l'enum Group/MusicState.

Els comentaris que ja estaven en català (els afegits a la mateixa
auditoria i els preexistents) no es toquen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:28:17 +02:00
JailDesignerandClaude Opus 4.7 d03e9fffdc refactor(core/audio): eliminar 4 sobrecàrregues enable/disable no-arg (INCONS-05/DEAD-05)
enableSound()/disableSound()/enableMusic()/disableMusic() eren inline
thunks que només cridaven la versió amb bool. Cap caller del projecte
les invoca. Es deixa només enableSound(bool) + toggleSound() (i
l'equivalent per música) com a API única.

Queda pendent la divergència semàntica amb Ja::enableSound/enableMusic
(Audio silencia per gain, Ja pararia els canals). No es resol aquí
perquè és una decisió de disseny que lliga amb l'encapsulament de
Ja::* (COUP-01).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:24:55 +02:00
JailDesignerandClaude Opus 4.7 3089177e69 refactor(core/audio): eliminar 5 funcions mortes a Ja:: (DEAD-01..04, PATCH-04)
Verificat amb grep que cap caller dins de source/ invoca:
  - Ja::setMusicPosition (stub no-op)
  - Ja::getMusicPosition (stub que retornava 0)
  - Ja::getMusicFilename (accessor a Music::filename, el camp s'accedeix
    directament des d'Audio)
  - Ja::getChannelState (getter sense consumidor públic)
  - Ja::setVolume (combinació master — Audio fa el gate per canal)

Es manté loadMusic(buffer, length, filename) perquè Resource::Cache
l'utilitza per registrar el path dins del Ja::Music.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:22:41 +02:00
JailDesignerandClaude Opus 4.7 355fcfb9de refactor(core/audio): iostream → cstdio per als missatges d'error (COUP-03)
jail_audio.hpp és un header inline que arrossegava <iostream> a cada
TU que l'incloïa, només per a std::cout de missatges d'error. <iostream>
és un include pesat i aquí no aporta res: estem fent logging simple.

Passem a <cstdio> + std::fprintf(stderr, ...) als dos fitxers (jail_audio
i audio.cpp). Missatges prefixats "Ja::funció:" / "Audio:" seguint la
convenció "Module: message" que ja fan servir director.cpp, options.cpp,
resource_cache.cpp, etc.

Efecte col·lateral: stderr és l'stream correcte per als errors (abans
anaven a stdout).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:20:08 +02:00
JailDesignerandClaude Opus 4.7 a917f6660a fix(core/audio): recuperar el nom del Ja::Music::filename (PATCH-02)
L'overload playMusic(Ja::Music*, ...) buidava music_.name a cegues, així
que getCurrentMusicName() mentia després d'aquest camí tot i que el
Ja::Music que li passaves podia portar filename (Ja::loadMusic té un
overload que l'accepta).

Ara copiem music->filename — si el caller va usar l'overload amb
filename, el nom es conserva; si no, queda buit i el contracte és
explícit al comentari.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:17:32 +02:00
JailDesignerandClaude Opus 4.7 66c5801725 refactor(core/audio): Ja::* és l'única font de veritat de MusicState (INCONS-02)
music_.state era una cache d'estat que podia divergir del real —
precisament per això existia getRealMusicState() com a "consulta honesta".
Es descarta la cache:

  - music_.state deixa d'existir; el struct privat Music només manté
    name i loop.
  - getMusicState() passa a ser static i consulta Ja::getMusicState(),
    projectant INVALID/DISABLED al STOPPED que exposa Audio::MusicState.
  - getRealMusicState() desapareix (getMusicState ja fa la consulta real).
  - update() ja no necessita el bloc de sincronització.
  - playMusicInternal/pauseMusic/resumeMusic/stopMusic/fadeOutMusic
    llegeixen getMusicState() en comptes de music_.state i deixen de
    reassignar-lo — Ja ja ho fa.

Externament: cap caller usa getMusicState/getRealMusicState, així que
canviar a static és segur (Audio::get()->getMusicState() continua
funcionant).

pauseMusic/resumeMusic/stopMusic es marquen amb NOLINT per
readability-make-member-function-const pel mateix motiu que INCONS-01
(muten globals de Ja::* que clang-tidy no veu).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:15:49 +02:00
JailDesignerandClaude Opus 4.7 964ff104ce refactor(core/audio): quitar const que mentia en 6 mètodes (INCONS-01)
playSound (x2), stopAllSounds, fadeOutMusic, setSoundVolume i
setMusicVolume es declaraven const però mutaven l'estat global de Ja::*
(channels[], outgoing_music, music_volume, sound_volume[]). El const no
protegia res: el compilador no detectava les mutacions perquè anaven a
globals, no a membres, i podia induir a error a qui llegia el header.

Els callers externs van via Audio* (no Audio const*), així que el canvi
és source-compatible.

Clang-tidy falla amb readability-make-member-function-const perquè
tampoc veu la mutació — és exactament el símptoma que motiva el
hallazgo. S'afegeix NOLINT localitzat a cada mètode amb la raó. Quan
s'executi COUP-01 (encapsular Ja::* en classe membre) els NOLINT
cauran sols perquè la mutació serà visible al compilador.

effectiveVolume es queda const — és un càlcul pur sobre els membres.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:10:44 +02:00
JailDesignerandClaude Opus 4.7 6a1ed37242 refactor(core/audio): template forEachTargetChannel (DUP-05)
pauseChannel, resumeChannel i stopChannel repetien tres vegades la
cadena "if channel == -1 forEach(channels) else if 0<=channel<MAX
apply()". S'extreu a forEachTargetChannel(channel, fn) i cada funció es
queda només amb la lògica per canal — pausar, reprendre, destruir.

Substitueix codi gairebé idèntic triplicat per una declaració de per
què fa el que fa cada funció.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:07:07 +02:00
JailDesignerandClaude Opus 4.7 dee00a8de7 refactor(core/audio): helper stealCurrentIntoOutgoing (DUP-04)
fadeOutMusic i crossfadeMusic repetien el bloc "destruir outgoing
anterior + preFillOutgoing + robar stream + reset state+vorbis". Es
treu a stealCurrentIntoOutgoing(duration_ms) i els dos callers deleguen.

Com a efecte secundari, homogeneïtza la neteja: abans fadeOutMusic no
posava outgoing.fade.active = false en destruir l'anterior i
crossfadeMusic sí. L'helper fa sempre les dues coses. El comportament
observable no canvia perquè la fade s'assigna completament just després,
però elimina una inconsistència latent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:05:28 +02:00
JailDesignerandClaude Opus 4.7 ee34638c89 refactor(core/audio): helper fadeProgress compartit per fades (DUP-03)
updateOutgoingFade i updateIncomingFade calculaven el mateix PERCENT
lineal (ELAPSED / duration_ms) per separat i prenien decisions sobre si
el fade havia acabat comparant bytes. Ara fadeProgress(FadeState)
retorna [0..1] i concentra la corba del fade — si cal passar-la a
logarítmica o quadràtica, es toca en un sol lloc.

També protegeix duration_ms<=0 com a "fade completat" (abans el divisor
donava una divisió per zero en debug o un progrés >> 1 en release).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:03:10 +02:00
JailDesignerandClaude Opus 4.7 893e5b27e4 refactor(core/audio): helper effectiveVolume compartit (DUP-02)
setSoundVolume i setMusicVolume repetien la mateixa lògica de clamp +
gate (enabled_ && channel_enabled_) + multiplicació pel master
config_.volume. S'extreu effectiveVolume(volume, channel_enabled) privat
i els dos setters es limiten a la crida Ja::*.

Any canvi futur a la política (p.ex. una corba logarítmica) queda en un
sol lloc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:01:06 +02:00
JailDesignerandClaude Opus 4.7 256a232a95 refactor(core/audio): factoritzar overloads playMusic (DUP-01)
Els dos playMusic compartien el dispatch crossfade-vs-stop+play i
l'actualització de music_.state/loop. S'extreu playMusicInternal privat
que rep el Ja::Music* ja resolt i fa el camí comú.

Cada overload es queda només amb el seu gating propi: la variant per
nom fa early-return si ja sona la mateixa pista i posa music_.name; la
variant per punter buida el nom (PATCH-02 recuperarà el filename del
propi Ja::Music).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:59:26 +02:00
JailDesignerandClaude Opus 4.7 a6cd12a944 fix(core/audio): toggle* i enable[Sound|Music] reapliquen volum (PATCH-01)
Abans, toggleEnabled/toggleSound/toggleMusic i les variants sense bool
només flipaven el flag intern; els canals actius no notaven el canvi
fins a la pròxima crida explícita a setSoundVolume/setMusicVolume —
silenciós i fàcil de perdre.

Ara:
  - toggleEnabled passa per enable(bool), que ja reaplicava els volums.
  - enableSound(bool) i enableMusic(bool) es mouen a audio.cpp i
    reapliquen el volum gated cap a Ja::*.
  - Les variants enableSound()/disableSound()/enableMusic()/disableMusic()
    i els toggles passen per aquestes dues funcions, així que qualsevol
    camí d'activació produeix el mateix efecte sobre l'estat sonor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:57:10 +02:00
JailDesignerandClaude Opus 4.7 92f38bc51c refactor(core/audio): eliminar fill(0.5F) sobreescrit a Ja::init (HC-05)
sound_volume[] és un inline float[] amb zero-init per defecte i la capa
Audio crida setSoundVolume(config_.sound_volume) via enable() just
després de Ja::init, així que el fill(0.5F) no arribava mai a l'usuari.
Era un valor màgic heretat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:54:53 +02:00
JailDesignerandClaude Opus 4.7 5939cd3aa3 docs(audit): HC-04 com a WONTFIX (tuning intern del motor)
MUSIC_CHUNK_SHORTS i MUSIC_LOW_WATER_SECONDS són paràmetres del streaming
de vorbis/SDL; viuen dins de jail_audio i moure'ls a Defaults acoplaria
el motor al projecte. Mateix criteri que HC-03.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:54:02 +02:00
JailDesignerandClaude Opus 4.7 621d454563 refactor(core/audio): static_assert Audio::Group ↔ Ja::MAX_GROUPS (HC-03)
Ja::MAX_SIMULTANEOUS_CHANNELS i Ja::MAX_GROUPS són límits interns del
motor. La proposta inicial era moure'ls a Defaults::Audio, però jail_audio
és reutilitzable entre projectes (no depèn d'Options ni d'utils/); moure
els límits trencaria aquesta independència.

El valor real del hallazgo era la invariant Audio::Group ↔ MAX_GROUPS,
que cap codi protegia. S'afegeix un static_assert a audio.cpp que falla
en compilació si s'afegeix una entrada nova a Group sense tocar
MAX_GROUPS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:52:51 +02:00
JailDesignerandClaude Opus 4.7 deb0fc2a4b refactor(core/audio): unificar format/canals/freq a Defaults::Audio (HC-02)
{SDL_AUDIO_S16, 2, 48000} apareixia com a literal a tres llocs de
jail_audio (Sound/Music/audio_spec) i audio.cpp:208 passava SDL_AUDIO_S16LE
i 2 canals per separat — dues convencions de format conviuen i els 2
canals eren número màgic.

Defaults::Audio guanya FORMAT (SDL_AUDIO_S16) i CHANNELS (2). Audio::
initSDLAudio passa ambdós junts a Ja::init. jail_audio obté un fallback
local Ja::DEFAULT_SPEC que consolida les tres instàncies; es queda
independent del projecte (no inclou utils/defaults.hpp) perquè l'engine
és reutilitzable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:50:41 +02:00
JailDesignerandClaude Opus 4.7 6f0626c31f refactor(core/audio): moure constants UI/tuning a Defaults::Audio (HC-01)
VOLUME_STEP, FREQUENCY i DEFAULT_CROSSFADE_MS vivien com a membres
constexpr d'Audio. Eren valors de configuració — no invariants de la
classe — i la convenció del projecte és que tots els paràmetres
ajustables visquin a utils/defaults.hpp.

Passen a Defaults::Audio::{VOLUME_STEP, FREQUENCY, CROSSFADE_MS}. L'únic
ús intern (Ja::init al constructor d'Audio) es reapunta al Defaults. Cap
ús extern dels tres símbols.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:47:40 +02:00
JailDesignerandClaude Opus 4.7 8b73198209 docs(audit): crear audit-core-audio.md con hallazgos inicials
Plantilla d'auditoria per a core/audio/ amb els IDs estables (DUP/HC/
PATCH/COUP/INCONS/DEAD/CONV) i el top-5 prioritari. Mateix format que
l'auditoria de core/rendering/; les resolucions s'aniran anotant a
mesura que s'apliquin els canvis, commit a commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:45:38 +02:00
JailDesigner 0a3ef447ea Auditoria de core/rendering: neteja general aplicada (rama audit/core-rendering)
Recull de 19 commits d'auditoria + 1 doc amb les troballes i el seu
estat. Veure docs/audit-core-rendering.md per al detall per-finding.

Resum per categoria:
- Duplicació: 7 items (surface blit, shaders MSL + factory, Text loops,
  Text glyph resolution, GIF extension switch, GIF double-parse, CRT-Pi
  defaults).
- Hardcoding: 9 items amb nom (INK_COLOR_INDEX, DEFAULT_TEXT_COLOR,
  OPAQUE_ALPHA, LZW/GCT constants, segment indices, position count,
  shadow offset, FONT_SIZE_PX, CRT reference height/subpixel div).
- Parxes: 5 neteja (readBytes comment, Sprite::update inline,
  mono_digits dedup init, GIF extension labels, NOLINTs eliminats).
- Acoblament: 4 desacoblaments (Notifier→game/ui; RenderInfo→Options;
  Screen→SDL3GPUShader concret; Surface→Screen; font+palette injectats).
- Codi mort: 9 items (writeShadowed/writeCentered/glyphWidth/
  getGlyphClip/codepointToUtf8/getSprite/getRect/incX/incY/
  getCurrentAnimationSize/getSurfaceData/Sprite::update/GIF extension
  structs).
- Convencions: shaders MSL a fitxers dedicats amb naming conforme.

Build verificat a cada commit; smoke test de 5s sota xvfb-run a cada
finding (exit 124 = corregut sencer, sense crash).

Items restants DEFERRED (no es tanquen com a cleanup perquè són
features o decisions d'arquitectura global): HC-04 (YAML HUD config),
PATCH-01 (RAII Wayland), PATCH-02 (FSM RenderInfo), COUP-05
(AnimationLoader), COUP-06 (reemplaçar singletons), INC-01/-02/-03
(polítiques globals de smart-pointers, errors i logging).
2026-04-20 10:24:20 +02:00
JailDesignerandClaude Opus 4.7 32d6067752 refactor(core/rendering): injectar font i paleta als overlays (COUP-04) + tancament auditoria
Notifier::init i RenderInfo::init cridaven Resource::Cache::get() i
construïen cadascun la seua pròpia instància de Text a partir de
"8bithud.gif" + "data/font/8bithud.fnt" (strings hardcoded). Notifier
també llegia la paleta "ui" a cada render().

Ara:
- Director construeix una sola std::shared_ptr<Text> compartida.
- RenderInfo::init(Frame, shared_ptr<Text>) i Notifier::init
  (shared_ptr<Text>, const Palette&) reben els recursos injectats.
- Notifier desa la Palette una volta i la consulta localment a
  render(), sense tocar el cache global.

Resultat: els overlays perden la dependència dels noms dels assets
i del cache; el cicle de vida queda controlat pel Director. El camp
intern text_ passa a shared_ptr<Text>.

El document d'auditoria es tanca:
- PATCH-02, COUP-05 → DEFERRED (feature beyond cleanup scope).
- INC-04 → WONTFIX (coexistència justificada de writeDX/write*).
- Estat global: neteja completada.

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:05:58 +02:00
JailDesignerandClaude Opus 4.7 f692739fed refactor(core/rendering): FONT_SIZE_PX compartit a hud_constants.hpp (HC-05)
Notifier::TEXT_SIZE i el TEXT_SIZE del namespace anònim de render_info
declaraven, els dos, "8" com l'alçada del glif de 8bithud. Ara
Rendering::Hud::FONT_SIZE_PX (a core/rendering/hud_constants.hpp) és
l'única font de veritat; Notifier i RenderInfo l'inclouen.

HC-04 (moure la resta de constants HUD a YAML) es marca DEFERRED al
document: és una feature de runtime-config, no pura neteja.

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:01:19 +02:00
JailDesignerandClaude Opus 4.7 e1e8bc11a5 refactor(core/rendering): unificar defaults de CRT-Pi (DUP-07)
Els defaults del shader CRT-Pi es duplicaven entre CrtPiParams
(shader_backend.hpp) i CrtPiUniforms (crtpi_pipeline.hpp). Ara
CrtPiUniforms es value-initializa a zeros i CrtPiPipeline::init(),
un cop el pipeline està creat, aplica setParams(CrtPiParams{}) per
a fixar els defaults canònics abans del primer render. Així queda
una sola font de veritat (els defaults del struct CrtPiParams al
interface ShaderBackend).

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:59:30 +02:00
JailDesignerandClaude Opus 4.7 2350f4060f refactor(core/rendering): nomenar magic numbers al shader CRT-Pi (HC-08)
crtpi_frag.msl tenia la línia
    float filter_width = (768.0f / u.texture_height) / 3.0f;
amb dues constants soltes (768 i 3) sense explicar. Ara es declaren
al mateix shader com CRT_REFERENCE_HEIGHT i SUBPIXEL_DIV amb un
comentari explicant que 768 és l'alçada CRT de referència on es va
tarar el filtre i 3 el nombre de subpíxels (R/G/B) del multisample.

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:58:14 +02:00
JailDesignerandClaude Opus 4.7 da66a7c03f refactor(core/rendering): netejar ctors de Sprite/MovingSprite (INC-06 + INC-07)
- MovingSprite: el ctor delegat Sprite(surface, pos) ja assigna pos_;
  el Sprite::pos_ = pos; manual que seguia era redundant (INC-06).
- Sprite(surface) usava initializer posicional {0.0F, 0.0F, ...} mentre
  la resta del projecte usa designated initializers ({.x = ..., .y = ...}).
  Unificat (INC-07).

INC-05 (el static_cast<int> de pos_.x a MovingSprite::move i setters)
es marca WONTFIX al document: és snap intencional a píxel per al
pixel-art, divergent només al setter per SDL_FRect que es reserva per
a posicionament directe sense arrodoniment.

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:57:17 +02:00
JailDesignerandClaude Opus 4.7 8b413affa8 refactor(core/rendering): Rendering::OPAQUE_ALPHA constant (HC-02)
Surface i Screen repetien el bit d'alpha opac com a 0xFF000000U o com
(0xFFU << 24). S'afegeix inline constexpr Rendering::OPAQUE_ALPHA a
canvas.hpp i s'usa a les tres ocurrències.

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:55:40 +02:00
JailDesignerandClaude Opus 4.7 be94a4390e refactor(core/rendering): neteja del parser GIF i construcció de Surface (DUP-01/-06 + HC-03/-10 + PATCH-05)
- DUP-01: les tres branques del switch d'extensions que saltaven el
  mateix (bloc fix + sub-blocs) es substitueixen per una crida a
  skipExtensionBlock(); com que totes les extensions comparteixen
  layout, el switch desapareix i el codi es redueix a una línia.
- DUP-06: el constructor Surface(file_path) llegia el fitxer dues
  voltes (una via loadSurface per pixels i una altra via
  loadPaletteFromGif per la paleta); ara carrega el buffer una sola
  vegada i n'extrau pixels + paleta en passades separades sobre la
  memòria. Surface::loadSurface passa a no existir públicament (no
  tenia callers externs).
- HC-03: les màscares i talls màgics del decoder LZW i del Screen
  Descriptor passen a constants nominals (LZW_MIN/MAX_CODE_LENGTH,
  LZW_MASK_INITIAL, LZW_MASK_OVERFLOW, SCREEN_GCT_FLAG,
  SCREEN_GCT_SIZE_MASK, GCT_BYTES_PER_ENTRY).
- HC-10: processImageDescriptor tenia gct i resolution_bits
  [[maybe_unused]] vestigials; se'n treu la signatura. L'struct RGB,
  que només servia per a aquests args, també s'elimina.
- PATCH-05: el comentari "reemplaçar macro READ" (migració parcial des
  de C) passa a ser un comentari neutre sobre el que fa readBytes.

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:53:43 +02:00
JailDesignerandClaude Opus 4.7 bbb397abb2 refactor(core/rendering): nomenar constants a RenderInfo (HC-06/-07/-09 + PATCH-08)
render_info.cpp tenia diverses "cifres soltes" amb significat
estructural:
- Índex 0/1/2/3 dins segments_ per a FPS/driver/shader/temps — ara
  FPS_SEGMENT, DRIVER_SEGMENT, SHADER_SEGMENT, TIME_SEGMENT (HC-06).
- +1,+1 per a la ombra del text — ara SHADOW_OFFSET_PX (HC-09).
- % 3 al cicle de posicions de global_inputs.cpp — ara
  Defaults::RENDER_INFO_POSITION_COUNT (HC-07).

A més, init() assignava mono_digits=true al segment de temps que
refreshSegments() ja marca a cada crida, inclosa la primera d'update()
abans de render. S'elimina el doblegat (PATCH-08); el comentari
explica la garantia.

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:47:49 +02:00
JailDesignerandClaude Opus 4.7 ce3b6da617 refactor(core/rendering): esborrar structs d'extensions GIF no usats (DEAD-09)
gif.hpp declarava Extension, GraphicControlExtension, ApplicationExtension
i PlaintextExtension com a tipus "empaquetats" per a llegir blocs del
stream GIF. El parser a gif.cpp, però, processa els bytes cru (avança
el punter i llig block_size + sub-blocs manualment) sense instanciar-los
mai. Les struct eren code pattern orientat a reus futurs que no va
arribar a materialitzar-se.

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:44:48 +02:00
JailDesignerandClaude Opus 4.7 597402aa1b refactor(core/rendering): esborrar codi mort a Sprite/Surface i inline Sprite::update (DEAD-04/-05/-07/-08/-10 + PATCH-04)
Eliminats mètodes sense callers:
- Sprite::getRect() (DEAD-04)
- Sprite::incX / incY (DEAD-05)
- AnimatedSprite::getCurrentAnimationSize() (DEAD-07)
- Surface::getSurfaceData() (DEAD-08)

Sprite::update passa a ser virtual amb cos buit inline al header, en
lloc de la definició a .cpp amb (void)delta_time; desapareix una línia
de "parxejament" sense alterar la polimorfia (Player::update, heretat
via AnimatedSprite/MovingSprite, continua fent override correctament).
Això tanca DEAD-10 i PATCH-04 alhora.

Els ctors per defecte Sprite()/MovingSprite() s'apunten com WONTFIX al
document: són necessaris per a la cadena de construcció base d'Animated
Sprite (que no delega explícitament al ctor parametritzat).

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:43:39 +02:00
JailDesignerandClaude Opus 4.7 b51109e380 refactor(core/rendering): esborrar codi mort de Text (DEAD-01 + DEAD-02)
Eliminats cinc mètodes sense callers externs:

- writeShadowed / writeCentered: la funcionalitat viu a writeDX(flags,
  ...) que és l'API reutilitzable; ningú crida aquestes variants
  discretes.
- glyphWidth / getGlyphClip: getters per glif no consumits enlloc.
- codepointToUtf8: helper de serialització UTF-8 no consumit (només es
  consumeix el parser nextCodepoint, que queda).

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:40:54 +02:00
JailDesignerandClaude Opus 4.7 81fe826d17 refactor(core/rendering): Surface desacoblat de Screen via Canvas (COUP-02b / COUP-03)
Surface::renderWithColorReplace usava Screen::get().getCanvasArgb() i
companys per a obtenir el buffer destí. S'introdueix una abstracció
mínima:

- source/core/rendering/canvas.hpp: struct Canvas amb pixels/width/
  height i la fàbrica activeCanvas() que retorna el canvas actiu.
- source/core/rendering/canvas.cpp: implementació que delega a Screen.

Surface ara inclou canvas.hpp en lloc de screen.hpp i crida
Rendering::activeCanvas(). L'indirecció permet, més endavant, empènyer
canvases alternatius (off-screen, efectes) sense tocar Surface; ara
mateix es comporta idèntic al codi anterior.

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:38:11 +02:00
JailDesignerandClaude Opus 4.7 029907d5a0 refactor(core/rendering): Screen desacoblat de SDL3GPUShader concret (COUP-02a)
Screen ja no inclou sdl3gpu/sdl3gpu_shader.hpp ni instancia la classe
concreta. S'afegeix la fàbrica Rendering::makeDefaultShaderBackend()
declarada a shader_backend.hpp i implementada a sdl3gpu_shader.cpp
(únic fitxer que encara coneix la classe concreta).

Per a que Screen no necessite static_cast, releaseWindow() passa a ser
mètode virtual pur de la interfície ShaderBackend (abans només existia
a SDL3GPUShader). SDL3GPUShader::releaseWindow marca override.

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:36:11 +02:00
JailDesignerandClaude Opus 4.7 a63deac6a3 refactor(core/rendering): deduplicar Text::write* i nomenar magic numbers (DUP-03 + HC-01)
Text tenia cinc mètodes (write, writeColored, writeMonoDigits, length,
lengthMonoDigits) i dos getters (glyphWidth, getGlyphClip) que repetien
el mateix patró de resolució de codepoint: find(cp), fallback a '?' si
no hi és. Extret a un helper privat resolveGlyph(cp) que centralitza
aquesta lògica; totes les funcions passen a usar-lo.

write() ara delega directament a writeColored(...) passant el color per
defecte, eliminant ~19 línies del bucle UTF-8 duplicades.

Els literals 1 (índex de "tinta" al PNG) i 15 (color per defecte a la
paleta) passen a ser Text::INK_COLOR_INDEX i Text::DEFAULT_TEXT_COLOR
(HC-01).

Build + smoke 5s xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:32:37 +02:00
JailDesignerandClaude Opus 4.7 5f46fb22a4 refactor(sdl3gpu): extreure shaders MSL i helpers a headers compartits (CONV-03)
Els dos pipelines GPU (PostFX i CrtPi) tenien:
- Raw strings MSL incrustades al .cpp amb NOLINTBEGIN(readability-
  identifier-naming) envoltant ~150 línies cadascun.
- Funcions createShaderMSL / createShaderSPIRV duplicades byte a byte
  en cada .cpp (DUP-04), amb un comentari admetent-ho.
- El vertex shader postfx_vs escrit dues voltes, idèntic (DUP-05).

S'introdueixen:
- source/core/rendering/sdl3gpu/shader_factory.hpp amb els dos helpers
  inline, per a reutilitzar des de qualsevol pipeline.
- source/core/rendering/sdl3gpu/msl/ amb un header per shader
  (postfx_vert compartit, postfx_frag, crtpi_frag); cada constant es
  diu kCamelCase i cau per defecte sota el project naming, així que
  els blocs NOLINT desapareixen (PATCH-07).
- Suppresió a cppcheck per a sdl3gpu/msl/* (és MSL, no C++).

Els .cpp es redueixen ~190 línies cada un i queden només amb la
lògica del pipeline.

Build + smoke test de 5s sota xvfb OK (exit 124).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:28:52 +02:00
JailDesignerandClaude Opus 4.7 d479475dcf refactor(core/rendering): deduplicar Surface::render via delegació (DUP-02)
Surface::render i Surface::renderWithColorReplace compartien ~30 línies
del bucle de blit (clipping, flip, transparent check, escriptura ARGB) i
només diferien en l'ARGB final. Ara render() delega a la versió amb
remap passant source==target==transparent_color_: qualsevol píxel opac
té COLOR != source_color (els transparents ja s'han filtrat), per tant
REMAPPED == sub_palette_[COLOR] — resultat idèntic al path original.

Verificat amb make build + smoke test de 5s sota xvfb-run (exit 124 =
timeout sencer, sense crash).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:19:09 +02:00