Files
Crystal/docs/ARCHITECTURE.md
T
Sébastien QUEROLandClaude Opus 5.5 dc10343520
Checks / web (push) Canceled after 0s
Open Mr. Pokémon's house, the first rival battle and the return to Elm
Mr. Pokémon's ROM scene gives the Mystery Egg and Pokédex, heals the
party, sets the Cherrygrove spawn with blackoutmod and queues Elm's
call. Cherrygrove's rival scene runs both triggers and all three
RIVAL1_1 parties as a CANLOSE battle; reloadmap returns to the script.
Smart AI is accepted for effects with no AI_Smart_EffectHandlers entry
(Normal Hit, Attack Down, Defense Down) and still refuses others before
any random draw, including for reserve Pokémon.

Special calls read SpecialPhoneCallList: the entry's condition decides,
and the ROM's CheckSpecialPhoneCall.script and Script_ReceivePhoneCall
run its caller script through memcall, ending with HangUp's timed
"Click!". Ringing and the caller box are not presented yet.

In Elm's lab, NameRival shows the ROM's "RIVAL'S NAME?" title (the
prompt previously had no page and soft-locked) and applies InitName's
default to blank names. Handing over the Egg and the aide's Poké Balls
run from the ROM. Saves gain rivalName and specialPhoneCall.

The quest fixtures use a seeded RNG (a constant byte never ends the
damage-variation loop) and no longer carry the house's moved objects
into Cherrygrove.

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

7.0 KiB

Architecture

The default application is a native TypeScript game. It loads no CPU interpreter, Game Boy hardware model or WebAssembly emulator. The reference emulator has a separate entry point and build.

flowchart LR
  ROM[User ROM] --> Gate[Exact revision validation]
  Gate --> Assets[ROM reader and asset decoders]
  Assets --> World[Native world rules]
  Assets --> Scripts[Resumable event, movement and text interpreters]
  Scripts --> World
  Input[Touch and keyboard] --> Runtime[Native runtime]
  Runtime --> World
  World --> View[Canvas world and DOM dialogue]
  Session[Application session] --> Runtime
  Session --> Saves[Native save storage]
  Reference[Separate reference emulator] -. local comparisons only .-> World
Module Responsibility
src/rom/validate.ts Exact cartridge identity, no networking
src/game/rom Banked reads, LZ, maps/tilesets/sprites/palettes and pinned addresses
src/game/world Overworld ticks, movement, collision, warps, connections, scenes, callbacks and save validation
src/game/scripts The game's event, movement and text formats and translated specials, no GB instructions
src/game/rendering Planar pixels, metatiles, connection strips, adaptive camera, sprites, emotes and minimap
src/game/party Party Pokémon generation, stat formulas, bag pockets and their validation
src/game/runtime.ts 30 Hz tick scheduling from real time, input routing and presentation updates
src/input Abstract buttons, multiple sources and browser bindings
src/app Runtime-independent load/play/pause/backup lifecycle
src/storage Versioned envelope, IndexedDB transactions and ordered writes
src/ui, src/main.ts Accessible game tabs, UI composition and DOM dialogue, no ROM offsets
src/platform Browser locks and future native platform adapters
src/reference, src/emulator Original-game reference, excluded from the native build
reference/public Licensed emulator files for the reference build only

GameRuntime is a lifecycle interface, not an emulator contract. Native state is validated JSON, schema version 3. It holds the map, position, facing, name/avatar, player ID and in-game tile count; event and engine flags and sprite variables; scene values, phone contacts, a queued special call, the rival's name, the weekday offset and DST flag; positions of objects that scripts moved; and the party, bag, Pokédex sets and money. Early version-3 backups without a money field migrate to the starting balance. It contains no graphics, music, dialogue, GB memory or ROM bytes. Versions 1 and 2 migrate with new-game defaults. Party Pokémon are validated by recomputing their stats from the ROM. The backup envelope retains the historical coreId field; native backups use it for their runtime/schema ID.

Saves made during an unfinished walk or jump retain its source tile and completed step count, so loading cannot skip a landing event or map transition. Directional mats wait for exit input; doors, stairs and caves force an animated downward step after arrival.

Native and reference sessions use different IndexedDB databases and Web Locks. Imports reject incompatible schemas and invalid positions before changing the world. A failed import restores the prior state. Original cartridge saves and emulator snapshots cannot be used as native saves.

GameWorld advances in overworld ticks of two video frames, as the original loop does. The runtime converts elapsed time to ticks. Tile events follow CheckTileEvent: a completed step checks connections, then warps, then coordinate events for the current scene. Idle ticks run the current scene script. Map entry runs the NEWMAP, TILES and OBJECTS callbacks on a copy first, so an unsupported callback refuses the step instead of entering a half-initialized map. Objects spawn from event flags at map load, and later flag changes leave them in place until appear/disappear or the next load.

Scripts are generators that evaluate against a private draft and yield presentation effects: pages, yes/no or weekday choices, movement, emotes and pauses. The world presents each effect and resumes the script with the answer, or after the animation's ticks. Unknown commands, movement commands and specials abort. Aborting discards the draft and restores the positions the scene changed. The draft commits only when the script ends. A save made during a scene contains the committed state, and coordinate events rewind to the tile before the triggering step, so the scene replays without partial rewards or flags. Specials are translated natively and selected by the routine that the ROM's special table points to.

Asset caches and the ROM reader remain in memory for lazy extraction. Neither ROMs nor extracted assets enter IndexedDB, the build or a backend. Vite blocks serving the root ROM and private reference checkout during development.

The native viewport follows its available CSS dimensions and draws additional world tiles at a bounded pixel resolution. Beyond a map's edge it draws the connected map with the current tileset, as the original connection strips do, and the border block elsewhere. Resizing also redraws paused sessions. The local minimap caches terrain from the loaded map and updates its marker from the same world position used by the player; the Map tab shares that view. It is a current-area map, not a full region atlas. Leaving Game pauses the session and releases held input; returning requires explicit play. The reference player retains its own fixed-screen styling.

game/battle now holds the native battle core and ROM-backed battle tables. Browser tests cover the overworld-to-battle flow using a test-only encounter fixture; Route 29 uses ROM encounter tables. Trainer battles share that core, with ROM trainer decoders, the trainer's send-out and switch AI, and explicit AI support checks. Map trainers (sight and talk) are started by game/world and run the ROM's own trainer scripts. Introduce future modules with working features: game/audio, then features/link and features/walking. Party and bag models live in game/party; scripts change them only on their draft state. Rules must not import UI/browser modules. Transports and step providers must not write directly into world state.