# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

**Aventures En Egipte (AEE)** — a retro-style 2D game written in C++ using SDL3. The game uses a software-rendered 8-bit paletted graphics engine (320x200, 256 colors), custom audio (JailAudio), and GIF-based assets. The codebase and commit messages are in Valencian/Catalan.

## Build

```bash
# Linux
cmake -B build
cmake --build build

# Windows (MinGW)
cmake -B build -G "MinGW Makefiles"
cmake --build build
```

Dependencies: SDL3. Uses CMake (minimum 3.10) with C++20. SPIR-V shaders compiled automatically if `glslc` is available; precompiled headers used as fallback.

The executable is output to the project root. The `data/` folder must be in the working directory at runtime.

## Architecture

### New Rules (Modernization Phase)

The old "Golden Rule: Do Not Touch Gameplay" has been **revoked**. The original C-style code (jail engine + gameplay modules) is now a **modernization target**, not a sacred zone. The parallel-overlay approach has reached its ceiling: fades and cinematics are still blocking loops, audio relies on an async `SDL_AddTimer`, and the emulator-style game thread blocking at `publishFrame` is incompatible with an emscripten port.

The five current objectives are:

1. **Idiomatic C++**: RAII, `std::vector`/`std::string`/`std::optional`, classes with real constructors/destructors. No more raw `malloc/free` in structs.
2. **Zero blocking events**: no `while (...) { poll; }`, no `SDL_Delay` inside gameplay, no `cv.wait()` in `publishFrame`. Every subsystem must be able to advance in a single tick call.
3. **Time-based**: animations, cinematics and fades measured in milliseconds, not frames. `JG_ShouldUpdate()` as gameplay gate is on its way out.
4. **Overlay integrated**: overlay stops being a post-game layer painted by Director — it becomes part of the same render pass the game tick produces.
5. **SDL3 callbacks**: main loop handed over to `SDL_AppInit` / `SDL_AppIterate` / `SDL_AppEvent` / `SDL_AppQuit`, single-threaded, compatible with emscripten.

**Iron rule: zero gameplay regressions.** Each phase of the modernization must leave the game playing identically — same feel, same timings, same collisions, same scoring. See [docs/scenes-migration-plan.md](docs/scenes-migration-plan.md) for the phased plan.

### Migration Status (2026-04-16)

**Completat.** Totes les fases del pla original (0–7) i la migració `scenes::` (Steps 0–10) estan fetes, ModuleGame és una `scenes::Scene` tick-based, el cooperative fiber s'ha eliminat, i el build emscripten/WASM arrenca i es publica a maverick.

**Arquitectura actual**:
- Un sol thread (Director). Main loop via SDL3 Callback API (`SDL_MAIN_USE_CALLBACKS`): `SDL_AppInit/Iterate/Event/Quit` a [main.cpp](source/main.cpp).
- `Director::iterate()` posseeix l'estat d'escena (`current_scene_`, `game_state_`) i fa input → tick de l'escena → `JD8_Flip` (sense yield, només converteix `screen` → `pixel_data`) → overlay → present. Tot en línia recta, zero fibers, zero mutex.
- Totes les escenes (inclòs `ModuleGame`) implementen `scenes::Scene` amb `onEnter/tick(delta_ms)/done/nextState`.
- `ModuleSequence` (el vell dispatcher) eliminat. Despatxa via `game_state_ == 0` (gameplay → `ModuleGame`) o `game_state_ == 1` (cinemàtica → `SceneRegistry::tryCreate(num_piramide)`).

**Escenes migrades** (totes registrades a `Director::init` via `SceneRegistry`):
- `MortScene` (state 100) · `BannerScene` (2..5) · `MenuScene` (0) · `SlidesScene` (1, 7)
- `CreditsScene` (8) · `SecretaScene` (6) · `IntroNewLogoScene` (255, `use_new_logo=true`)
- `IntroScene` (255, `use_new_logo=false`) · `IntroSpritesScene` (sub-escena de les dues intros)

**Files d'`Options::game` exposats per a tests ràpids** (persistits a `config.yaml`):
`piramide_inicial`, `habitacio_inicial`, `vides`, `diamants_inicial`, `diners_inicial`, `use_new_logo`, `show_title_credits`.

