# 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

### Golden Rule: Do Not Touch Gameplay

The original game logic (gameplay, entities, map, scoring, collisions, animations) must remain untouched. All modernization work targets the presentation layer and infrastructure only. Any new feature must be implemented as an overlay on top of the existing game, never by modifying original gameplay code.

### Boundary: Original vs New Code

| Path | Owner | Rule |
|------|-------|------|
| `source/core/jail/` | Original engine | **Do not modify** gameplay behavior |
| `source/game/*.cpp/hpp` (except options/defines/defaults) | Original game | **Do not modify** |
| `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** |
| `data/fonts/, data/ui/, data/shaders/` | New assets | Free to modify |

### Original "Jail" Engine (`source/core/jail/`)

Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay logic.**

- **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**. Owns main thread. Launches game thread that runs `ModuleGame`/`ModuleSequence::Go()`. Emulator-style architecture: Director runs at ~60 FPS independently, polls SDL events, updates overlay, presents frames. Game thread blocks at `JD8_Flip()` → `Director::publishFrame()` until Director consumes the frame. Director is **non-blocking**: if no new frame is available, it re-presents the last known game frame with fresh overlay on top

### 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
- **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 (blocks game thread at publishFrame + `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.

### Threading Model (Emulator Architecture)

```
Main thread (Director)              Game thread (ModuleGame/Sequence::Go())
────────────────────                ────────────────────────────────────
loop at ~60 FPS {                   loop {
  SDL_PollEvent()                     ... game logic ...
  GlobalInputs, Mouse                 JD8_Flip():
  if new_frame_available:               palette→ARGB in pixel_data
    copy to game_frame                  publishFrame(pixel_data)  ⏸
    signal → ────────────────────→      (blocks until Director consumes)
  copy game_frame → present_buffer      ←──── signal_consumed
  Overlay::render(present_buffer)       continue game loop
  Screen::present(present_buffer)     }
  SDL_Delay to hit 60fps
}
```

**Key points:**
- Director is NON-BLOCKING: if no new frame, re-presents the last one with fresh overlay
- Double buffer: `game_frame` (untouched copy from game) + `presentation_buffer` (regenerated with overlay each frame)
- Game thread pauses at `JD8_Flip()` waiting for Director — natural sync point
- SDL events processed ONLY on main thread (SDL requirement)
- `JI_Update()` no longer polls events — reads Director's state

### 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) |

### 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`)**: `JI_CheatActivated` in [jinput.cpp:46](source/core/jail/jinput.cpp#L46) compares `SDL_Scancode` values (e.g. `SDL_SCANCODE_R`=21) against ASCII chars (`'r'`=114). They never match. Regression from SDL3 migration. Fix requires either scancode→char conversion in `JI_moveCheats` or storing chars directly.
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.

### Pending / Ideas for Later

- **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.

### 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) now lives inside `Director::gameThreadFunc()`, running on the game thread.
