Tin Tower's floors and roof and the Whirl Islands are open. The Clear Bell gets the player past the sages to Suicune in Tin Tower; after the League, the Rainbow Wing leads up to Ho-Oh, and the Silver Wing to Lugia. New specials: MonCheck and BeastsCheck (CheckOwnMonAnywhere, with its five-letter OT comparison and no Day-Care search). The playthrough driver plans ledge jumps for the later stages, waterfall descents, warp panels and Surf between warps; the run gains two stages, Routes 45 and 46 back to Ecruteak and Tin Tower's Suicune. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
671 lines
70 KiB
Markdown
671 lines
70 KiB
Markdown
# 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:
|
||
|
||
```sh
|
||
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 for local checks. `CRYSTAL_ROM_PATH` optionally selects another local location; checks otherwise use `Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc` at the root. The private CI workflow downloads the user's ROM from the `CRYSTAL_ROM_URL` repository secret, verifies its size and SHA-1, and builds this reference with the pinned RGBDS 1.0.3 source. It runs only for `main`, never pull requests.
|
||
|
||
The reference checkout, rebuilt ROM, extracted graphics, saves and gameplay screenshots must stay in local or private CI workspaces and remain ignored. Private CI deletes them and does not upload artifacts or cache game data. The shipping profile contains addresses and format constants, not game content.
|
||
|
||
## Routine map
|
||
|
||
These paths refer to the pinned [PRET source](https://github.com/pret/pokecrystal/tree/7a7881d0d62e0ddbd82dcf10e7116807487ac651).
|
||
|
||
| 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. Native tileset animation and special palettes; Johto, Ice Path, restless Ilex Forest and Tin Tower each match 96 reference VRAM frames. 192 palette cases match the original routine. |
|
||
| 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`. Variable sprites and Pokémon icons decoded; grass priority supported. Owned room decorations support selection, removal, slot placement, descriptions and validated saves. |
|
||
| `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`. Forced movement, Surf, Cut, Strength, cycling rules, pits and elevators implemented. Fourteen reference traces compare turn timing, actors and event flags; map-entry boundaries verify the completed destination. The Bicycle is used from the pack or with Select. Field moves chosen from the party menu are checked here (`menuFieldMove`). |
|
||
| 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` takes one tile toward the leader's previous square. All movement opcodes decode, including teleport, fishing, skyfall, shake and flicker poses; Controlled reference scenes check spin, teleport, skyfall, fishing and flicker direction, visibility and lift. |
|
||
| 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), room decoration toggles, `PokemonCenterPC` and `PlayersHousePC` (Bill's, the player's and Oak's PCs), the Town Map, `HealParty`, `HealMachineAnim` (presentation only), `CheckPokerus` and the `StubbedTrainerRankings_Healings` stub. |
|
||
| `game/scripts/pc.ts` | `engine/events/pokecenter_pc.asm`, `engine/pokemon/bills_pc_top.asm`, `engine/pokemon/bills_pc.asm`, `engine/events/prof_oaks_pc.asm`, `engine/events/std_collision.asm` | The Pokémon Center and house PCs: Bill's PC (deposit, withdraw, release, change and name boxes, MOVE PKMN W/O MAIL, stats), item storage, the MAIL BOX (`MailboxPC`) and Oak's rating. Menu strings and texts come from the ROM; lists are native. Printing is not ported. |
|
||
| `game/scripts/start-menu.ts` | `engine/menus/start_menu.asm`, `engine/pokemon/mon_menu.asm`, `mon_submenu.asm`, `party_menu.asm`, `switchpartymons.asm`, `engine/items/pack.asm`, `engine/items/item_effects.asm` | The start menu, the party menu with the Pokémon submenu (STATS, SWITCH, MOVE, ITEM) and the pack (USE, GIVE, TOSS) with the field item effects listed below, including Rare Candy, stones and vitamins. Menu strings, prompts and texts come from the ROM; screens are native. Field moves from the menu (checked by the world), Softboiled and Milk Drink, SEL and the Select button (`SelectMenu`), MAIL (`MonMailAction`, `ComposeMailMessage`) and `SwitchItemsInBag`. `party/mail.ts` holds the `mailmsg` struct. |
|
||
| `game/scripts/pokegear.ts` | `engine/pokegear/pokegear.asm`, `engine/phone/phone.asm`, `engine/phone/scripts/mom.asm`, `elm.asm`, `engine/rtc/print_hours_mins.asm` | `PokeGear` with each card's joypad handling: the clock (`Pokegear_UpdateClock`, `PrintHoursMins`), the map card's landmark cursor (`TownMap_InitCursorAndPlayerIconPositions`, `TownMap_GetKantoLandmarkLimits`), the phone list (`PokegearPhone_UpdateDisplayList`, `GetCallerClassAndName`, `PokegearPhoneContactSubmenu`, `CheckCanDeletePhoneNumber`) and calls (`MakePhoneCallFromPokegear`, `CheckPhoneContactTimeOfDay`, `GetMapPhoneService`, `HangUp`). Callee scripts run in the event interpreter on the start menu's draft. Screens are native; the radio's stations are not ported. |
|
||
| `game/scripts/trainer-card.ts` | `engine/menus/trainer_card.asm`, `engine/gfx/player_gfx.asm` (`GetCardPic`), `engine/gfx/cgb_layouts.asm` (`_CGB_TrainerCard`), `home/game_time.asm` | `TrainerCard`'s two reachable pages with their joypad rules, strings from `.Name_Money`, `.Dex_PlayTime` and `.Badges`, the caught count, and the badges from `TrainerCard_JohtoBadgesOAM` with their eight-frame steps. `rendering/trainer-card.ts` draws the card picture, leader faces and badges from the ROM with the card's palettes and attribute map. `world/game-time.ts` is `GameTimer` (capped at 999:59:59). Text is native; the blinking colon's and badges' phase start at the page load. |
|
||
| `game/scripts/pokedex.ts`, `game/rom/pokedex.ts` | `engine/pokedex/pokedex.asm`, `pokedex_2.asm` (`DisplayDexEntry`, `GetDexEntryPointer`), `pokedex_3.asm` (`LoadQuestionMarkPic`), `data/pokemon/dex_order_new.asm`, `dex_entry_pointers.asm`, `dex_entries/`, `gfx/footprints.asm`, `engine/gfx/cgb_layouts.asm` (`_CGB_Pokedex`) | New mode's main screen and entry screen with their joypad rules: `Pokedex_ListingHandleDPadInput`, `Pokedex_InitCursorPosition`, `Pokedex_NextOrPreviousDexEntry`, `Pokedex_MoveArrowCursor` and `Pokedex_Page`. Strings come from the ROM; screens are native. Tests compare the order, all 251 entries and every footprint with the PRET source. AREA, CRY, PRNT, the option and search screens, Old and A to Z modes and Unown mode are not ported. `ui/pokedex.ts` draws the screen and the Pokédex tab. |
|
||
| `game/world/atlas.ts` | `data/maps/landmarks.asm`, `data/maps/flypoints.asm`, `engine/pokegear/pokegear.asm` (`_FlyMap`, `CheckIfVisitedFlypoint`), `home/map.asm` (`GetWorldMapLocation`) | The Map tab's atlas: each region's landmarks with their Town Map positions and ROM names, the player's landmark, the visited flypoints and the landmarks with ported maps. Kanto follows the Fly map's Indigo Plateau rule. `rendering/town-map.ts` draws it. Tests compare positions and flypoints with the PRET source. |
|
||
| `game/scripts/options.ts` | `engine/menus/options_menu.asm`, `data/default_options.asm`, `engine/battle/core.asm` (`CheckWhetherToAskSwitch`), `engine/menus/start_menu.asm` | `_Option` with `OptionsControl` and each option's routine on the four option bytes, `DefaultOptions`, and the two settings consumed so far: `BATTLE_SHIFT` and `MENU_ACCOUNT`. Strings come from the ROM; the screen is native. |
|
||
| `game/scripts/fly-map.ts` | `engine/pokegear/pokegear.asm` (`_FlyMap`, `FlyMap`, `TownMapBubble`, `CheckIfVisitedFlypoint`), `data/maps/flypoints.asm`, `engine/events/field_moves.asm` (`FlyFromAnim`, `FlyToAnim`), `engine/sprite_anims/functions.asm` | The Fly map's region choice (Kanto only after Indigo Plateau, without the player's icon otherwise), the default flypoint and up/down scrolling over visited flypoints (`wVisitedSpawns` through the ROM's `EngineFlags`). The flights follow `SpriteAnimFunc_FlyFrom` and `SpriteAnimFunc_FlyTo` frame by frame with the party icon. The Town Map, bubble and icons are drawn natively; the leaves are not presented. |
|
||
| `game/scripts/evolution.ts` | `engine/pokemon/evolve.asm`, `engine/pokemon/learn.asm`, `engine/movie/evolution_animation.asm`, `engine/pokemon/party_menu.asm` | `EvolveAfterBattle`/`EvolvePokemon` over the raw evolution bytes, the evolution texts and a timed, B-stoppable picture sequence, `UpdateSpeciesNameIfNotNicknamed`, `CalcMonStats` and the HP rise, `LearnLevelMoves`, `LearnMove`/`ForgetMove` outside battle and `PlacePartyMonEvoStoneCompatibility`. |
|
||
| `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, evolution lists, learnsets, move PP, item pockets, growth-rate exp and front pictures (`FixPicBank`, `PadFrontpic`). Starter pictures match rebuilt bytes; all 251 evolution lists match the pinned source. |
|
||
| `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`. `SendMonIntoBox`, `SendGetMonIntoFromBox` (deposit and withdraw), `RestorePPOfDepositedPokemon` and `CalcLevel` for the PC's boxes and item storage. TMs and HMs are refused. |
|
||
| `game/scripts/screen.ts`, `game/rendering/tile-screen.ts`, `game/rom/font.ts` | `home/tilemap.asm`, `engine/gfx/load_font.asm` (`_LoadStandardFont`, `LoadFrame`) | Tile screens: a 20x18 tilemap with background palettes and sprites over it, built by a minigame or special from ROM graphics and answered with buttons. Blinking sprites follow bit 4 of the frame counter. Overlay screens (a menu box) leave the map visible around them. The standard font and the option's textbox frame load as 2bpp tiles. |
|
||
| `game/scripts/unown-puzzle.ts` | `engine/games/unown_puzzle.asm`, `engine/events/specials.asm` (`UnownPuzzle`), `engine/gfx/cgb_layouts.asm` (`_CGB_UnownPuzzle`) | `_UnownPuzzle`: `InitUnownPuzzlePiecePositions` with Random, the d-pad rules, picking up and placing pieces, `CheckSolvedUnownPuzzle`, and the graphics (`ConvertLoadedPuzzlePieces`' doubling, the piece borders, the cursor and START/CANCEL, the OAM layouts) and palettes from the ROM. Sounds are not played. |
|
||
| `game/scripts/ruins.ts` | `engine/events/unown_walls.asm`, `data/events/unown_walls.asm` | `DisplayUnownWords` as an overlay screen with the chamber tileset's Unown letters, `HoOhChamber` and `OmanyteChamber`. |
|
||
| `game/scripts/goldenrod.ts`, `game/world/day-timers.ts` | `engine/events/name_rater.asm`, `haircut.asm`, `lucky_number.asm`, `engine/overworld/time.asm`, `engine/menus/intro_menu.asm` | Name Rater ownership checks, ROM haircut happiness tables, Lucky Number party and box matching with first-winner ties, saved number and Friday countdown with 140-day rollover. |
|
||
| Goldenrod shops, gifts and prizes | `engine/items/mart.asm`, `engine/pokemon/move_mon.asm` (`GivePoke`), `engine/events/specials.asm` (`GameCornerPrizeMonCheckDex`), `engine/pokedex/new_pokedex_entry.asm` | All five mart types, gift OT/nickname and MAIL, full-party gifts to the box, coin commands and balance boxes, and first-owned prize registration with two A/B pages. Boxed gifts approximate an unspecified stale enemy held item as zero. SlotMachine and CardFlip run as described below. |
|
||
| Route 35, Route 36, Route 37, Ecruteak and Burned Tower | `home/trainers.asm` (`_CheckTrainerBattle`), `mobile/mobile_41.asm` (`CheckMobileAdapterStatusSpecial`), `engine/overworld/overworld.asm` (`LoadUsedSpritesGFX`, `RefreshSprites`) | These maps' scripts (Floria and the Sudowoodo Squirtbottle battle, Arthur, Sunny, the Itemfinder house, the Kimono Girls and HM03 Surf, Eusine and the rival's Burned Tower cutscene) run on already-ported primitives. `checkTrainers` skips a map trainer whose script is `ObjectEvent` (the Burned Tower rival) once its own flag is set: the original reads `ObjectEvent`'s bytes as flag $f251, bit 1 of `wNumKeyItems` through echo RAM. `tests/reference-trainer-sight.spec.ts` runs `_CheckTrainerBattle` for every `wNumKeyItems` 0-6: the original matches the port only with two, three or six key items; otherwise it reports a sighting and `LoadTrainer_continue` copies `ObjectEvent`'s own bytes into `wTempTrainer` as a trainer header (class $26 SWIMMERM, ID $16), one past SWIMMERM's last trainer. `ReadTrainerParty`'s name-skipping loop then runs off the end of its table into the next one with no padding between them, landing on a real, undamaged record: SwimmerF's ELAINE (a single level 21 Staryu), shown with SWIMMERM's sprite, cry and class text — confirmed by running `ReadTrainerParty` and `Battle_GetTrainerName` themselves. That battle is never reached cleanly: `StartBattleWithMapTrainerScript` first prints the decoded seen text ($46ee in the map scripts' bank, the middle of `SproutTower1FStatueText`). Its first byte, the space $7f, is dispatched as a text command; `TextCommands` has 23 entries, so the pointer comes from home code beyond the table and execution jumps to $0428, inside the VBlank handlers. What follows depends on the whole CPU and stack state, so the port keeps skipping the sighting rather than inventing an outcome. The pursuit stops here: the trainer check runs before `CheckTileEvent`'s warp, so the sighting is reachable after the cutscene's `warpcheck`, but no end-to-end emulator replay of the scene was made (the repository has no cartridge-save export). `CheckMobileAdapterStatusSpecial` is unused outside the Japanese release and always clears its result; `LoadUsedSpritesGFX`/`RefreshSprites` are VRAM sprite tile loading, presentation only. |
|
||
| `game/world/roam-mons.ts` | `engine/overworld/wildmons.asm` (`InitRoamMons`, `CheckEncounterRoamMon`, `UpdateRoamMons`, `JumpRoamMons`), `data/wild/roammon_maps.asm` | Raikou and Entei roam from `InitRoamMons`; Suicune's slot is kept but not started. `UpdateRoamMons` runs on connections, door and pit warps and after one wild battle in sixteen; `JumpRoamMons` on Fly, Teleport and Continue. A roaming beast can take a grass encounter, and the battle writes its HP back or clears the slot. |
|
||
| Bug-Catching Contest | `engine/events/bug_contest/*.asm`, `maps/Route35NationalParkGate.asm`, `maps/Route36NationalParkGate.asm`, `engine/menus/start_menu.asm` | Registration, Park Balls, contestant selection, the timer on the game clock, out of balls, QUIT, judging from `BugContestantPointers` with ties to the earlier entry, prizes and `CheckPartyFullAfterContest`. The contest state saves. The status box is a text page above the start menu. No sound. |
|
||
| `game/scripts/cianwood.ts`, Routes 38–41, Olivine and Cianwood | `engine/events/shuckle.asm` (`GiveShuckle`, `ReturnShuckie`), `engine/pokemon/bills_pc_top.asm` (`CheckCurPartyMonFainted`), `engine/pokemon/caught_data.asm` (`SetGiftPartyMonCaughtData`), `engine/events/poke_seer.asm`, `engine/events/print_photo.asm`, `engine/printer/printer.asm` (`PrintPartymon`, `CheckPrinterStatus`), `engine/printer/printer_serial.asm`, `engine/events/specials.asm` (`PlaySlowCry`) | Shuckie is a level 15 party-only gift with MANIA's OT, ID 518, nickname, a Berry and `LANDMARK_GIFT` caught data; it sets `ENGINE_GOT_SHUCKIE_TODAY`. `ReturnShuckie` returns all five codes; `CheckCurPartyMonFainted` looks at the rest of the party, and Mania answers `SHUCKIE_FAINTED` with his happiness speech, as the original does. `PokeSeer` follows `ReadCaughtData`, including the missing `cp [hl]` that compares only the OT ID's high byte, `GetCaughtLocation`'s event and gift landmarks, `GetCaughtLevel`'s ??? that leaves `wSeerCaughtLevel` unchanged, and `SeerAdvice`'s inclusive thresholds. `wSeerCaughtLevel` lives in the "Miscellaneous WRAM 1" union: the port keeps the Seer's own last value until a load, while in the original other users of those bytes may change it. `PhotoStudio` has no printer to talk to: the handshake reads $ff, so `CheckPrinterStatus` reports `PRINTER_ERROR_2`, and `SendScreenToPrinter` retries until B sets `hPrinter`. The port shows "Printer Error 2" with "Press B to Cancel" on one page, then the no-photo text; the print page behind the box and its alternation with "CHECKING LINK" are not drawn. `PlaySlowCry` is audio. The story maps run on ported script commands; `tests/olivine-cianwood.test.ts` and six playthrough stages cover them. |
|
||
| `game/scripts/lake-of-rage.ts`, Route 42 to Mahogany, the Lake of Rage, the Rocket Hideout and Pryce | `engine/events/magikarp.asm` (`CheckMagikarpLength`, `CalcMagikarpLength`, `PrintMagikarpLength`, `MagikarpHouseSign`), `engine/menus/intro_menu.asm` (`InitializeMagikarpHouse`), `engine/pokemon/search_party.asm` (`FindThatSpecies`), `engine/overworld/scripting.asm` (`DoScene`), `engine/events/money.asm` (`TakeMoney`) | The Magikarp is measured from its own DVs and OT ID with the battle code's `CalcMagikarpLength` (whose `.BCLessThanDE` compares only the high byte). A longer one, compared feet then inches, becomes the record with the Magikarp's OT name as holder; the record starts at 3′6″ by RALPH and is saved as `magikarpRecord`. `FindThatSpecies` reads `wPartySpecies`, where an Egg is EGG, so an Egg is never found. `setmapscene`/`setscene` on a map without a scene variable (Route 43, the Lake of Rage): `GetMapSceneID` leaves `de` alone and `DoScene` still writes the byte through it when it is nonzero. At the Route 43 gate `de` is `EventFlagAction`'s byte offset of `EVENT_CLEARED_ROCKET_HIDEOUT` (4), the MBC3 RAM-enable register, so nothing changes. After the Red Gyarados, `de` is whatever `closetext` left; the port consumes the byte and changes nothing, and that write was not checked in an emulator. `TakeMoney` leaves a short account at zero, so the Route 43 Rockets take all of a smaller balance. The Rocket Hideout's cameras, traps, passwords, executives and Electrodes and Pryce's ice floor run on ported primitives; `PlayCurMonCry` is audio. `tests/mahogany.test.ts` and five playthrough stages cover them. |
|
||
| `game/scripts/blackthorn.ts`, the Radio Tower takeover, Ice Path, Blackthorn and Dragon's Den | `engine/events/dratini.asm` (`GiveDratini`), `engine/events/move_deleter.asm` (`MoveDeletion`), `engine/pokemon/mon_menu.asm` (`ChooseMoveToDelete`), `engine/events/whiteout.asm` (`Script_Whiteout`), `data/maps/setup_scripts.asm` (`MapSetupScript_Continue`) | `GiveDratini` searches the party backwards by species and writes `.Movesets` 0 (WRAP, THUNDER WAVE, TWISTER, EXTREMESPEED) or 1 with each move's base PP; a value of 2 or more changes nothing. `MoveDeletion` follows the routine's order of questions; `ChooseMoveToDelete` is shown as a list of the moves, and `.DeleteMove` shifts the later moves and PP bytes, PP Ups included. A save records objects whose visibility no longer matches their flags: `MapSetupScript_Continue` skips `LoadMapObjects`, so a flagless object a script made disappear (the Radio Tower director) stays gone after Continue and one that appeared with its flag set stays visible; a later visit rebuilds them from the map. A battle lost inside an `scall` jumps to `Script_Whiteout` without unwinding; its `newloadmap` is followed by `endall`, which drops the caller. The switch room's doors, the warehouse's reset, the Ice Path and Blackthorn Gym stone tables, whirlpools and the Dragon Shrine quiz run on ported primitives. `tests/blackthorn.test.ts`, `tests/radio-tower.test.ts` and six playthrough stages cover them. |
|
||
| `game/scripts/tin-tower.ts`, Tin Tower and Whirl Islands | `engine/pokemon/search_owned.asm` (`BeastsCheck`, `MonCheck`, `CheckOwnMonAnywhere`, `CheckOwnMon`) | `CheckOwnMonAnywhere` returns nothing with an empty party, then reads the party structs' species (an Egg counts as its species), `sBox` and the fourteen `sBoxN` copies, matching the player's ID and the OT's first five letters (`NAME_LENGTH_JAPANESE - 2` compares, stopping early at a shared terminator); the Day-Care is not searched. The original's `sBox` is the current box, and its own `sBoxN` slot may still hold an older copy from before the last box change; the port keeps the boxes without stale copies, so a Pokémon only in such a copy is not found. The sages, Suicune, Ho-Oh and Lugia run on ported primitives (`BATTLETYPE_SUICUNE`, `BATTLETYPE_FORCEITEM`). `tests/tin-tower.test.ts` and two playthrough stages cover them. |
|
||
| `game/scripts/slot-machine.ts`, `game/scripts/card-flip.ts` | `engine/games/slot_machine.asm`, `engine/games/card_flip.asm`, `engine/gfx/cgb_layouts.asm` | Both on a realtime tile screen (two frames per tick, buffered buttons). Slots: reel strips, spin rates and scrolling, bias tables with the lucky machine and seven streak, reel 3's Golem and Chansey replay with their sprites, payouts. Card Flip: deck, 12×11 board, cursor sprites and `ChooseCard_HandleJoypad`, paytable. Tilemaps, all 48 cursor positions, selected reel states and whole Golem/Chansey/Egg sequences are checked against the ROM (`tests/reference-game-corner.spec.ts`, which runs `CardFlip_UpdateCursorOAM`, `Slots_UpdateReelPositionAndOAM`/`.SpinReel` and the sprite-animation engine's own Golem/Chansey/Egg routines): the cursor, the reel symbols and the three creatures each anchor an OAM-relative pixel pair 8 (x) and 16 (y) beyond the screen's, as `cursorSprites` reads; `depixel`'s own two arguments end up swapped by `_InitSpriteAnimStruct` (confirmed against the reference emulator), so Golem/Chansey/Egg's X and Y bases were swapped to match, and Golem's own OAM table X-flips its right half. `PlaySpriteAnimations` is compared frame by frame over a three-Golem run and a Chansey round with two eggs (shadow OAM, `wSlotsDelay` and `hSCY`): frames last 8 ticks (the first draw plus 7 decrements); Golem's frames 3 and 4 flip the whole OAM set (offsets become −8−offset); a sprite removed by `DeinitializeSprite` is still drawn that tick, because `UpdateAnimFrame` runs after the animation function; the Golem's roll toggles `hSCY` by ±2, which the tile screen shows with the space tiles `_SlotMachine` filled the BG map with. The rows scrolled in use palette 0, since the attribute map beyond row 18 is not reproduced. No sound. |
|
||
| 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, 300 `CalcMonStats` cases (with and without stat experience, including levels above 100 that reach the 999 cap) and every `CalcExpAtLevel` value for the six growth rates and levels 1–100. Battle unit tests cover win, escape, loss, experience, level-up and move-learning flows, and `tests/playthrough.test.ts` plays the whole opening quest from a new game through `GameWorld`'s inputs, with save/reload after each stage. 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.
|
||
|
||
`tests/reference-random.spec.ts` also executes the unpatched
|
||
`BattleCommand_Critical`, `BattleCommand_CheckHit` and
|
||
`BattleCommand_DamageVariation` routines. The original Link RNG and the native
|
||
random callback start from the same ten-byte stream. Each case compares the
|
||
result, modified accuracy, miss damage and final RNG stream/cursor, covering
|
||
skipped draws and rejection sampling across stream regeneration. Each browser
|
||
runs 2,560 critical cases, 2,914 accuracy cases and 2,149 damage-variation cases.
|
||
Critical coverage includes every random byte at all five reachable stages,
|
||
Focus Energy, Scope Lens and the species-specific items. Accuracy coverage
|
||
includes all stage pairs, Foresight, BrightPowder, perfect accuracy, X Accuracy,
|
||
rain/Thunder, Dream Eater, drain/substitute failure and Jump Kick miss damage.
|
||
Damage coverage includes 0/1's no-draw path, rounding and 16-bit boundaries.
|
||
Mutating each command's threshold or divisor makes its comparison fail.
|
||
|
||
These are isolated command comparisons using controlled Link RNG inputs, not
|
||
a claim of Link battle support or matching the hardware-timed non-Link RNG.
|
||
Protect's text path and the refused Lock-On/Fly/Dig paths are outside these
|
||
fixtures. Capture decision comparisons are described below; whole-battle
|
||
reference comparisons remain open.
|
||
|
||
## Validation
|
||
|
||
```sh
|
||
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 synthetic CI. The separate private CI job requires a verified ROM and PRET build before running these tests. Comparing with rebuilt graphics also needs that build. Manual-save generators stay opt-in, and browser-independent engine traces run only in Chromium. Do not report skipped 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, and later Mom's savings (`momsMoney`, migrated by absence to 0). 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`.
|
||
|
||
Later fields keep, by absence-migrated defaults: `memory`, the saved WRAM bytes that scripts read and write directly, by PRET label (`SAVED_WRAM`); and `unownDex` with `firstUnownSeen` (`wUnownDex`, `wFirstUnownSeen`).
|
||
|
||
## 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. `game/battle/smart-ai.ts` translates every `AI_Smart_EffectHandlers` entry, including its random thresholds, byte score wrapping and the `CheckTypeMatchup` bug: the AI's type checks read the move struct and Foresight flag through `hBattleTurn`, so they can use the player's struct, and Conversion2's write to it persists. `tests/reference-smart-ai.spec.ts` runs the unmodified `AI_Smart` routine in the reference emulator for all 251 moves, comparing scores, Random consumption, `hBattleTurn` and the player's move type. Synthetic scoring vectors cover the other layers.
|
||
|
||
`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: Pursuit on a switching Pokémon, Perish Song and Spikes. Side effects of the type scans on the move structs (overwritten types and animation) are not modelled; the next `ParseEnemyAction`/`ParsePlayerAction` reloads them.
|
||
|
||
`AI_SwitchOrTryItem`'s DontSwitch path runs `AI_TryItem`, including when Mean Look or Wrap blocks switching. Only the trainer's highest-level Pokémon (ties included) may use the two class items. All 13 `AI_Items` entries are ported: Full Restore, Max/Hyper/Super Potion, Potion, Full Heal, X Accuracy, Guard Spec, Dire Hit and the four X stats. The HP, status, first-turn and random gates preserve CONTEXT_USE, ALWAYS_USE and UNKNOWN_USE. A used item consumes the enemy's turn, updates the party, and clears the original move counters and substatuses.
|
||
|
||
The original bugs remain: Full Heal leaves confusion; status healing leaves Nightmare and burn/paralysis stat penalties; X stats see stale miss/failure flags; a declined second-slot item strands the first slot for the rest of the scan. That pointer can also read the base reward as a third item. The port models this extra slot, but not further drift into AI flag bytes; no real two-item trainer's base reward matches an item in this ROM. Battle Tower is still closed. `wEnemyItemState`'s redraw counter is omitted.
|
||
|
||
`tests/trainer-items.test.ts` covers every item and the switch-blocked paths. `tests/reference-trainer-items.spec.ts` compares `AI_TryItem` over 5,632 controlled states, including HP boundaries, stale miss/failure flags, item/party/HP/status/substatus/stat-level state and RNG draw counts. Four more reference cases cover the base-reward collision directly and through no-switch-style, Mean Look and Wrap dispatch. These isolated calls redirect Random and bypass `PrintText`, `PlaySFX`, `AnimateHPBar` and `UpdateEnemyHUD`; they do not verify presentation. `tests/trainer-battle.test.ts` exercises Clair's real Kingdra and class items through `Battle.run()`.
|
||
|
||
`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, Mr. Pokémon's house, Route 31, Violet City, Sprout Tower and Dark Cave's Violet entrance 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, Elm's special call, capture and the catching tutorial. NPCs roam. The PC and boxes are ported. Next: 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.
|
||
|
||
## Capture and the catching tutorial
|
||
|
||
`PokeBallEffect`, `BallMultiplierFunctionTable` (every ball, including
|
||
Kurt's and the Park Ball outside the contest), `GetPokeBallWobble` with `WobbleProbabilities`,
|
||
`UseBallInTrainerBattle` and `BattleMenu_Pack`'s `.UseItem` in
|
||
`engine/items/item_effects.asm`, `engine/battle_anims/pokeball_wobble.asm`
|
||
and `engine/battle/core.asm`. The catch rate is computed with the original
|
||
8- and 16-bit arithmetic and kept bugs (no bonus for BRN/PSN/PAR, overflow
|
||
above 341 maximum HP). Only the random draws are kept from the throw
|
||
animation: one per shake until the ball clicks or the Pokémon breaks free.
|
||
The caught Pokémon follows the second `LoadEnemyMon` (the wild item roll
|
||
is redrawn and then restored), `GeneratePartyMonStats` with `wBattleMode`
|
||
set (wild moves, PP and HP) and `SetCaughtData`. Pokédex registration,
|
||
`NewDexDataText`, `AskGiveNicknameText` and the naming field with
|
||
`InitName`'s default follow. The ball is taken from the selected slot, as
|
||
`RemoveItemFromPocket` does with `wCurItemQuantity`.
|
||
|
||
`Script_catchtutorial` and `CatchTutorial` run the battle as
|
||
`BATTLETYPE_TUTORIAL`: `<PLAYER>` reads "DUDE" from the ROM, `DoBattle`
|
||
skips the send-out, and the auto input's PACK and ball choice go straight
|
||
to `PokeBallEffect`, which always catches and keeps nothing (`.FinishTutorial`).
|
||
Prompts continue on their own. The Dude's back picture, the ball animation,
|
||
capture music and the Pokédex page (`NewPokedexEntry`) are not presented.
|
||
`TX_LOW` text is now read as a line break.
|
||
|
||
Refused: the other ball
|
||
multipliers, Park/Safari Balls, a transformed target and a catch-rate
|
||
division by zero. Pure vectors cover the catch formula; local-ROM tests
|
||
cover capture, every break-free message, Master Ball draws, blank
|
||
nicknames, the blocked trainer ball, full-party rollback, both tutorial
|
||
triggers, declining and the repeat offer, and mid-battle reloads. Browser
|
||
tests cover PACK, catching and naming.
|
||
|
||
`tests/reference-capture.spec.ts` compares the production capture decision
|
||
and shake loop with `PokeBallEffect`'s calculation and the original
|
||
`GetPokeBallWobble`. Each browser runs 3,220 capture cases and 7,424 shake
|
||
cases: every catch-rate byte and supported multiplier, HP scaling and
|
||
wrapping, status bonuses, success-draw equality, every random byte at each
|
||
wobble-table entry, early/later escapes, and the Master Ball/tutorial
|
||
bypasses. Final rates, outcomes, shake counts and random-draw counts match.
|
||
The native decision and shake loop are private helpers called by the actual
|
||
ball-use path; existing capture integration tests still exercise that path.
|
||
|
||
Unlike the battle commands, capture calls hardware `Random`. This fixture
|
||
patches its entry in the private in-page ROM copy to consume supplied bytes,
|
||
enters after the capture UI setup, and inserts a return after the decision.
|
||
The catch arithmetic, ball multipliers and wobble routine are unmodified.
|
||
It does not compare hardware RNG timing, animation, caught-mon reloading,
|
||
party/PC writes, or a whole battle. No real held item has `HELD_CATCH_CHANCE`,
|
||
so that synthetic effect remains covered by pure vectors only. Maximum HP
|
||
342 and 683 are omitted from emulator execution because their divisor is
|
||
zero; synthetic tests verify the native refusal. Deliberate mutations of
|
||
success equality, formula rounding and shake equality fail the comparisons.
|
||
|
||
## Switching, battle items and the stats screen
|
||
|
||
`engine/battle/core.asm`: `BattleMenu_PKMN` and `TryPlayerSwitch` ("is
|
||
already out", "can't be recalled" while wrapped or blocked by Mean Look,
|
||
`CheckIfCurPartyMonIsFitToFight` with its fainted and Egg texts),
|
||
`PlayerSwitch` and `BattleMonEntrance`. The switch runs `ParseEnemyAction`
|
||
first, and `ParsePlayerAction` then skips it, so the random draws happen
|
||
once. `SendOutMonText` and `WithdrawMonText` choose their cheer from the
|
||
enemy's HP as a byte percentage of max HP / 4, including
|
||
`wEnemyHPAtTimeOfPlayerSwitch`. A maximum HP below 4 would divide by zero
|
||
and freeze the original; it is refused. `PursuitSwitch` reads the enemy
|
||
move's effect as `GetMoveEffect` does, and Pursuit is refused.
|
||
|
||
After a faint, `HandlePlayerMonFaint` runs `AskUseNextPokemon` (wild
|
||
battles only; NO tries to run, compared with the first party Pokémon's
|
||
stored Speed) and `ForcePlayerMonChoice`, whose party menu cannot be
|
||
cancelled. A double faint in a trainer battle goes through `DoubleSwitch`,
|
||
and a fainted Pokémon gains no share of the experience. When a trainer
|
||
sends out the next Pokémon, `CheckWhetherToAskSwitch` and `OfferSwitch` run
|
||
Shift mode's offer (the only battle style, since options are not ported);
|
||
switching clears both participant sets as the original does.
|
||
`NewBattleMonStatus` also clears the enemy's wrap count and Mean Look.
|
||
|
||
`engine/items/item_effects.asm` and `pack.asm`: `BattlePack` shows the Items
|
||
and Balls pockets. `CheckItemContext` gives Oak's "This isn't the time"
|
||
text for items with no battle use. `ItemEffects` is read from the ROM and
|
||
dispatches every non-ball effect usable in battle:
|
||
|
||
- Party items (`.BattleField`): `RestoreHPEffect` (`ItemRestoreHP`,
|
||
`HealingHPAmounts`, `RestoreHealth`), `StatusHealingEffect`
|
||
(`UseStatusHealer`, `StatusHealingActions`, `HealStatus`,
|
||
`IsItemUsedOnConfusedMon`), `FullRestoreEffect`, `ReviveEffect` (half or
|
||
full HP; a revived participant shares the experience again through
|
||
`wBattleParticipantsIncludingFainted`), the bitter Energypowder, Energy
|
||
Root, Heal Powder and Revival Herb (`ChangeHappiness` and "It looks
|
||
bitter…"), `BitterBerryEffect` and `RestorePPEffect` (Ether, Max Ether,
|
||
Elixer, Max Elixer, MysteryBerry; `GetMaxPPOfMove`, `BattleRestorePP`, a
|
||
`move` prompt for `MoveSelectionScreen` whose B reopens the party menu).
|
||
- Battle-only items (`.BattleOnly`): `XItemEffect` (`XItemStats`,
|
||
`RaiseStat` with `hBattleTurn` on the player, used even when the stat
|
||
cannot rise, and the X-item happiness gain), X Accuracy, Guard Spec and
|
||
Dire Hit (set once; a second use keeps the item), and `PokeDollEffect`
|
||
(ends a wild battle as a draw; Oak's warning in a trainer battle).
|
||
|
||
"Won't have any effect" and a cancelled menu keep the item and the turn.
|
||
`wItemEffectSucceeded` shares `wBattlePlayerAction`'s byte, so a used item
|
||
takes the turn. `UseItemText` waits for a button. `ChangeHappiness` reads
|
||
`HappinessChanges` and updates the battle Pokémon's copy only when
|
||
`wCurPartyMon` equals the 1-based `wPartyMenuCursor`, as in the original;
|
||
fainting and levelling up use it too, and `LevelUpHappinessMod` gives the
|
||
larger gain on the map where the Pokémon was caught.
|
||
|
||
`CalcPlayerStats` reapplies the paralysis and burn penalties to the side
|
||
named by `hBattleTurn` after a switch of turns, which can be the enemy.
|
||
`hBattleTurn` at the battle menu is modelled: `DoBattle` leaves the
|
||
player's turn, `HandleHealingItems` ends a full turn on the enemy's, and
|
||
the trainer AI reports when `AI_Types`, `AI_Status` or `AIDamageCalc`
|
||
set it. A status heal after a full turn therefore penalises a paralysed
|
||
or burned enemy again, as the original does. Kurt's balls keep their
|
||
documented bugs (Heavy Ball bank, Fast Ball's three bytes, Moon Ball's
|
||
BURN_HEAL compare, Love Ball's same gender). The Heavy Ball is refused for
|
||
Kadabra and Sunflora, whose original weight read runs into VRAM.
|
||
|
||
Special battle types follow `LoadEnemyMon`, `TryToRunAwayFromBattle`,
|
||
`BattleMenu`, `CheckContestBattleOver`, `PokeBallEffect`,
|
||
`BugContest_SetCaughtContestMon` and `BattleEnd_HandleRoamMons`. The
|
||
contest's Park Balls and caught Pokémon (`BugContest`) and a roaming
|
||
Pokémon's struct (`RoamMon`) are passed in and updated by the battle. The
|
||
world keeps the contest in its script state while one runs, and switches
|
||
encounters to `BugCatchingContestBattleScript` and `ContestMons`. It has
|
||
no roaming Pokémon yet.
|
||
|
||
`engine/pokemon/stats_screen.asm`: the stats screen's three pages (HP,
|
||
status, types, experience to the next level; item and moves with PP; ID,
|
||
OT and stats) with the ROM's strings, `TypeNames`, `GetGender` and the
|
||
front picture. It opens from BattleMonMenu (B returns to the party menu,
|
||
as `BattleMenuPKMN_ReturnFromStats` does) and from Bill's PC, where a
|
||
boxed Pokémon shows `CalcTempmonStats`' full HP and no status.
|
||
|
||
Tests: local-ROM battles cover switching, cancelling, the withdrawal texts,
|
||
forced choices (including a fainted pick and a cancelled forced menu), run
|
||
after NO, Shift mode's offer (accepted and declined), a trainer double
|
||
faint, and every item effect above, including the reapplied enemy penalty
|
||
and the caught-location level-up bonus. Trainer AI tests pin its
|
||
`hBattleTurn` reporting. The reference suite runs the original
|
||
`SendOutMonText` (its final jump to `BattleTextbox` patched to `ret` in the
|
||
test's ROM copy), `WithdrawMonText`'s text routine, `RestoreHealth`,
|
||
`RestorePP`, `ChangeHappiness` and `CalcPlayerStats` with both values of
|
||
`hBattleTurn`: 300, 300, 200, 300 and 200 cases. Browser tests switch
|
||
through the party menu, use a Potion from the Items pocket and page
|
||
through the stats screen. `WithdrawMonText` still assumes the leftover
|
||
multiplicand high byte is zero; a whole battle has not been compared with
|
||
the original.
|
||
|
||
## Start menu, party menu and pack
|
||
|
||
`StartMenu.SetUpMenuItems` builds the list from the Pokédex and Pokégear
|
||
engine flags and the party, and `StartMenu.Items` supplies each item's
|
||
string and description. The start menu runs as `StartMenuRun`, a native
|
||
generator on a copy of the script state, so nothing commits before it
|
||
closes and a save meanwhile keeps the previous state. POKéDEX is not
|
||
ported and gives a notice; POKéGEAR, the trainer card and OPTION run inside
|
||
the start menu run.
|
||
|
||
The party menu prints `PartyMenuStrings` for the `PARTYMENUACTION_*` in use.
|
||
`GetMonSubmenuItems` reads `MonMenuOptions`: field moves first, then STATS,
|
||
SWITCH, MOVE, ITEM (MAIL when `MailItems` holds the held item) and CANCEL
|
||
when fewer than `NUM_MONMENU_ITEMS` are listed; the strings come from
|
||
`MonMenuOptionStrings` and move names. `SwitchPartyMons`/`_SwitchPartyMons`
|
||
swap two party entries. `MoveScreenLoop` swaps two moves and their PP; the
|
||
original's flag-dependent write into `wBattleMon` after a swap outside
|
||
battle is not modelled (the next battle reloads that structure). Its left
|
||
and right (`.cycle_left`, `.cycle_right`) arrive as "left" and "right"
|
||
answers of a `menu` effect with `sides`. `GiveTakePartyMonItem`,
|
||
`TryGiveItemToPartymon` (including the swap and the full-pack refund) and
|
||
`TakePartyItem` use `GiveItemToPokemon`/`ReceiveItemFromPokemon` on the
|
||
chosen pack slot. Giving MAIL ends with `GivePartyItem`'s
|
||
`ComposeMailMessage`.
|
||
|
||
The pack's submenu follows `.ItemBallsKey_LoadSubmenu`: `CANT_TOSS`,
|
||
`CANT_SELECT` and the field menu (`CheckItemMenu`, the help byte's high
|
||
nibble) pick one of the six `MenuHeader_*` menus. `TossMenu` confirms the
|
||
quantity and tosses from the chosen slot. `UseItem` dispatches by field menu
|
||
and then `_DoItemEffect` by the `ItemEffects` routine address, with the
|
||
field side of `RestoreHPEffect`, `StatusHealingEffect`, `FullRestoreEffect`,
|
||
`ReviveEffect`/`RevivalHerbEffect`, `EnergypowderEffect`/`EnergyRootEffect`,
|
||
`HealPowderEffect` and `RestorePPEffect` (Ether, Max Ether, Elixer, Max
|
||
Elixer, MysteryBerry and PP Up with `ApplyPPUp`), and since the remaining
|
||
field items increment `UseRepel` (`UseItemText`), `CoinCaseEffect`,
|
||
`BlueCardEffect`, `_SacredAsh` (`CheckAnyFaintedMon`, then the queued
|
||
`SacredAshScript`), `OpenBox` (`DecorationIDs`, then the decoration's
|
||
event flag), `ItemFinder` (the world's `CheckForHiddenItems` picks the
|
||
queued script), `_CardKey`, `_BasementKey` (map and facing tile in
|
||
`wPlayerMapX/Y` coordinates) and `_Squirtbottle`, whose
|
||
`.CheckCanUseSquirtbottle` asks the world's `GetFacingObject` and leaves the
|
||
object in `hLastTalked`, and the rods (`UseRod`, below). Items are used
|
||
from the
|
||
Items pocket slot they were chosen from (`wCurItemQuantity`).
|
||
`ItemActionTextWaitButton` prints `PrintPartyMenuActionText` with the HP
|
||
change. `ChangeHappiness` outside battle does not touch a battle Pokémon.
|
||
`BicycleEffect` runs `BikeFunction`'s checks; a queued script runs once the
|
||
menu closes, and a refusal gives Oak's warning because
|
||
`wFieldMoveSucceeded` and `wItemEffectSucceeded` share a byte.
|
||
|
||
`wBattleMenuCursorPosition`, `wPartyMenuCursor` and `wLastPocket` persist
|
||
between openings in memory and are cleared when a battle ends
|
||
(`CleanUpBattleRAM`). The party cursor is passed to the next battle, whose
|
||
`ChangeHappiness` compares it with `wCurPartyMon`.
|
||
|
||
Tests: `tests/start-menu.test.ts` (local ROM) covers the item list and
|
||
descriptions, the cursor, the submenu with field moves, switching, move
|
||
reordering, giving, swapping and taking items (including a full pack and
|
||
MAIL), every ported field effect, the PP Up step and its maxed-out refusal,
|
||
tossing, the Bicycle outdoors and indoors, unported effects and saving
|
||
while the menu is open. A Chromium/WebKit test opens the menu with Start,
|
||
switches Pokémon, changes pockets and tosses a Potion. These are not
|
||
emulator comparisons.
|
||
|
||
MAIL lives on the `PartyMon` that holds it (`sPartyMail` follows its
|
||
Pokémon, so `_SwitchPartyMons`' mail swap and `RemoveMonFromParty`'s shift
|
||
need nothing more) and in the state's `mailbox` (`sMailboxes`, up to
|
||
`MAILBOX_CAPACITY`). Saves accept a message exactly while MAIL is held, and
|
||
box Pokémon never carry one. The author's "nationality" bytes are not kept.
|
||
`MonMailAction`'s READ, TAKE (`SendMailToPC`, or `ReceiveItemFromPokemon`
|
||
after `MailLoseMessageText`) and QUIT, `MailboxPC`'s READ MAIL, PUT IN PACK
|
||
(`DeleteMailFromPC`) and ATTACH MAIL (`MoveMailFromPCToParty`), and
|
||
`BillsPC_CheckMail_PreventBlackout`'s `PCString_RemoveMail` use the ROM's
|
||
strings. The naming screen's `mail` mode is `_ComposeMailMessage`: the
|
||
first line gets its `<NEXT>` once full, so a second typed line pads it.
|
||
`ReadAnyMail` shows the message and author natively; `MailGFXPointers`'
|
||
stationery and the Portrait MAIL's picture are not drawn. A scripted
|
||
`givepoke` with MAIL (`GivePokeMail`) is refused.
|
||
|
||
`Pack_InterpretJoypad`'s Select runs `SwitchItemsInBag` on the bag
|
||
(`.try_combining_stacks`, `.combine_stacks`, `.above` and `.below`) through
|
||
a `select:` answer of the `pack` effect, which shows the moving entry
|
||
(`wSwitchItem`) and `AskItemMoveText`; the original's CANCEL row is the
|
||
pack's BACK. The cursor now stays on the last row chosen while the pack is
|
||
open.
|
||
|
||
`DoRepelStep` runs in the world where `CountStep` calls it, after
|
||
`CheckSpecialPhoneCall`; the poison, happiness, egg, Day-Care and bike
|
||
steps of `CountStep` are not ported yet. `RepelWoreOffScript` starts with
|
||
the step's committed position, like a coordinate event, and its committed
|
||
save keeps the last Repel step; a wild battle's committed save likewise
|
||
keeps the step's count. `TryWildEncounter`'s `CheckRepelEffect` follows
|
||
`ChooseWildEncounter`. `text_ram` of `wPlayerName`, `wRivalName` and
|
||
`wMomsName` reads the text context's names.
|
||
|
||
`FishFunction` runs in the world (`.TryFish`'s surf and `WATER_TILE` checks,
|
||
`GetFishingGroup` from the map header's `MAP_FISHGROUP`) and queues
|
||
`Script_GotABite`, `Script_NotEvenANibble` or `Script_NotEvenANibble2`;
|
||
`scripts/fish.ts` is `Fish` over `FishGroups` and `TimeFishGroups`
|
||
(`GetFishGroupIndex`'s swarm groups need `wFishingSwarmFlag`, which only
|
||
trainer calls set). The interpreter gained `loademote` (an emote loaded to
|
||
the bubble's tiles is what `show_emote` shows next), `loadmem hBGMapMode`
|
||
(ignored), `Fishing_CheckFacingUp`, `PutTheRodAway` (after the pending
|
||
text) and an accepted `LoadFishingGFX`. `fish_cast_rod` and `fish_got_bite`
|
||
set the actor's `OBJECT_ACTION_FISHING` until `PutTheRodAway`; the renderer
|
||
then draws `FACING_FISH_*` from the player's tiles with `FishingGFX` or
|
||
`KrisFishingGFX` over tiles 2-3, 6-7 and 10-11 and the rod at $fc-$fd,
|
||
all in VRAM bank 1 like the player's sprite (`.InitSprite` sets
|
||
`OAM_BANK1` below tile $80). Facing offsets are now read as signed bytes.
|
||
The battle's `BATTLETYPE_FISH` prints `HookedPokemonAttackedText`.
|
||
`LoadEnemyMon`'s `.Magikarp` filters and `CalcMagikarpLength` (with its
|
||
`.BCLessThanDE` high-byte comparison) replace the Magikarp refusal.
|
||
|
||
Tests: `tests/field-items.test.ts` (local ROM) uses each Repel, walks with
|
||
one through a test-only grass table (weaker Pokémon kept away, a fainted
|
||
lead, an equal level let through, the last step's message and its replay
|
||
after a reload), and covers the Coin Case, the Blue Card, Sacred Ash with
|
||
and without a fainted Pokémon, both boxes, the save migration, the
|
||
Itemfinder at each edge of the screen around Route 30's hidden Potion,
|
||
the keys (a start-menu run with the unopened maps' positions) and the
|
||
Squirtbottle (the interpreter facing a test Sudowoodo on Route 36),
|
||
`Fish` against the Shore tables, a bite, no bite and no fish in New Bark
|
||
Town with the poses and the bubble, the surf and water refusals, and the
|
||
Magikarp DV filters. `tests/start-menu.test.ts` moves and merges items
|
||
with Select (keys included, and none in GIVE's pack) and steps through the
|
||
party on the MOVE screen past an Egg. `tests/mail.test.ts` writes MAIL
|
||
given from the pack and swapped for a Berry, reads it, takes it to a PC
|
||
(and a full MAILBOX) or into the pack (and a full pack), keeps it out of
|
||
the boxes, reads, packs and attaches it from the MAIL BOX, and covers the
|
||
save rules. `tests/reference-parity.spec.ts` compares
|
||
`magikarpLength` with `CalcMagikarpLength` in the reference emulator over
|
||
1,509 DV and ID pairs. Chromium/WebKit tests hook a Magikarp with the
|
||
Old Rod, move a Potion below a Rare Candy with the keyboard's Select, and
|
||
write and read MAIL through the two-line entry and the letter panel; the four fishing directions were checked in close-up
|
||
screenshots, not against the original.
|
||
|
||
## Evolution
|
||
|
||
The battle's experience routine sets a party member's bit in
|
||
`wEvolvableFlags` when it levels up. `ExitBattle` runs `EvolveAfterBattle`
|
||
only when `wBattleResult & $0f` is a win; the world then presents it before
|
||
the script receives the battle result, on the same draft, so the evolution
|
||
commits with the battle. `CleanUpBattleRAM`'s cursor reset follows.
|
||
|
||
`EvolveAfterBattle` reads `EvosAttacksPointers` and walks each Pokémon's
|
||
entries with the original's pointer steps (`.dont_evolve_1/2/3`): trades are
|
||
skipped without Link, a forced evolution (a stone) skips every non-item
|
||
entry by two bytes, items need `wCurItem` and `wForceEvolution`, levels and
|
||
happiness (`HAPPINESS_TO_EVOLVE`, `TR_*` against `wTimeOfDay`) and Tyrogue's
|
||
`EVOLVE_STAT` check the copied Pokémon, and an Everstone blocks all but
|
||
items. The first match evolves; a stopped evolution goes on to the next
|
||
Pokémon. `UpdateSpeciesNameIfNotNicknamed` compares the nickname with the
|
||
old species' name (`wBaseDexNo`, which the animation restores).
|
||
`CalcMonStats` uses the stat experience; HP rises by the maximum HP's
|
||
increase. `LearnLevelMoves` then offers moves at exactly the current level,
|
||
and `SetSeenAndCaughtMon` records the species.
|
||
|
||
`EvolutionAnimation` is presented as a picture schedule in overworld ticks:
|
||
40 ticks on the old picture, then seven rounds of a B-checking wait (7 … 1
|
||
ticks) and that many flashes (two ticks each way). The waits and flashes
|
||
are approximations of the original frame timing; B is accepted only during
|
||
the waits and never when `wForceEvolution` is set. No music, cry, palette
|
||
or `AnimateFrontpic` is presented.
|
||
|
||
`LearnMove` outside battle: the first empty slot, otherwise `ForgetMove`
|
||
(yes/no, then a move list; `IsHMMove` refuses HM moves), `Text_1_2_and_Poof`
|
||
with its `text_asm` continuation, or "Stop learning" (NO asks again) and
|
||
"did not learn".
|
||
|
||
PACK items: `RareCandyEffect` (`CalcExpAtLevel` for the new level, the HP
|
||
rise that also revives a fainted Pokémon, `LevelUpHappinessMod` against the
|
||
caught landmark, `PARTYMENUTEXT_LEVEL_UP`, a `PrintTempMonStats` page,
|
||
`LearnLevelMoves` and an unforced `EvolvePokemon`), `EvoStoneEffect`
|
||
(`PARTYMENUACTION_EVO_STONE`'s ABLE/NOT ABLE labels copy ten bytes and step
|
||
three at a time, as the original does; bytes past the copy read as the end
|
||
here, where the original reads stale RAM) and `VitaminEffect`
|
||
(`StatExpItemPointerOffsets`, the high byte limit of 100, `StatStrings`,
|
||
`ItemStatRoseText` and `HAPPINESS_USEDITEM`).
|
||
|
||
Tests: `tests/start-menu.test.ts` covers the Rare Candy level-up, stat page,
|
||
HM refusal, forgetting and declining a move, the revived fainted Pokémon,
|
||
evolution with and without B, a stone (ABLE labels, no effect on another
|
||
species or with an Everstone, B ignored, a kept nickname) and a vitamin up
|
||
to its limit. `tests/battle-world.test.ts` wins a wild battle whose level-up
|
||
evolves Cyndaquil, and checks that a non-participant that could evolve does
|
||
not. A Chromium/WebKit test evolves through a Rare Candy. No emulator
|
||
comparison of these routines yet.
|
||
|
||
## Field moves from the menu, SEL and MOVE PKMN W/O MAIL
|
||
|
||
`PokemonActionSubmenu` passes a field move to the world, which evaluates
|
||
the menu function's checks on the menu's draft for the chosen party slot:
|
||
`CheckBadge` with `BadgeRequiredText`, `CheckMapForSomethingToCut`
|
||
(`CheckCutCollision` and `CutTreeBlockPointers`), `SurfFunction` (Fog
|
||
Badge, forced cycling, already surfing, water permission, `CheckDirection`
|
||
and `CheckFacingObject`), `StrengthFunction`, `FlashFunction`
|
||
(`SpecialAerodactylChamber` by map attributes, which sets
|
||
`EVENT_WALL_OPENED_IN_AERODACTYL_CHAMBER`, or `DARKNESS_PALSET`),
|
||
`TryWhirlpoolMenu`, `CheckMapCanWaterfall`, `FlyFunction` (the Storm
|
||
Badge, an outdoor map, then the Fly map), `EscapeRopeOrDig` (a cave or
|
||
dungeon with a `wDigWarpNumber`) and `TeleportFunction` (an outdoor map and
|
||
`IsSpawnPoint` on `wLastSpawnMapGroup`), and the checks of Headbutt and Rock
|
||
Smash. A success queues `Script_CutFromMenu`,
|
||
`SurfFromMenuScript`, `Script_StrengthFromMenu`, `Script_UseFlash`,
|
||
`Script_WhirlpoolFromMenu`, `Script_WaterfallFromMenu` or
|
||
`SweetScentFromMenu.SweetScent` with the field-move context the interpreter
|
||
already reads (`GetPartyNickname`, the replacement block, the surfing
|
||
state). New interpreter support: `callasm BlindingFlash` (sets the Flash
|
||
flag), `callasm SweetScentEncounter` (the encounter the world drew with
|
||
`CanEncounterWildMon`, `GetMapEncounterRate` and `ChooseWildEncounter`, no
|
||
rate roll) and `UseFlashTextScript`'s sound-only `text_asm`.
|
||
`FlyFunction.FlyScript`, `TeleportFunction.TeleportScript` and
|
||
`EscapeRopeOrDig.UsedDigScript` run with the chosen `wDefaultSpawnpoint` or
|
||
the dig warp's position. `newloadmap` handles `MAPSETUP_WARP`,
|
||
`MAPSETUP_TELEPORT` and `MAPSETUP_FLY` (`EnterMapSpawnPoint` from
|
||
`SpawnPoints`) and `MAPSETUP_DOOR` (`EnterMapWarp` to the warp Dig copies
|
||
into `wNextWarp`; the player keeps facing unless `CheckWarpFacingDown`),
|
||
then stops the script, which resumes on the new map. `EnterMapWarp`'s
|
||
`SaveDigWarp` records any warp from an outdoor map into an indoor one
|
||
(`CheckIndoorMap`), except from Mount Moon Square and Tin Tower's roof; saves
|
||
keep it as `digWarp`. `FlyFromAnim`, `FlyToAnim`, `HideSprites`,
|
||
`SkipUpdateMapSprites`, `.ReturnFromFly` and `WaitSFX` are accepted, the Fly
|
||
animations as timed `fly` effects (65 and 33 ticks). The Escape Rope
|
||
(`EscapeRopeEffect`) runs `EscapeRopeOrDig` from the pack, opening the Kabuto
|
||
chamber's wall there (`SpecialKabutoChamber`) and using up one rope; a
|
||
failure prints nothing before Oak's warning. `JumpRoamMons` (no roaming
|
||
Pokémon yet) and the fades are not modelled. A destination map that is not
|
||
ported gives a notice. `TreeMonEncounter` and `RockMonEncounter` run since
|
||
the Headbutt and Rock Smash increment.
|
||
`Softboiled_MilkDrinkFunction` runs in the menu with `GetOneFifthMaxHP`,
|
||
`RemoveHP` and `RestoreHealth`, refusing the user itself and Pokémon that
|
||
fainted or are at full HP.
|
||
|
||
`RegisterItem` stores `wWhichRegisteredItem` (pocket in bits 6-7, 1-based
|
||
slot in bits 0-5) and `wRegisteredItem`, now saved as `registeredItem`.
|
||
`SelectMenu` runs as a start-menu run: `CheckRegisteredItem` (items by slot,
|
||
key items anywhere in their pocket, balls never because `.CheckBall`'s
|
||
carry test is inverted, TMs never; a miss clears the registration) and
|
||
`UseRegisteredItem` by field menu, with `wUsingItemWithSelect` choosing
|
||
`Script_GetOnBike_Register`/`Script_GetOffBike_Register`. `MayRegisterItemText`
|
||
and `ItemsOakWarningText` come from the ROM.
|
||
|
||
`BillsPC_MovePKMNMenu`: `IsAnyMonHoldingMail`, then
|
||
`StartMoveMonWOMail_SaveGame`'s question (the native session saves by
|
||
itself, so no save messages follow). `_MovePKMNWithoutMail` treats the party
|
||
as box 0; lists are presented per box. `BillsPC_CheckMail_PreventBlackout`,
|
||
`BillsPC_CheckSpaceInDestination` and `MovePKMNWithoutMail_InsertMon` with
|
||
`.CheckTrivialMove`, `CalcBufferMonStats` for a boxed Pokémon joining the
|
||
party, and `InsertPokemonIntoBox`'s `RestorePPOfDepositedPokemon`.
|
||
|
||
Tests: `tests/overworld-rom.test.ts` uses Surf, Cut, Strength, Flash, Sweet
|
||
Scent and Rock Smash from the menu (badge refusals, failure texts, the dark
|
||
map and the encounter); `tests/start-menu.test.ts` covers Softboiled, SEL,
|
||
Select with and without a registration, indoors, and a registration that
|
||
left its slot; `tests/pc.test.ts` moves Pokémon party to box, box to party,
|
||
within the party and within a full box, and refuses the last Pokémon and a
|
||
full destination. Browser tests register the Bicycle and ride it with
|
||
Select from the keyboard and the on-screen button.
|
||
|
||
### The Pokégear, phone calls and Mom's savings
|
||
|
||
`PokeGear` runs from the start menu as a generator (`Pokegear`) that yields
|
||
each card as a `pokegear` effect and is answered with the button pressed, so
|
||
the original's joypad rules (`PokegearClock_Joypad`,
|
||
`PokegearMap_ContinueMap`, `PokegearPhone_Joypad`, `PokegearRadio_Joypad`
|
||
and the contact submenu's loop) stay in the rules layer. The world routes A,
|
||
B, the directions, Start and Select to it and refreshes the clock card from
|
||
the game clock every tick, as `Pokegear_UpdateClock` runs every frame.
|
||
Texts, menu strings, caller names and the AM/PM strings come from the ROM
|
||
(`POKEGEAR` profile table, verified by `check:reference`). The map header's
|
||
palette byte's high nybble is decoded as `GetMapPhoneService`.
|
||
|
||
A call runs the callee script from `PhoneContacts` (or
|
||
`PhoneOutOfAreaScript`/`PhoneScript_JustTalkToThem`) in the event
|
||
interpreter with the world's script host, on the start menu's draft; its
|
||
state is adopted when it ends. The callee's last text waits for A or B, as
|
||
`PokegearPhone_FinishPhoneCall` does, then `HangUp` shows "Click!" (20
|
||
frames) and the boops (120 frames). The two "……" rings last 12 ticks each,
|
||
an estimate from `Sfx_Call`'s notes; no sound is played.
|
||
|
||
Interpreter additions: `random` (`Script_random`'s rejection of draws past
|
||
the largest multiple), `givemoney`, `takemoney` (an account that is short is left at
|
||
zero, like `TakeMoney`), `checkmoney` (`HAVE_MORE`/`HAVE_AMOUNT`/`HAVE_LESS`) and
|
||
`getmoney` on `YOUR_MONEY` or `MOMS_MONEY`, `getcurlandmarkname` and
|
||
`getlandmarkname` (`GetLandmarkName`; `<BSP>` prints as a space). The money
|
||
and landmark macros put the account or landmark before the string buffer.
|
||
|
||
`WinTrainerBattle.give_money` now splits the reward: the reward is added
|
||
four times, to Mom for as many quarters as `wMomSavingMoney`'s mask says
|
||
(SOME|HALF counts as four) unless `.CheckMaxedOutMomMoney` finds her
|
||
account full, and to the wallet otherwise; the text is `SentSomeToMomText`,
|
||
`SentHalfToMomText` or `SentAllToMomText`. Only `ENGINE_MOM_SAVING_MONEY`
|
||
(the SOME bit) is reachable: nothing in the ROM sets the HALF and ALL bits.
|
||
After a trainer battle, `Script_reloadmapafterbattle` calls
|
||
`MomTriesToBuySomething` (see Mom's bank below).
|
||
|
||
Tests: `tests/pokegear.test.ts` (local ROM) covers the cards and their
|
||
switching, the live clock, the map cursor's wrapping, phone scrolling over
|
||
ten slots, the contact submenu, Elm's call, "Just go talk to that person!",
|
||
Mom's lecture and balance call with commits after the menu closes, no
|
||
service and the save migration. `tests/trainer-battle.test.ts` and
|
||
`tests/trainer-world.test.ts` cover Mom's quarter, her full account and the
|
||
purchase refusal. A browser test drives the cards and a call to Elm from the
|
||
keyboard and the on-screen D-pad.
|
||
|
||
### Mom's bank and her purchases
|
||
|
||
`BankOfMom` (`scripts/mom.ts`) follows the ROM's jumptable: the yes/no
|
||
questions and the GET/SAVE/CHANGE/CANCEL menu (strings from
|
||
`BankOfMom_MenuHeader`) are ordinary effects, and the deposit/withdraw box is
|
||
a `mombank` effect answered with the button pressed, like the Pokégear's
|
||
cards, so `Mom_WithdrawDepositMenuJoypad`'s rules stay in the rules layer.
|
||
`DigitQuantities` is read from the ROM. Texts, strings and the routine come
|
||
from the `MOM` profile table, verified by `check:reference`. Refusal texts
|
||
end in `prompt` and the routine returns to the same jumptable entry, which
|
||
prints its question again and resets the amount; natively the refusal is a
|
||
page before the next box. The last text is left for the script's
|
||
`waitbutton`.
|
||
|
||
`DSTChecks` reads `hHours` from the game clock. `SetClockForward` and
|
||
`SetClockBack` move `wStartHour` with a carry into `wStartDay`; the native
|
||
clock keeps `minuteOffset` within ±720 minutes, so `shiftClockHour` wraps it
|
||
and moves `weekdayOffset` the other way, which keeps the weekday and the
|
||
daily reset day unchanged.
|
||
|
||
`MomTriesToBuySomething` runs at `Script_reloadmapafterbattle` after a
|
||
trainer battle. `CheckBalance_MomItem2` compares her savings with the next
|
||
MomItems_2 trigger; failing that, `.check_have_2300` adds ¥2300 to
|
||
`wMomItemTriggerBalance` while it is below her savings, and only an exact
|
||
match buys a `RandomRange(5)` MomItems_1 entry (and steps once more). The
|
||
trigger balance keeps those increments even when nothing is bought, as in
|
||
the original. `Mom_GiveItemOrDoll` puts an item in the PC (a full PC cancels
|
||
the purchase after the steps) or sets the doll's decoration event. The ROM's
|
||
`.Script` memscript runs `.ASMFunction` (the cost and `wWhichMomItem`) and
|
||
then `Script_ReceivePhoneCall` with `Mom_GetScriptPointer`'s item or doll
|
||
script. Natively `.ASMFunction`'s changes are made when the call is queued
|
||
and the queued script is `Script_ReceivePhoneCall` itself with that caller
|
||
script; the call starts at the next `PlayerEvents`, so nothing can observe
|
||
the difference. `wMapReentryScriptQueueFlag`'s early return is the world's
|
||
rule that keeps an already queued script.
|
||
|
||
Tests: `tests/mom.test.ts` (local ROM; the clock shift runs without it)
|
||
covers the first visit, declining, deposits and withdrawals with each
|
||
refusal, the digit cursor and step limits, B and zero-amount cancels,
|
||
CHANGE, the Daylight Saving Time branches and the near-midnight refusal.
|
||
`tests/trainer-world.test.ts` covers a MomItems_2 purchase and its call, an
|
||
exact ¥2300 step, a step without a purchase, a doll, a full PC and the save
|
||
migration. A browser test deposits through the keyboard and the on-screen
|
||
buttons and checks the backup.
|