**La capa `scenes::`** ([source/scenes/](source/scenes/)): `scene.hpp` (interfície), `scene_registry.hpp/.cpp`, `timeline`, `sprite_mover`, `frame_animator`, `palette_fade`, `surface_handle`, `scene_utils` (`playMusic`). Pures tick-based, zero while, zero `JG_ShouldUpdate`.

### Modernization Targets

**Invariants to preserve** (touch these and you broke the game):
- Gameplay feel, movement speed, enemy AI behavior
- Collision detection, scoring, lives, level progression
- Visible animation cadence (once translated to ms, must look identical)
- Difficulty curves and cinematic timings
- Cheat codes (`reviu`, `alone`, `obert`)
- Original palettes, fades, music cues

**Free to change** (internal representation):
- Data structures (structs → classes with RAII)
- Ownership (raw pointers → `std::unique_ptr`/`std::vector`/`std::string`)
- Timing representation (frame counters → ms accumulators)
- Threading model (game thread → single-threaded state machine)
- Global state (the old `info::` namespace is now an `inline` singleton `info::ctx` of type `GameContext`; access is `info::ctx.X` instead of `info::X`. Can be reset with `info::ctx.reset()`)
- API shapes of jail subsystems (as long as callers are updated consistently)

### Boundary: Original vs New Code

| Path | Owner | Rule |
|------|-------|------|
| `source/core/jail/` | Legacy engine, modernization target | Free to modify with care — preserve external behavior |
| `source/game/*.cpp/hpp` | Legacy gameplay, modernization target | Free to modify with care — preserve gameplay invariants |
| `source/core/rendering/` | New presentation layer | Free to modify |
| `source/core/input/` | New input layer | Free to modify |
| `source/utils/` | New utilities | Free to modify |
| `source/game/options,defines,defaults` | New config system | Free to modify |
| `data/gfx/, data/music/` | Original assets | **Do not modify** — assets remain untouchable |
| `data/fonts/, data/shaders/, data/locale/` | New assets | Free to modify |

### Legacy "Jail" Engine (`source/core/jail/`) — modernization target

Flat C-style APIs (no classes), prefixed by subsystem. Being progressively converted to idiomatic C++ (see Phase 1 of the plan). External API names are kept stable during the transition to avoid churning call sites.

- **JG** (`jgame`) — Game loop timing: init/finalize, fixed-timestep update via `JG_ShouldUpdate()`
- **JD8** (`jdraw8`) — 8-bit paletted software renderer. 320x200 screen buffer (`JD8_Surface` = `Uint8*`), palette-indexed blitting with color-key transparency, fade effects. `JD8_Flip()` converts palette→ARGB and delegates to `Screen::present()`
- **JA** (`jail_audio`) — Custom audio mixing using SDL3 audio streams (OGG via stb_vorbis, WAV)
- **JI** (`jinput`) — Input: keyboard state polling, key debouncing, cheat code detection. Filters GUI keys from game, calls `GlobalInputs::handle()` and `Mouse::updateCursorVisibility()` each update
- **JF** (`jfile`) — File I/O: filesystem folder mode (`data/`) or packed resource file (`.jrf`). Config folder at `~/.config/jailgames/aee/`

### System Layer (`source/core/system/`)

- **Director** (`director.hpp/cpp`) — **Orchestrator singleton**, únic thread del runtime. Posseeix l'estat d'escena (`current_scene_: unique_ptr<Scene>`, `game_state_`, `last_tick_ms_`) directament com a members. `iterate()` fa: poll events (via `SDL_AppEvent`) → input (Gamepad/KeyRemap/GlobalInputs/Mouse) → `JA_Update` → transició d'escena si `done()` → `scene->tick(delta_ms)` → `JD8_Flip` (converteix `screen` → `pixel_data`) → overlay → present → `SDL_Delay` al frame target. Dispatcher: `game_state_ == 0` → `new ModuleGame`, `game_state_ == 1` → `SceneRegistry::tryCreate(info::ctx.num_piramide)` (amb redirect `num_piramide == 6 && diners < 200 → 7` replicant el vell `ModuleSequence::Go`).

### Presentation Layer (`source/core/rendering/`)

