Files
Crystal/docs/NATIVE_PORT.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

16 KiB

Native port status and reference checks

Reproducible reference

The private .reference/pokecrystal checkout is pinned to 7a7881d0d62e0ddbd82dcf10e7116807487ac651. With RGBDS 1.0.3, its crystal11 build is byte-identical to the supplied English US/Europe revision 1.1 ROM. npm run check:reference verifies the files and commit. It checks every ROM and RAM address in src/game/rom/profile.ts against generated symbols, event constants against constants/event_flags.asm, and map-group bounds against data/maps/maps.asm.

To recreate the private reference:

git clone https://github.com/pret/pokecrystal.git .reference/pokecrystal
git -C .reference/pokecrystal checkout 7a7881d0d62e0ddbd82dcf10e7116807487ac651
make -C .reference/pokecrystal crystal11 -j4
npm run check:reference

Install RGBDS 1.0.3 separately. No script downloads a game ROM. CRYSTAL_ROM_PATH optionally selects another local location; checks otherwise use Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc at the root.

The reference checkout, rebuilt ROM, extracted graphics, saves and gameplay screenshots must stay local and ignored. The shipping profile contains addresses and format constants, not game content.

Routine map

These paths refer to the pinned PRET source.

Native module PRET reference Evidence and limits
game/rom/lz.ts home/decompress.asm All LZ command types have unit vectors; six tilesets match original ASM execution byte-for-byte.
game/rom/assets.ts maps/events data/maps/maps.asm, data/maps/attributes.asm, data/maps/scenes.asm, data/maps/landmarks.asm, macros/scripts/maps.asm Any map header in the ROM's 26 groups decodes; only maps explicitly listed in NATIVE_MAPS can be entered. Location names, scene-variable owners and connection offsets come from the ROM.
Tiles/palettes home/map.asm, engine/gfx/color.asm, engine/tilesets/mapgroup_roofs.asm Graphics match rebuilt reference bytes. Connected maps draw beyond the edges with the current tileset, like the original strips. Terrain animation and other special palettes pending.
Sprites/rendering data/sprites/sprites.asm, data/sprites/facings.asm, data/sprites/emotes.asm Chris/Kris, standard NPCs, emote bubbles and jump shadows decoded. Walking frames follow SetFacingStepAction. Dynamic Pokémon icons and room decorations pending.
game/world/world.ts engine/overworld/events.asm, player_movement.asm, warp_connection.asm, data/collision/collision_permissions.asm 30 Hz overworld ticks, 8-tick walks, 16-tick ledge jumps, directional mats, animated door/stair exits, connection coordinates, events, scenes and callbacks. NPC random walks and spins follow HandleNPCStep. Surfing remains absent; turn timing still needs comparison.
Event interpreter macros/scripts/events.asm, engine/overworld/scripting.asm, engine/events/std_scripts.asm Resumable; yields text, choices, movement, emotes and pauses. Scenes, phone contacts, string buffers and object commands. Unknown commands and specials abort without committing.
Movement interpreter macros/scripts/movement.asm, engine/overworld/movement.asm, engine/overworld/map_objects.asm Turns, steps, slides, jumps (two tiles with the original arc), sleeps, facing locks, visibility and emotes. follow moves the follower into the leader's previous square.
Specials data/events/special_pointers.asm, engine/rtc/timeset.asm, engine/overworld/decorations.asm Selected by routine address from the ROM table: SetDayOfWeek, initial DST flags, RestartMapMusic (no audio yet), the empty-room decoration toggles, HealParty, HealMachineAnim (presentation only), CheckPokerus and the StubbedTrainerRankings_Healings stub.
game/rom/pokemon.ts data/pokemon/base_stats, evos_attacks.asm, data/moves/moves.asm, data/items/attributes.asm, engine/gfx/load_pics.asm Species, move and item names, base data, learnsets, move PP, item pockets, growth-rate exp and front pictures (FixPicBank, PadFrontpic). Starter pictures match rebuilt bytes.
game/party engine/pokemon/move_mon.asm, engine/pokemon/caught_data.asm, engine/items/items.asm GeneratePartyMonStats for script gifts: DVs, FillMoves, PP, exp, CalcMonStatC, happiness and caught data. PutItemInPocket, key items and TossItem. PC boxes, TMs and HMs are refused.
Text interpreter macros/scripts/text.asm, constants/charmap.asm, home/text.asm Literal/far text, RAM names and string buffers from native state, numbers of dots, and sound or pause cues (consumed). text_asm and other RAM reads are refused.

