The pack's TM/HM pocket lists TMs and HMs with their moves, gives TMs and teaches either through the ABLE/NOT ABLE party menu, the compatibility and "already knows" checks and LearnMove; a TM is used up and makes the Pokémon happier. Eggs show no level or HP in the party menus, get the Egg stats page with its hatching hint, and are skipped by HealParty. Bug Catcher Wade's number registers (VAR_CALLERID and RandomPhoneMon). The playthrough now runs through Falkner's ZEPHYRBADGE, the Togepi Egg, Kyle's Onix, Flash in Dark Cave, Wade's number and Earl's blackboard. With MAKE_TEST_SAVES it writes a Violet City backup for the manual checks. Validated with 610 unit tests, 157 browser tests (including all 256 badge bytes against the original BadgeStatBoosts), native/reference builds and reference/vendor/release checks. Egg hatching (#29) and trainer calls (#30) remain on their cards. Closes #31 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
53 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. 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/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. |
| 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
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, 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.
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, 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, 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 throughwBattleParticipantsIncludingFainted), the bitter Energypowder, Energy Root, Heal Powder and Revival Herb (ChangeHappinessand "It looks bitter…"),BitterBerryEffectandRestorePPEffect(Ether, Max Ether, Elixer, Max Elixer, MysteryBerry;GetMaxPPOfMove,BattleRestorePP, amoveprompt forMoveSelectionScreenwhose B reopens the party menu). - Battle-only items (
.BattleOnly):XItemEffect(XItemStats,RaiseStatwithhBattleTurnon 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), andPokeDollEffect(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 (unchanged when short, 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.