Files
aee/CLAUDE.md
2026-04-16 18:46:58 +02:00

30 KiB
Raw Blame History

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

# 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 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.
  • 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 screenpixel_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/): 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 screenpixel_data) → overlay → present → SDL_Delay al frame target. Dispatcher: game_state_ == 0new ModuleGame, game_state_ == 1SceneRegistry::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 (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/cppOptions namespace with inline globals and YAML load/save. Structs: KeysGame, Video, RenderInfo, Audio, Window, Game, PostFXPreset, CrtPiPreset

Utilities (source/utils/)

  • utils.hpp/cpptoLower() 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

UI/system key bindings are loaded from 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. Cada frame entra pel SDL_AppIterateDirector::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 (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). 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 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:

Build:

  • make pack compila l'eina (target pack_resources a EXCLUDE_FROM_ALL de CMakeLists.txt) i genera resource.pack a la rel. 33 entrades ≈ 4 MB.
  • ./build/pack_resources --list resource.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 (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, text.cpp, scene_utils.cpp, modulegame.cpp, jdraw8.cpp.
  • Scaffold .jrf eliminat de 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 (_linux_release/_windows_release/_macos_release) depenen de pack i copien resource.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 (00000001.ogg..00000008.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), 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). 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 screenpixel_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 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 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 (getpwuidgetenv("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_setconfigfolderOptions::loadLocale::load("locale/ca.yaml")Options::loadPostFX/CrtPiJG_InitScreen::initJD8_InitJA_InitOptions::applyAudio()Overlay::initMenu::initDirector::init (also calls Gamepad::init()) → Director::run() (blocks until quit). Shutdown: Options::saveDirector::destroyMenu::destroyOverlay::destroyJA_QuitJD8_QuitScreen::destroyJG_Finalize.

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