Supported event opcodes are explicit in the interpreter switch. Audio commands consume their operands, but sound sequencing is pending; those opcodes do not imply complete audiovisual behavior. pause holds the current text or waits the original number of overworld ticks.

Scripts run on a private draft. The world presents each yielded effect, then resumes the script with the player's answer or when the animation ends. The draft is committed only when the script ends. A save made during a scene returns the committed state, and coordinate events rewind to the source of the triggering walk or two-tile jump. Saves during movement also retain the source tile until arrival events run, including across map connections. A failed scene restores the positions it had changed. As in LoadMapObjects, map objects spawn when the map loads. Later flag changes do not remove objects already shown; only appear and disappear do.

The opening scenes currently run end to end from the ROM: Mom's Pokégear, weekday and DST scene, the teacher at the west edge, the rival at the lab window, Elm's introduction with its yes/no loop, the lab's exit guard, the starter choice with pokepic, givepoke and the nickname prompt, Elm's directions and the aide's Potion through the ROM's own GiveItemScript. callasm accepts only explicitly empty routines or native translations: HalveMoney, GetWhiteoutSpawn, the item ball's TryReceiveItem and the fruit tree routines. The naming screen is a native text field limited to the characters the original offers.

Actual emulator comparison

tests/reference-parity.spec.ts loads the user's ROM into the reference emulator and places a small original test trampoline in emulated WRAM. It calls the ROM's FarDecompress routine at $0b40, then sets a return sentinel. Every output byte is compared with the native decoder for six supported map tilesets. This executes the original routine rather than comparing two copies of our code.

The same suite compares 400 damage cases and 400 type-effectiveness cases per browser against the original battle routines. Battle unit tests cover win, escape, loss, experience, level-up and move-learning flows. This establishes parity only for the compared routines and inputs. It does not prove complete gameplay, rendering, battles, audio, timing or physical-device parity. Extend controlled comparisons as each subsystem is translated.

Validation

npm test
npm run test:e2e
npm run check:reference
npm run check:vendor
npm run build
npm run check:release
npm run build:reference

User-ROM tests skip when the file is absent, including public CI. Synthetic tests still run. Comparing with rebuilt graphics also needs the private PRET build. Do not report skipped private checks as CI passes.

Native production is dist/; the optional reference output is dist-reference/. Release checks reject emulator dependencies, binary game files and embedded binary assets in the native output. Source review is still required when adding assets or data.

Save schema

Native saves are version 3. Version 2 added scene values by owning map, phone contacts, the weekday offset and DST flag, and positions of objects that scripts moved on the current map. Version 3 adds the player ID, party, bag, Pokédex seen/caught sets and money. Early version-3 backups without money receive the original starting balance; explicitly invalid balances are rejected. Restores recompute each Pokémon's stats from its DVs and reject any mismatch, and check every item against its ROM pocket. Older saves migrate with new-game defaults; a missing player ID is drawn as a new game draws it. Objects are restored from the save on continue, and only the TILES callback runs, as in MapSetupScript_Continue.

Scripted trainer battle foundation

game/rom/trainers.ts decodes TrainerGroups, TrainerClassAttributes, TrainerClassNames, TrainerClassDVs and map trainer headers. Tests cover all 542 ROM records across 67 classes; CAL2 construction refuses its SRAM-dependent Mystery Gift party.

game/battle/ai.ts translates AIChooseMove, AI_Redundant and the Basic, Setup, Types, Offensive, Opportunist, Aggressive, Cautious, Status and Risky layers. It preserves the original random tie selection, known redundancy quirks, HP thresholds, exclusions and the deterministic pre-variation damage estimate used by Aggressive and Risky. Smart runs only for move effects that have no AI_Smart_EffectHandlers entry (Normal Hit, Attack Down, Defense Down; enough for the first rival). Other effects are refused before any random draw. Synthetic scoring vectors establish the tested branches; original-engine AI execution parity is still pending.

loadtrainer, winlosstext and startbattle run scripted trainer battles through the existing battle UI. Tests cover Joey's ROM party/text/reward, RUN refusal, trainer experience, Amulet Coin, CANLOSE behavior, money limits, transactional rollback and midbattle restore. Chromium and WebKit exercise the UI with a test-only script.

Map trainers

