Finish the opening milestone's switching, items and acceptance run

- Decode evolution lists from the ROM; all 251 match the pinned PRET source.
- Compare CalcMonStats and CalcExpAtLevel against the original routines in
  the reference emulator.
- Add a continuous opening playthrough from the intro to the catching
  tutorial, reloading a JSON save after every stage.
- Port player switching in battle: the PKMN menu, forced choices after a
  faint, Shift mode's offer and double faints, with the original texts.
- Port HP and status healing items in battle through the ROM's ItemEffects,
  HealingHPAmounts and StatusHealingActions tables.
- Add the battle party menu and the Items pocket to the UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-09-30 11:04:11 +02:00
1 parent eb03a20d8f
commit f8e0f9c65a
20 files changed
+1902 -96

No files matched your search

+53 -4
View File
@@ -32,7 +32,7 @@ These paths refer to the pinned [PRET source](https://github.com/pret/pokecrysta
| 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), item storage, the empty mail box and Oak's rating. Menu strings and texts come from the ROM; lists are native. Moving Pokémon, the stats screen and printing are not ported. |
| `game/rom/pokemon.ts` | `data/pokemon/base_stats`, `evos_attacks.asm`, `data/moves/moves.asm`, `data/items/attributes.asm`, `engine/gfx/load_pics.asm` | Species, move and item names, base data, learnsets, move PP, item pockets, growth-rate exp and front pictures (`FixPicBank`, `PadFrontpic`). Starter pictures match rebuilt bytes. |
| `game/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. |
@@ -46,7 +46,7 @@ The opening scenes currently run end to end from the ROM: Mom's Pokégear, weekd
`tests/reference-parity.spec.ts` loads the user's ROM into the reference emulator and places a small original test trampoline in emulated WRAM. It calls the ROM's `FarDecompress` routine at `$0b40`, then sets a return sentinel. Every output byte is compared with the native decoder for six supported map tilesets. This executes the original routine rather than comparing two copies of our code.
The same suite compares 400 damage cases and 400 type-effectiveness cases per browser against the original battle routines. Battle unit tests cover win, escape, loss, experience, level-up and move-learning flows. This establishes parity only for the compared routines and inputs. It does not prove complete gameplay, rendering, battles, audio, timing or physical-device parity. Extend controlled comparisons as each subsystem is translated.
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.
## Validation
@@ -80,7 +80,7 @@ Native saves are version 3. Version 2 added scene values by owning map, phone co
`home/trainers.asm` and `engine/events/trainer_scripts.asm`: talking to an `OBJECTTYPE_TRAINER` object runs `TalkToTrainerScript` with the object's header copied as `wTempTrainer`; `_CheckTrainerBattle` runs `SeenByTrainerScript` with `wSeenTrainerDistance`. The world checks sight in `PlayerEvents` order: before warps, coordinate events and encounters at the end of a step (a seen player's tile events are then skipped, as `DisableEvents` does), and on idle ticks before scene scripts and input. Walls and objects do not block sight. `TrainerWalkToPlayer`/`ComputePathToWalkToPlayer` build the raw `wMovementBuffer` bytes, which `applymovementlasttalked` decodes with the normal movement reader. `wRunningTrainerBattleScript` lives in the script run: zero at the start, set by `loadmem` after the battle so `endifjustbattled` ends the after-script. Trainer commands without a map trainer are refused. `MomTriesToBuySomething` after scripted battles does nothing while Mom's savings (not ported) are zero.
`engine/battle/core.asm` and `engine/battle/ai/items.asm`, `switch.asm`: when a trainer's Pokémon faints and others remain, `UpdateBattleStateAndExperienceAfterEnemyFaint` zeroes its party HP and runs `NewEnemyMonStatus`; `EnemyPartyMonEntrance` then sends out the Pokémon chosen by `FindMonInOTPartyToSwitchIntoBattle`. When every candidate is threatened it draws `BattleRandom & 7`, which tests pin. `AI_SwitchOrTryItem` runs before either side moves: `CheckAbleToSwitch`, the Often/Rarely/Sometimes thresholds, `AI_TrySwitch` and `AI_Switch` (withdraw text, HP and status back to the party). `CheckTypeMatchup` ignores the type its switch-AI callers load and uses `BATTLE_VARS_MOVE_TYPE`, so every such matchup uses the enemy's chosen move type; `FindEnemyMonsWithASuperEffectiveMove` never resets its result between Pokémon; `FindEnemyMonsWithAtLeastQuarterMaxHP` rotates HP bits instead of multiplying. All three bugs are kept. `UsedMoveText` records `wPlayerUsedMoves`. Refused: the Shift-mode offer to switch (`OfferSwitch`), `AskUseNextPokemon` with other Pokémon left, Pursuit on a switching Pokémon, Perish Song, Spikes, and trainer items. Side effects of the type scans on the move structs (overwritten types and animation) are not modelled; the next `ParseEnemyAction`/`ParsePlayerAction` reloads them.
`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.
@@ -147,7 +147,7 @@ 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: `SendMonIntoBox` (a catch with a full party), the other ball
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
@@ -155,3 +155,52 @@ 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. No emulator comparison of the
capture routines has been run.
## Switching and healing items in battle
`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:
`RestoreHPEffect` (`ItemRestoreHP`, `HealingHPAmounts`, `RestoreHealth`) and
`StatusHealingEffect` (`UseStatusHealer`, `StatusHealingActions`,
`HealStatus`, `IsItemUsedOnConfusedMon`) run with `UseItem_SelectMon`, the
party menu action texts and `UseDisposableItem`. "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.
`CalcPlayerStats` reapplies the paralysis and burn penalties to the side
named by `hBattleTurn` after a switch of turns, which can be the enemy.
Because `hBattleTurn` at the battle menu is not modelled everywhere, a
status heal that would reapply a penalty to a paralysed or burned enemy is
refused. Other item effects (X items, Poké Doll, Full Restore, revives,
Ether) are refused.
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, Potion, Max Potion, a Berry on a benched Pokémon, no-effect cases,
Antidote and PSNCUREBERRY, Parlyz Heal restoring Speed, Oak's warning and a
refused X Attack. Browser tests switch through the party menu and its
SWITCH/STATS/CANCEL submenu and use a Potion from the Items pocket. The
stats screen is not ported. No emulator comparison of these routines has
been run.