Files
aee/CLAUDE.md

343 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (07) i la migració `scenes::` (Steps 010) 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/*.gif, *.ogg` | Original assets | **Do not modify** — assets remain untouchable |
| `data/fonts/, data/ui/, data/shaders/` | 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/`)
- **GlobalInputs** (`global_inputs.hpp/cpp`) — Maps configurable function keys 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. Poll-based each frame: D-pad/left stick (deadzone 12000) → virtual arrow keys for game movement; A/B buttons, Start, Back translate to synthetic SDL key events (F12/ESC/Enter/Backspace) when menu is open, so Director handles them exactly like keyboard. Loads extra mappings from `gamecontrollerdb.txt` (next to the executable) at init via `SDL_AddGamepadMappingsFromFile`, extending SDL's built-in controller database
- **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::KeysGUI`, `Defaults::KeysGame`, `Defaults::Video`, `Defaults::Audio`, `Defaults::Window`, `Defaults::Game`
- **options.hpp/cpp** — `Options` namespace with inline globals and YAML load/save. Structs: `KeysGUI`, `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 | Toggle stretch filter (nearest ↔ linear) |
| 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 |
All key bindings are configurable via `Options::keys_gui` and stored in `config.yaml` (section `controls:` with SDL scancode names). Game movement keys (`Options::keys_game.up/down/left/right`) can be remapped via the CONTROLS submenu — the `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`, `integer_scale`), audio (incl. `enabled` master + `music_*` + `sound_*`), window, render_info (incl. `show_time`), game, shader selection, controls (movement keys + menu_toggle + pause_toggle) |
| `~/.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/`) — **en construcció**
Sistema d'empaquetat d'assets a l'estil `coffee_crisis_arcade_edition`. Genera un sol fitxer binari opac `resource.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=resource.pack]` + `--list pack`.
**Build**:
- `make pack` compila l'eina (target `pack_resources` a `EXCLUDE_FROM_ALL` de [CMakeLists.txt](CMakeLists.txt)) i genera `resource.pack` a la rel. 33 entrades ≈ 4 MB.
- `./build/pack_resources --list resource.pack` inspecciona el pack.
**Estat actual (Fase 1 completada, 2026-04-16)**:
- Classes + eina compilen i funcionen. El pack es genera correctament i el `--list` mostra les 33 entrades (totes les GIFs, OGGs, font `.fnt`+`.gif`, `locale/ca.yaml`, `shaders/*`).
- **Encara no està cablejat al joc**: cap callsite usa `ResourceHelper::loadFile`. Tots segueixen cridant `file_readfile`. El joc funciona exactament igual que abans.
- Nou getter `file_getresourcefolder()` afegit a [jfile.hpp](source/core/jail/jfile.hpp) perquè ResourceHelper puga construir el path del fallback.
**Pendent (Fases 2-6 del pla [.claude/plans/declarative-popping-breeze.md](/home/sergio/.claude/plans/declarative-popping-breeze.md))**:
1. **Fase 2** — Afegir `ResourceHelper::initializeResourceSystem(pack_path, enable_fallback=true)` a [main.cpp](source/main.cpp) just després de `file_setresourcefolder`, i `shutdownResourceSystem()` a `SDL_AppQuit`.
2. **Fase 3** — Migrar callsites `file_readfile``ResourceHelper::loadFile` (tipus canvia `std::vector<char>``std::vector<uint8_t>`):
- [locale.cpp:30](source/core/locale/locale.cpp#L30)
- [text.cpp:65, 129](source/core/rendering/text.cpp) (`.fnt` + bitmap)
- [scene_utils.cpp:12](source/scenes/scene_utils.cpp) (música escenes)
- [modulegame.cpp:52](source/game/modulegame.cpp) (música gameplay)
- [jdraw8.cpp:47, 65](source/core/jail/jdraw8.cpp) (`JD8_LoadSurface`, `JD8_LoadPalette`)
3. **Fase 4** — Eliminar scaffold `.jrf` de [jfile.cpp](source/core/jail/jfile.cpp): `file_setresourcefilename`, `file_setsource`, `SOURCE_FILE`/`SOURCE_FOLDER`, `dictionary_loaded()`, `file_getfilepointer()`, `file_readfile()`. Mantenir només config-folder + `file_setresourcefolder` + `file_getresourcefolder`.
4. **Fase 5** — Als targets release de [Makefile](Makefile) (`_linux_release`/`_windows_release`/`_macos_release`): afegir dependència `pack` i canviar `cp -r data``cp resource.pack`. WASM intacte (segueix usant `--preload-file data@/data`).
5. **Fase 6** — A [main.cpp](source/main.cpp): `enable_fallback = false` només per a `NDEBUG && !__EMSCRIPTEN__` (pack obligatori a Release natiu; Debug i WASM mantenen fallback).
### 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/`)
- `*.gif`, `*.ogg` — Original game assets (**do not modify**)
- `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
- `ui/` — Reserved for future UI graphics
### 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 110) + `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.
- **Resource Pack — Fases 2-6**: la classe `ResourcePack`, `ResourceHelper` i l'eina `pack_resources` ja estan fetes (Fase 1). Queda cablejar-ho al joc: init a `main.cpp`, migrar 5 callsites de `file_readfile` a `ResourceHelper::loadFile`, eliminar l'scaffold `.jrf` de `jfile`, integrar `resource.pack` als bundles release, i flip `enable_fallback=false` per a Release natiu. Detall complet a la secció *Resource Pack* i al pla [.claude/plans/declarative-popping-breeze.md](/home/sergio/.claude/plans/declarative-popping-breeze.md).
### 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).