home/trainers.asm and engine/events/trainer_scripts.asm: talking to an OBJECTTYPE_TRAINER object runs TalkToTrainerScript with the object's header copied as wTempTrainer; _CheckTrainerBattle runs SeenByTrainerScript with wSeenTrainerDistance. The world checks sight in PlayerEvents order: before warps, coordinate events and encounters at the end of a step (a seen player's tile events are then skipped, as DisableEvents does), and on idle ticks before scene scripts and input. Walls and objects do not block sight. TrainerWalkToPlayer/ComputePathToWalkToPlayer build the raw wMovementBuffer bytes, which applymovementlasttalked decodes with the normal movement reader. wRunningTrainerBattleScript lives in the script run: zero at the start, set by loadmem after the battle so endifjustbattled ends the after-script. Trainer commands without a map trainer are refused. MomTriesToBuySomething after scripted battles does nothing while Mom's savings (not ported) are zero.

engine/battle/core.asm and engine/battle/ai/items.asm, switch.asm: when a trainer's Pokémon faints and others remain, UpdateBattleStateAndExperienceAfterEnemyFaint zeroes its party HP and runs NewEnemyMonStatus; EnemyPartyMonEntrance then sends out the Pokémon chosen by FindMonInOTPartyToSwitchIntoBattle. When every candidate is threatened it draws BattleRandom & 7, which tests pin. AI_SwitchOrTryItem runs before either side moves: CheckAbleToSwitch, the Often/Rarely/Sometimes thresholds, AI_TrySwitch and AI_Switch (withdraw text, HP and status back to the party). CheckTypeMatchup ignores the type its switch-AI callers load and uses BATTLE_VARS_MOVE_TYPE, so every such matchup uses the enemy's chosen move type; FindEnemyMonsWithASuperEffectiveMove never resets its result between Pokémon; FindEnemyMonsWithAtLeastQuarterMaxHP rotates HP bits instead of multiplying. All three bugs are kept. UsedMoveText records wPlayerUsedMoves. Refused: the Shift-mode offer to switch (OfferSwitch), AskUseNextPokemon with other Pokémon left, Pursuit on a switching Pokémon, Perish Song, Spikes, and trainer items. Side effects of the type scans on the move structs (overwritten types and animation) are not modelled; the next ParseEnemyAction/ParsePlayerAction reloads them.

BGEVENT_ITEM runs HiddenItemScript with wHiddenItemData; SetMemEvent sets the item's flag.

A trainer sighting whose script is refused is not restarted until the player moves. This is a native safeguard, not original behavior.

Next slice

Route 29, Cherrygrove, Route 30 and Mr. Pokémon's house are enabled with item balls, hidden items, fruit trees and the daily reset, the Pokémon Center nurse, the recorded whiteout spawn, map trainers, the first rival battle and Elm's special call. NPCs roam. Next: capture with the catching tutorial, then party, bag and Pokégear menus over the native models. Other specials stay refused until translated.

Reflect and Light Screen

BattleCommand_Screen in engine/battle/effect_commands.asm and HandleScreens in engine/battle/core.asm now run natively. Each screen has a side-owned five-turn counter, including its setup turn. Repeated use fails without refreshing it; send-outs preserve it. Expiration runs after defrost, before healing items, player before enemy and Light Screen before Reflect. Messages come from the ROM. The existing damage path doubles the appropriate defense and retains the original critical/stage rule. Safeguard and other screen flags remain refused. Seven local-ROM tests cover move execution, counters, PP, damage stats and send-out persistence; no screen-routine emulator parity or physical-device validation is claimed.

Mr. Pokémon, the first rival and special calls

MrPokemonsHouse.asm, CherrygroveRivalScene* and the ElmsLab.asm officer and Egg scripts run from the ROM. Supporting translations: blackoutmod, reloadmap (returns to the script; native presentation keeps the map), readvar VAR_SPECIALPHONECALL and memcall (only through wCallerContact + PHONE_CONTACT_SCRIPT2_BANK). Specials: NameRival (native naming field, ROM title and InitName default), FadeOutMusic, and the presentation-only fades. CountStep's CheckSpecialPhoneCall reads the queued SpecialPhoneCallList entry. It checks SpecialCallOnlyWhenOutside or SpecialCallWhereverYouAre, then runs CheckSpecialPhoneCall.script with that entry's caller script. RingTwice_StartCall and InitCallReceiveDelay are accepted without effect, because ringing presentation and random calls are not ported. HangUp shows "Click!" for its 140 frames. Local-ROM tests cover the house, both rival triggers with all three parties, a rival loss, the call indoors, outdoors and on reload, the officer's naming (including blank names and reload at the naming screen), and the Egg and Poké Ball handovers. Browser tests cover the house, the rival battle and the naming field.