- **Screen** (`screen.hpp/cpp`) — Singleton. Manages SDL_Window, SDL_Renderer, SDL_Texture. Dual rendering path: SDL3GPU with shaders (primary) or SDL_Renderer fallback. Handles fullscreen, zoom, aspect ratio 4:3, integer scaling, VSync. Counts FPS and updates render info segments
- **Overlay** (`overlay.hpp/cpp`) — Paints directly on the ARGB pixel buffer before presentation. Handles notifications (slide-in animation), animated render info (4 independent segments with per-segment anim + vertical slide state machine), persistent PAUSA indicator, and double-ESC-to-quit logic
- **Text** (`text.hpp/cpp`) — Bitmap font renderer. Loads `.fnt` + `.gif` pairs, renders UTF-8 glyphs directly on `Uint32*` ARGB buffer. Supports `drawClipped(x, y, text, color, clip_xmin, clip_xmax, clip_ymin, clip_ymax)` for per-pixel 2D clipping (used by menu transitions)
- **Menu** (`menu.hpp/cpp`) — Floating options menu with stack-based page navigation (root → VIDEO/AUDIO/CONTROLS). Uses ItemKind enum: Toggle/Cycle/IntRange/Submenu/KeyBind. Features: vertical expand animation on open (outQuad), horizontal slide + height interpolation on page transitions (forward/backward direction), key capture mode for remapping. Callbacks delegate to `Screen::*` / `Overlay::*` / `Options::applyAudio()` to avoid duplication
- **SDL3GPUShader** (`sdl3gpu/`) — GPU shader backend (Vulkan/Metal). PostFX and CRT-Pi shaders with presets, supersampling (3×/6×/9×), Lanczos downscaling. Supports 4:3 aspect ratio stretch fused into the upscale pass to avoid artifacts

### Input Layer (`source/core/input/`)

- **KeyConfig** (`key_config.hpp/cpp`) — **Font única de veritat per a les tecles d'UI/sistema**. Carrega `data/input/keys.yaml` al boot (12 entrades: F1-F10 GlobalInputs + F11 pausa + F12 menú de servei) i opcionalment aplica overrides des de `~/.config/jailgames/aee/keys.yaml`. Exposa `KeyConfig::scancode("id")`, `scancodePtr("id")` (per a Menu KeyBind), `setScancode(...)`, `isGuiKey(sc)` (filtre del Director per a no propagar tecles d'UI a `JI_AnyKey`). `saveOverrides()` només persistix les entrades que difereixen del default. Les tecles de moviment del jugador NO viuen ací — es queden a `Options::keys_game`
- **GlobalInputs** (`global_inputs.hpp/cpp`) — Maps function keys (via `KeyConfig::scancode("dec_zoom")`, etc.) to presentation actions. Uses debounce. Returns whether a key was consumed (to suppress from game layer)
- **Mouse** (`mouse.hpp/cpp`) — Auto-hides cursor after 3 seconds of inactivity
- **Gamepad** (`gamepad.hpp/cpp`) — First-gamepad support with hot-plug + overlay notification with controller name. Poll-based each frame: D-pad/left stick (deadzone 12000) → virtual arrow keys for game movement. Mapeig: SOUTH/EAST/WEST/NORTH (4 botons frontals) → Enter sintètic per avançar escenes; al menú EAST=accept, SOUTH=cancel/back. SELECT → menu_toggle (servei), START → pause_toggle (via `KeyConfig::scancode(...)`). Loads extra mappings from `gamecontrollerdb.txt` at init via `SDL_AddGamepadMappingsFromFile`
- **KeyRemap** (`key_remap.hpp/cpp`) — Each frame, reads `Options::keys_game.*` and mirrors physical keyboard state to virtual standard scancodes (`SDL_SCANCODE_UP`/DOWN/LEFT/RIGHT). Allows full movement key remapping without touching hardcoded game code in `prota.cpp`/`mapa.cpp`

### Locale Layer (`source/core/locale/`)

- **Locale** (`locale.hpp/cpp`) — Flat key → string map loaded from YAML at boot. Keys use dot notation (`menu.items.zoom`, `notifications.pause`). Returns the key itself when missing (visible fallback for debugging). Strings live in [data/locale/ca.yaml](data/locale/ca.yaml) (Valencian, default). Designed for future multilanguage support

### Configuration System (`source/game/`)

Follows the pattern from `jaildoctors_dilemma`, persists to YAML:

- **defines.hpp** — Game constants: `Texts::WINDOW_TITLE`, `Texts::VERSION`, `GameScreen::WIDTH/HEIGHT`
- **defaults.hpp** — Default values: `Defaults::KeysGame`, `Defaults::Video`, `Defaults::Audio`, `Defaults::Window`, `Defaults::Game`. (Les tecles d'UI viuen a `data/input/keys.yaml` via `KeyConfig`)
- **options.hpp/cpp** — `Options` namespace with inline globals and YAML load/save. Structs: `KeysGame`, `Video`, `RenderInfo`, `Audio`, `Window`, `Game`, `PostFXPreset`, `CrtPiPreset`

### Utilities (`source/utils/`)

- **utils.hpp/cpp** — `toLower()` and other helpers
- **easing.hpp/cpp** — Easing functions for animations: `linear`, `outQuad`, `inQuad`, `inOutQuad`, `outCubic`, `inCubic`, `lerp`, `lerpInt`. Used by Menu transitions, render info slide, and segment animations

### Function Key Map

| Key | Action |
|-----|--------|
| F1 | Decrease window zoom |
| F2 | Increase window zoom |
| F3 | Toggle fullscreen |
| F4 | Toggle shaders on/off |
| F5 | Toggle aspect ratio (square pixels ↔ 4:3 CRT) |
| F6 | Toggle supersampling |
| F7 | Cycle shader type (PostFX ↔ CRT-Pi) |
| F8 | Cycle shader presets |
| F9 | Cycle texture filter (nearest ↔ linear) — sempre aplicat, independent de 4:3 |
| F10 | Cycle render info (off → top → bottom → off) |
| F11 | Toggle pause (Director skips `scene->tick()` + `JA_PauseMusic`/`JA_ResumeMusic`) |
| F12 | Toggle floating options menu |
| ESC | Double-press to quit (with overlay notification) / close menu if open |
| Backspace | Go up one menu level / close menu if at root |
| ↑↓←→ / Enter | Menu navigation |

UI/system key bindings are loaded from [data/input/keys.yaml](data/input/keys.yaml) via `KeyConfig`. Overrides fets des del menú es persistixen a `~/.config/jailgames/aee/keys.yaml` (només les que difereixen del default). Game movement keys (`Options::keys_game.up/down/left/right`) viuen separadament a `config.yaml` (secció `controls:`) i es remapejen via la CONTROLS submenu — el `KeyRemap` module mirrors custom physical keys to virtual standard scancodes so hardcoded game code keeps working.

### Execution Model (Single-threaded, Scene-based)

Zero threads, zero fibers, zero mutex. Main loop via SDL3 Callback API (`SDL_MAIN_USE_CALLBACKS`) a [main.cpp](source/main.cpp). Cada frame entra pel `SDL_AppIterate` → `Director::iterate()`:

```
SDL_AppIterate → Director::iterate() {
  if (quit_requested_) { scene.reset(); return false; }
  if (!context_initialized_) initGameContext();

  Gamepad/KeyRemap/GlobalInputs/Mouse::update
  JA_Update()                     ← audio pump

  if (!paused_) {
    if (scene && (scene->done() || JG_Quitting()))
      game_state_ = scene->nextState(); scene.reset();
    if (!scene) {
      if (game_state_ == -1 || JG_Quitting()) return false;
      scene = createNextScene();     ← ModuleGame o registry.tryCreate()
      scene->onEnter();
    }
    JI_Update()
    scene->tick(now - last_tick_ms_)
    JD8_Flip()                       ← converteix screen indexat → pixel_data
    memcpy pixel_data → game_frame
  }

  memcpy game_frame → presentation_buffer
  Overlay::render(presentation_buffer)
  Screen::present(presentation_buffer)
  SDL_Delay(frame_target - elapsed)
}
SDL_AppEvent → Director::handleEvent()    ← events lliurats un a un per SDL
SDL_AppQuit  → Director::teardown()       ← Options::save + descàrrega ordenada
```

**Key points:**
- `Director` posseeix `current_scene_`, `game_state_`, `last_tick_ms_`, `context_initialized_` com a members — abans vivien al stack del fiber.
- `JD8_Flip()` només converteix paleta + `screen` a ARGB (`pixel_data`). Ja **no fa yield** — tot corre lineal.
- `JG_ShouldUpdate()` encara existeix a `jgame.cpp` com a timing-gate per a `ModuleGame::Update()` (10 ms fix), però ja no fa yield. Cap caller fa spin-wait.
- Pausa (F11) simplement salta el bloc de tick; overlay i present continuen, es re-presenta l'últim frame congelat.
- Doble buffer (`game_frame` + `presentation_buffer`) es manté perquè el Director pot presentar múltiples frames per cada tick durant pausa; el cost (256 KB memcpy) és trivial a 320×200.
- SDL3 Callback API compatible amb emscripten: el navegador posseeix el main loop i ens crida via `requestAnimationFrame`. Zero canvis de codi per a portabilitat.

### Rendering Pipeline (inside Screen::present)

```
Screen::present(pixel_data):
  1. FPS count + render info text update
  2. IF GPU + shaders enabled:
     - uploadPixels → scene_texture (320×200)
     - [IF 4:3] stretch pass fused with upscale: scene → scaled_texture (W×factor, H×factor×1.2)
     - [IF SS] upscale pass: scene → scaled_texture (W×factor, H×factor)
     - PostFX or CRT-Pi shader → swapchain (with viewport letterboxing)
  3. ELSE IF GPU without shaders:
     - uploadPixels → clean render → swapchain
  4. ELSE (fallback):
     - SDL_UpdateTexture → SDL_RenderPresent
```

### Pixel Format

JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL texture uses `SDL_PIXELFORMAT_ABGR8888`. GPU textures use `SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM` (same byte layout on little-endian). Overlay colors are ABGR format.

### Persistence Files

| File | Content |
|------|---------|
| `~/.config/jailgames/aee/config.yaml` | Main config: video (incl. `vsync`, `scaling_mode`, `texture_filter`), audio (incl. `enabled` master + `music_*` + `sound_*`), window, render_info (incl. `show_time`), game, shader selection, controls (només moviment del jugador) |
| `~/.config/jailgames/aee/keys.yaml` | UI key overrides (només entrades que difereixen del default de [data/input/keys.yaml](data/input/keys.yaml)). Generat per `KeyConfig::saveOverrides()` |
| `~/.config/jailgames/aee/postfx.yaml` | PostFX shader presets (6 defaults: CRT, NTSC, CURVED, SCANLINES, SUBTLE, CRT LIVE) |
| `~/.config/jailgames/aee/crtpi.yaml` | CRT-Pi shader presets (4 defaults: DEFAULT, CURVED, SHARP, MINIMAL) |

### Resource Pack (`source/core/resources/`)

Sistema d'empaquetat d'assets a l'estil `coffee_crisis_arcade_edition`. Genera un sol fitxer binari opac `resources.pack` que substitueix la carpeta `data/` als releases natius.

**Format AEE1** (fidel a CCAE amb clau pròpia):
```
Header:   "AEE1" (4B) + version uint32 + resource_count uint32
Index:    per recurs → filename_len uint32 + filename + offset uint64 + size uint64 + checksum uint32
Payload:  data_size uint64 + bytes XOR-xifrats amb "AEE_RESOURCES__2026"
```
Checksum: djb2-like amb seed `0x12345678`. Càrrega full-to-RAM (sense mmap).

**Fitxers**:
- [source/core/resources/resource_pack.hpp/cpp](source/core/resources/) — classe `ResourcePack`: `loadPack`, `savePack`, `addFile`, `addDirectory`, `getResource(name) → std::vector<uint8_t>`, `hasResource`
- [source/core/resources/resource_helper.hpp/cpp](source/core/resources/) — namespace `ResourceHelper`: `initializeResourceSystem(pack, enable_fallback)`, `loadFile(relative_path)`, `shutdownResourceSystem`. Prova el pack primer, cau a `file_getresourcefolder()+path` si el fallback està actiu.
- [tools/pack_resources/pack_resources.cpp](tools/pack_resources/pack_resources.cpp) — eina standalone CLI: `pack_resources [input_dir=data] [output=resources.pack]` + `--list pack`.

**Build**:
- `make pack` compila l'eina (target `pack_resources` a `EXCLUDE_FROM_ALL` de [CMakeLists.txt](CMakeLists.txt)) i genera `resources.pack` a la rel. 33 entrades ≈ 4 MB.
- `./build/pack_resources --list resources.pack` inspecciona el pack.

**Estat actual (Fases 1-6 completades, 2026-04-16)**:
- `ResourcePack` + `ResourceHelper` + eina `pack_resources` compilen i funcionen. El pack genera 33 entrades ≈ 4 MB.
- Cablejat al joc via `ResourceHelper::initializeResourceSystem` a [main.cpp](source/main.cpp) (amb `return SDL_APP_FAILURE` si falla), i `shutdownResourceSystem` a `SDL_AppQuit`.
- Tots els callsites de recursos usen `ResourceHelper::loadFile` (`std::vector<uint8_t>`): [locale.cpp](source/core/locale/locale.cpp), [text.cpp](source/core/rendering/text.cpp), [scene_utils.cpp](source/scenes/scene_utils.cpp), [modulegame.cpp](source/game/modulegame.cpp), [jdraw8.cpp](source/core/jail/jdraw8.cpp).
- Scaffold `.jrf` eliminat de [jfile.cpp](source/core/jail/jfile.cpp): `file_setresourcefilename`, `file_setsource`, `SOURCE_FILE`/`SOURCE_FOLDER`, `dictionary_loaded`, `file_getfilepointer`, `file_readfile`. Només queden config-folder i resource-folder getters/setters.
- Targets release a [Makefile](Makefile) (`_linux_release`/`_windows_release`/`_macos_release`) depenen de `pack` i copien `resources.pack` en lloc de `data/`. WASM intacte (`--preload-file data@/data`).
- `enable_fallback = false` a Release natiu (`NDEBUG && !__EMSCRIPTEN__`): el pack és obligatori. Debug i WASM mantenen el fallback actiu.

### External Libraries (`source/external/`)

- `gif.h` — Header-only GIF decoder. **Cannot be included from more than one .cpp** (no include guards on functions). Other files use `extern` declarations for `LoadGif()`
- `stb_vorbis.h` — stb single-header OGG decoder
- `fkyaml_node.hpp` — Header-only YAML parser (fkYAML v0.4.2)

### Data Assets (`data/`)

- `gfx/` — Original game GIFs (**do not modify content**): `frames.gif`/`frames2.gif` (sprite sheet del joc), `logo.gif`/`logo_new.gif` (intros), `menu.gif`/`menu2.gif`, `intro.gif`/`intro2.gif`/`intro3.gif` (slides), `ffase.gif` (banner nivells), `final.gif`/`finals.gif` (crèdits), `gameover.gif`, `tomba1.gif`/`tomba2.gif` (escena secreta)
- `music/` — 8 pistes OGG originals amb noms temàtics: `mort.ogg` (game over), `secreta.ogg` (escena secreta + piràmide 6), `menu.ogg` (menú + intros), `banner.ogg` (banner de fase), `final.ogg` (slides finals + crèdits), `piramide_1_4_5.ogg` (gameplay default), `piramide_2.ogg`, `piramide_3.ogg`
- `fonts/8bithud.fnt + .gif` — Bitmap font for overlay (8×8, 124 glyphs, UTF-8 with accents)
- `shaders/` — GLSL sources: `postfx.vert`, `postfx.frag`, `upscale.frag`, `downscale.frag`, `crtpi_frag.glsl`
- `locale/ca.yaml` — UI strings in Valencian (menu titles/items/values, notifications). Edit freely; reload at restart

### Known Issues & Technical Debt

1. **gif.h cannot be included twice**: Functions are not `static` or `inline`, causing multiple definition errors. Text class uses `extern` forward declarations as workaround
2. ~~**Cheats are broken (`reviu`, `alone`, `obert`)**~~: Fixed. `JI_moveCheats` tradueix `SDL_Scancode` → ASCII via `scancode_to_ascii` abans de ficar-los al buffer ([jinput.cpp:32-37, 55-61](source/core/jail/jinput.cpp#L32-L61)), i `JI_CheatActivated` compara ASCII amb ASCII.
3. **No sound effects in game**: Game code never calls `JA_PlaySound*`/`JA_LoadSound` — only music via `JA_PlayMusic`/`JA_FadeOutMusic`. The SONS and VOL SONS menu items control volume of an empty channel pool. Infrastructure ready for future SFX.
4. ~~**Raw `malloc`/`free` in gameplay structs**~~: Majoritàriament arreglat. `Sprite`/`Entitat` usen `std::vector<Frame>` i `std::vector<Animacio>` ([sprite.hpp](source/game/sprite.hpp)). `jfile.cpp` ja no té el global `scratch[255]` (substituït per `thread_local std::string`). L'API `file_getfilebuffer` (que tornava raw `char*` amb `malloc`) s'ha substituït per `file_readfile` que retorna `std::vector<char>` RAII — elimina els leaks silenciosos que hi havia a `JD8_LoadPalette`, `ModuleGame::Go()` i `scenes::playMusic`. Queda `jail_audio.hpp` barrejant `new`/`malloc`/`SDL_malloc` de forma pairada i correcta (no leak), pendent de polir amb `std::unique_ptr` quan toque.
5. ~~**Blocking loops in cinematics and fades**~~: Fixed. Migració completa de `ModuleSequence::do*()` a la capa `scenes::` (Steps 1–10) + `ModuleGame` també tick-based (Phase A). Tot `while(!JG_ShouldUpdate())` i `wait_frame_or_skip()` eliminat. Els fades bloquejants `JD8_FadeOut`/`JD8_FadeToPal` també eliminats (Phase B.2): només queda l'API tick-step `JD8_FadeStart*` + `JD8_FadeTickStep`, encapsulada pel wrapper `scenes::PaletteFade`. ModuleGame té fases `FadingIn`/`FadingOut` pròpies.
6. ~~**`SDL_AddTimer` in audio**~~: Fixed in Phase 3. `jail_audio` is now a header-only `inline` module (single `.cpp` stub only hosts the `stb_vorbis` implementation to avoid multiple definitions). Music uses true streaming via `stb_vorbis_open_memory` + `JA_PumpMusic` with a 0.5s low-water-mark instead of decoding the whole OGG into RAM. Mixing/fade update runs manually via `JA_Update()` called once per frame from `Director::iterate()`. Ported from the `jaildoctors_dilemma` codebase.
7. ~~**Game thread + `publishFrame` mutex/cv**~~: Fixed in Phase 4+5 via cooperative `GameFiber`; **eliminated entirely in Phase B.2**. `JD8_Flip()` ja no fa yield — només converteix `screen` → `pixel_data`. Director posseeix l'estat d'escena (`current_scene_`, `game_state_`) i crida `scene->tick()` directament des d'`iterate()`. Fitxers `source/core/system/fiber.{hpp,cpp}` esborrats. Zero threads, zero mutex, zero fibers.
8. ~~**`ModuleSequence` legacy dispatcher**~~: Eliminated in Step 10. Era el vell switch per `num_piramide`, ara substituït per `SceneRegistry::tryCreate()` i dispatch directe des de `Director::iterate()`. `modulesequence.{hpp,cpp}` esborrats.

### WebAssembly Build

`make wasm` genera el build WASM via Docker (`emscripten/emsdk:latest`) i copia els 3 fitxers (`.js`/`.wasm`/`.data`) a `maverick:/home/sergio/gitea/web_jailgames/static/games/aee/wasm/`, amb un `ssh maverick './deploy.sh'` final. Output local a `dist/wasm/`.

**Diferències respecte build natiu** (a [CMakeLists.txt](CMakeLists.txt) dins `if(EMSCRIPTEN)`):
- SDL3 compilat des de font via `FetchContent` (no hi ha paquet de sistema).
- Shaders SPIR-V omesos (SDL3 GPU no suportat a WebGL2).
- `sdl3gpu_shader.cpp` exclòs dels sources — el fallback `SDL_Renderer` fa tota la presentació.
- [screen.cpp](source/core/rendering/screen.cpp) guarda `#ifndef NO_SHADERS` al voltant de l'include i les crides a `SDL3GPUShader` directes. La resta del codi va via interfície base `ShaderBackend`.
- Link flags: `--preload-file data@/data`, `-fexceptions`, `-sALLOW_MEMORY_GROWTH=1`, `-sMAX_WEBGL_VERSION=2`, `-sINITIAL_MEMORY=67108864`, `-sASSERTIONS=1`, `-sASYNCIFY=1`.
- Defines: `EMSCRIPTEN_BUILD`, `NO_SHADERS`.

**Filesystem**: MEMFS default — no persistent entre recàrregues. `file_setconfigfolder` té fallbacks robustos (`getpwuid` → `getenv("HOME")` → `/tmp`) perquè no pete quan emscripten no té `/etc/passwd`. La config es carrega per defecte cada vegada. IDBFS pendent si mai volguéssem persistència a web.

### Pending / Ideas for Later

- **Sound effects**: infraestructura `JA_PlaySound*`/`JA_LoadSound` ja preparada. Cablejar events de gameplay (col·lisió momia, mort, recollir objecte, trencar tomba, cheat activat). Menus SONS/VOL SONS ja controlen el volum del pool.
- **IDBFS persistence a WASM**: montar `/home/web_user/.config` com a IDBFS a l'init i `FS.syncfs` després de cada save. Opcional — ara per ara la config no persistix entre recàrregues de pàgina.
- **Gamepad**: map Y button (North) to `P` key for Pepe character selection at title screen (only input path not covered by current mapping).
- **Menu items for game**: habitacio_inicial, piramide_inicial, vides (already in `Options::game`, not exposed).
- **Multi-language**: add `data/locale/es.yaml` / `en.yaml` and a language selector item in menu. `Locale::load()` already handles arbitrary files.
- **Game keys remap**: currently only UP/DOWN/LEFT/RIGHT + `menu_toggle`. Could add remap for `pause_toggle`, `keys_game.exit` (needs care with ESC double-press flow).
- **Notification persistence**: notifications clear on each new one (`showNotification` does `notifications_.clear()`). Could queue instead.
- **FPS counter jitter**: time segment width changes per frame (100 Hz centi updates) causes ~1-2 px horizontal jitter in centered layout. Could lock to max-width or use monospace digits.
- **Notification messages partially hardcoded**: overlay/global_inputs/director now use Locale, but the window title (`Texts::WINDOW_TITLE`) and some game-layer strings remain hardcoded.
- **jail_audio `JA_Sound_t` RAII**: `JA_Music_t` ja està net (vector + string), però `JA_Sound_t` encara usa `Uint8*` via `SDL_LoadWAV` out-param. Petit polish per a completar la coherència RAII.

### Previously Fixed (kept for reference)

- **ESC double-press**: Fixed by intercepting KEY_DOWN in Director, setting atomic `esc_blocked_` immediately. `JI_KeyPressed(ESCAPE)` consults this flag. No race condition possible because Director's flag wins before game polls
- **Overlay freeze during intros**: Fixed by threading model. Director runs independently at 60 FPS regardless of game delays. Double buffer avoids overlay smearing on re-presented frames
- **ESC-closes-menu then closes game**: When menu closes via ESC, `esc_swallow_until_release_` flag blocks `JI_KeyPressed(ESC)` until physical key release. Cleared on ESC `KEY_UP`
- **Backspace-closes-menu skipping cinematics**: `menu_keys_held_[SDL_SCANCODE_COUNT]` array tracks scancodes consumed by menu on KEY_DOWN; matching KEY_UP is swallowed so game polling (`JI_AnyKey`) doesn't see them. Also covers F12, Backspace, cursor keys, capture-mode keys
- **Key remap not working after Backspace-close**: `JI_SetInputBlocked(false)` now also called when `Menu::handleKey` causes menu to close via Backspace (previously only cleared on ESC/F12 close paths)

### Virtual Keystates (OR'd sources)

`jinput.cpp` maintains `virtual_keystates[JI_VSRC_COUNT][SDL_SCANCODE_COUNT]` with two sources: `JI_VSRC_GAMEPAD` (from Gamepad::update) and `JI_VSRC_REMAP` (from KeyRemap::update). `JI_KeyPressed` returns true if either physical keystate OR any virtual source has the key set. `JI_SetInputBlocked` still overrides everything (menu open = input suppressed). `JI_SetVirtualKey(scancode, source, pressed)` is the write API — sources are independent so gamepad and keymap can't clobber each other.

### Variable FPS Cap

Director loop uses `FRAME_MS_VSYNC = 16` (60 FPS) or `FRAME_MS_NO_VSYNC = 4` (~250 FPS) depending on `Options::video.vsync`. Selected each iteration, so toggling VSync from the menu updates cap immediately. The GPU swapchain is also reconfigured via `shader_backend_->setVSync()` (IMMEDIATE/MAILBOX vs VSYNC present modes).

### Main Entry (`source/main.cpp`)

Init order: `file_setconfigfolder` → `Options::load` → `Locale::load("locale/ca.yaml")` → `Options::loadPostFX/CrtPi` → `JG_Init` → `Screen::init` → `JD8_Init` → `JA_Init` → `Options::applyAudio()` → `Overlay::init` → `Menu::init` → `Director::init` (also calls `Gamepad::init()`) → `Director::run()` (blocks until quit). Shutdown: `Options::save` → `Director::destroy` → `Menu::destroy` → `Overlay::destroy` → `JA_Quit` → `JD8_Quit` → `Screen::destroy` → `JG_Finalize`.

The state machine (alternating `ModuleSequence` state=1 and `ModuleGame` state=0) lives inside `gameFiberEntry()` in an anonymous namespace in [director.cpp](source/core/system/director.cpp), invoked as the entry of the cooperative fiber (single-threaded).
