Files
Crystal/docs/SESSION_HANDOFF.md
T
Sébastien QUEROLandClaude Opus 5.5 73022b1505
Checks / web (push) Canceled after 0s
Add the TM/HM pocket, Egg screens and Violet's remaining scripts
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>
2026-10-03 16:49:02 +02:00

1345 lines
74 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Recovered Claude session
Latest continuation: see **Battle effects, held items and obedience: 2026-10-03**
at the end of this file. Earlier sections are historical snapshots.
Recovered on 2026-09-23 from the local Claude Code transcript
`~/.claude/projects/-Users-kawa-Crystal/4ac88f30-29e6-4fd5-a4b2-b6a318dd83b3.jsonl`.
The session stopped at its usage limit at 09:54 UTC, during research, without a final handoff.
## Resumed increment: completed 2026-09-23
The movement increment below is complete. The remaining sections preserve the
original recovery context.
- Added directional two-tile ledge jumps with the existing arc/shadow animation.
Unsupported terrain, invalid destinations and occupied landings remain refused.
- Corrected directional mats to wait for exit input. Warp arrivals keep their
actual coordinates and appropriate facing; doors, stairs and caves animate a
downward exit. Removing the old inward teleport also corrected Elm's scripted
introduction position.
- Mid-walk, mid-jump and mid-connection saves retain the source position. Landing
scenes rewind to the actual source of a jump, rather than the intervening wall.
- Restored compatibility with early version-3 saves lacking the money field.
- Replaced the scratch diagnostic with synthetic movement regressions and
optional-ROM tests covering five Route 29 ledge types. Route 29 is enabled only
inside that test fixture, not in production.
- Fixed the battle test suite's eager ROM read so missing-ROM runs really skip
private tests. Updated browser stair inputs and added an exit-mat save/reload
flow. Updated the roadmap and architecture to distinguish the existing battle
code from its unfinished integration.
Final checks: 75 unit tests passed with the local ROM; without it, 55 passed and
20 private tests skipped. All 24 Chromium/WebKit browser tests passed. Native and
reference builds, release isolation, pinned-reference checks, vendor checks and
`git diff --check` passed. No commit or push was made.
Next: validate the overworld-to-battle browser flow and complete Route 29's
remaining script, capture/tutorial and item interactions before enabling the
route. Player turn timing also remains a documented comparison gap.
## Scope and current state
The original request was to read ARCHITECTURE.md, PLAN.md and PRESENTATION_PLAN.md
and continue the native port. The working tree contains substantial uncommitted
work from multiple increments; preserve it when continuing.
- Opening scenes, scripted movement, starter selection, naming, the aide's Potion
and version 3 saves are implemented and documented.
- Native battle modules, ROM tables, damage/type parity tests, encounter handling,
battle UI and whiteout handling have been added. These later changes are not
fully reflected in the architecture and roadmap documents.
- Only the original six maps are enabled in `NATIVE_MAPS`. Route 29 (`24:3`)
remains disabled. Passing battle unit tests does not establish a playable
Route 29 encounter or the full opening quest.
## Exact stopping point
Claude was preparing to revise player movement in `src/game/world/world.ts`.
Its last investigation concerned spawn facing after warps. It had not implemented
the planned ledge/warp revision.
The pinned local PRET checkout contains the relevant routines:
- `engine/overworld/player_movement.asm`: `.TryStep`, `.TryJump`,
`.CheckLandPerms`, `.CheckWarp` and the direction tables.
- `engine/overworld/tile_events.asm`: `CheckWarpCollision` and
`CheckDirectionalWarp`.
- `engine/overworld/player_object.asm`: `SpawnPlayer`.
- `engine/overworld/map_objects.asm`: movement/turn timing.
Research found that ledge tops have land permission; the jump check uses the
current tile and facing after an ordinary step fails. Doors use collision `$71`,
downward warp mats `$70`, and stairs `$7a`. Directional warp mats require their
designated input rather than an immediate warp simply upon entry. Verify these
rules against the local routines before changing the existing movement flow.
`tests/zz-scratch.test.ts` is Claude's unfinished diagnostic: it logs local-ROM
warp and ledge collisions, contains no assertions and unconditionally requires
the private ROM. Replace it with meaningful optional-ROM or synthetic tests
before treating the suite as ready for public CI.
## Validation after recovery
Run against the current working tree with the local ROM and reference checkout:
- `npm test`: 45 passed, including the assertion-free scratch diagnostic.
- `npm run build`: passed.
- `npm run test:e2e`: 22 passed across Chromium and WebKit, including native
damage/type comparisons against original routines in the reference emulator.
- `npm run check:reference`: ROM, pinned rebuild and profile addresses match.
- `npm run check:release`: native output excludes emulator and game assets.
- `npm run check:vendor`: passed.
These checks do not establish complete battle behavior, browser battle UI flows,
Route 29 progression or physical-device compatibility. No gameplay source was
changed during session recovery.
## Original next-increment notes (movement now completed)
Finish ledge and directional-warp movement with focused regression tests,
including interrupted movement/save behavior and preservation of the existing
opening scenes. Audit Route 29 callbacks, scripts and unsupported destinations
before enabling it. Then test wild encounters and battle controls through the
browser, including win, escape, loss and save/reload, and reconcile the roadmap
with the verified behavior.
## Battle integration increment: 2026-09-23 (resumed from Codex)
Codex committed and pushed `c08d250`, then stopped mid-increment with the
keyboard/D-pad battle test failing. Claude Code completed it:
- Browser battle flows run through the real UI via `tests/battle-fixture.ts`
(encounter selection only; production maps stay gated): win with level-up and
Smokescreen, escape, whiteout, keyboard/D-pad menus, mid-battle backup restore
and unsupported-effect rollback. `tests/battle-world.test.ts` covers the same
at world level, plus pending stat experience and whiteout callback rollback.
- Saves accept stored stats between the no-stat-exp and full-stat-exp values,
since `GiveExperiencePoints` awards stat experience without `CalcMonStats`.
- A script warp (e.g. whiteout) validates its destination and commits healing,
money loss and destination callbacks together; aborts restore state and map.
- Keyboard and on-screen D-pad navigate battle menus (`battle-selected`); A
continues messages, B leaves the move list.
- Fixed a phone-width layout bug: a long notice sat in the Play button's auto
grid column and crushed the status text, pushing the game panel off-screen.
Below 720px the notice now has its own row.
- Test fixes: the navigation test now presses A through the intro message, and
uses a real pointer click on the D-pad (synthetic pointer events cannot be
captured).
Final checks: 82 unit tests with the local ROM (55 passed, 27 skipped without
it); 36 Chromium/WebKit browser tests; native and reference builds; release,
vendor and reference checks; `git diff --check`. Not committed.
Next: Route 29 (`24:3`) scripts, catching tutorial and items before enabling it;
then capture, switching and PACK in battle.
## Route 29 and Cherrygrove increment: 2026-09-23
Enabled Route 29, its Route 46 gate, Cherrygrove City and its five buildings.
Item balls, the Pokémon Center nurse and the Poké Mart were each ported by a
Sonnet sub-agent in an isolated worktree, reviewed against the disassembly and
merged; the rest was done directly.
- Fruit trees run `FruitTreeScript`; a native `CheckDailyResetTimer` clears
the daily engine flags once per game day. `VAR_BOXSPACE` reads an empty box.
- Item balls run `FindItemInBallScript` with a native `TryReceiveItem`.
- The nurse runs `PokecenterNurseScript`; `HealParty` now applies
`ComputeMaxPP`'s PP Up clamp. `EnterMapWarp`'s spawn recording makes
whiteouts return to the last Pokémon Center entered.
- Standard marts run natively (`StandardMart`/`BuyMenu`/`SellMenu`).
- Battle command `recoil` (Struggle) was the only refusal found by a local
stress run of 360 Route 29 battles across starters and times of day.
- Save format 3 gained `fruitTrees`, `dailyReset` and `lastSpawnMap`
(migrated by absence).
- The phone-width status bar layout and parity test map counts were fixed.
Still refused: `catchtutorial` and Cherrygrove's rival scene (both after the
Mystery Egg), Route 30/46. NPCs do not roam yet.
Next: NPC roaming movement (`map_objects.asm` random walk/spin, radius and
visibility rules, deterministic RNG in tests), trainer sight lines and trainer
battles, Route 30 and Mr. Pokémon, capture and the catching tutorial.
## Trainer AI continuation: 2026-09-24
The trainer foundation was subsequently committed through `3e92936`. The next
AI increment translates Opportunist, Aggressive and Risky scoring, including
the ROM's stall/reckless/risky tables and Aggressive/Risky's deterministic
pre-variation damage estimate. Smart is now the only refused move-scoring layer,
so the rival remains gated. Continue with Smart before the rival, or with
trainer sight lines and normal trainer-object scripts for the already-supported
Youngster and Bug Catcher classes on Route 30.
## AI layer review: 2026-09-24 (resumed from Codex)
Claude Code checked Codex's uncommitted Opportunist, Aggressive and Risky layers
against `engine/battle/ai/scoring.asm`. The HP thresholds, `AI_50_50` (≥128),
`79 percent - 1` (200), last-wins ties, the power < 2 and reckless exclusions,
the strict-borrow KO test and the constant-damage paths all match.
One fix: `AIDamageCalc` goes through `EnemyAttackDamage` and
`BattleCommand_DamageCalc`, and both read `wCriticalHit`. A flag left over from
an earlier move therefore selects unboosted stats (ignoring screens) and
doubles the estimate. `enemyAIDamage` now reads the native `criticalHit`
instead of hard-coding `false`. It also reuses `truncate`. A local-ROM test
fixes the estimate at 4, 3 (Reflect) and 6 (leftover critical).
Known side effects of `AIDamageCalc` that are not modelled: it overwrites
`wCurDamage`, `wTypeModifier`, `wAttackMissed` and `hBattleTurn`. `CheckTurn`
resets the modifier and miss flag before each move. Whether the `wCurDamage`
overwrite can reach Counter/Mirror Coat still needs checking before those
effects are ported.
Final checks: 147 unit tests with the local ROM; 42 Chromium/WebKit browser
tests; native and reference builds; reference, release and vendor checks;
`git diff --check`. Not committed.
Next (unchanged): Smart AI before the rival, or trainer sight lines and
normal trainer-object scripts for Route 30's Youngster and Bug Catcher.
## Route 30 trainers: 2026-09-24
Chose option 2 from the review above: trainer sight lines and trainer-object
scripts. Both Route 30 trainers have two Pokémon, so the increment also ports
the trainer's send-out and switch AI. Route 30 and its Berry house are now
enabled in production.
- World: `_CheckTrainerBattle` at step end (before warps, coordinate events and
encounters) and on idle ticks; `TalkToTrainer` for `OBJECTTYPE_TRAINER`;
`BGEVENT_ITEM` hidden items. A refused sighting is not restarted until the
player moves (native safeguard against a rollback loop).
- Interpreter: `loadtemptrainer`, `trainertext`, `trainerflagaction`,
`scripttalkafter`, `endifjustbattled`, `checkjustbattled`, `writeobjectxy`,
`encountermusic`, `loadmem wRunningTrainerBattleScript`, `readmem
wHiddenItemID`, and callasm `TrainerWalkToPlayer`/`SetMemEvent`.
`applymovementlasttalked wMovementBuffer` decodes the native buffer.
- Battle: multi-Pokémon trainer parties (`HandleEnemyMonFaint`,
`EnemyPartyMonEntrance`, `FindMonInOTPartyToSwitchIntoBattle` with its
`BattleRandom` fallback), `AI_SwitchOrTryItem`/`CheckAbleToSwitch`/`AI_Switch`
with the original type-matchup, `e`-carry and quarter-HP bugs, and
`wPlayerUsedMoves`. `BattleTrainer` now requires `switchFlags`.
- Stress (local, not committed): 1,080 Route 30 battles; the only refusal was
Chikorita's Reflect at level 12 (`screen`), a pre-existing gap.
Final checks: 160 unit tests with the local ROM; 44 Chromium/WebKit browser
tests including the Route 30 sighting through the UI; native and reference
builds; reference, release and vendor checks; `git diff --check`. Not committed.
Next: Mr. Pokémon's house (Oak, Pokédex, Mystery Egg) and the return to Elm;
Reflect/Light Screen; then Smart AI before the rival. Joey (after the Egg)
needs `askforphonenumber` and the phone standard scripts.
## Reflect and Light Screen: 2026-09-24
Completed the battle gap found in the Route 30 stress run. `screen` now runs
both moves from ROM effect scripts. Independent five-turn counters include
the setup turn, do not refresh on repeated use, survive enemy send-outs and
expire with ROM text in `HandleScreens` order. The existing damage path handles
physical/special defense and critical stage rules.
Seven new local-ROM regressions cover player and enemy use, PP on failure,
concurrent durations, expiration order, send-out persistence and damage stats.
Also moved the NPC test's eager ROM read into `beforeAll`, restoring its
intended skip behavior when no ROM is available.
Checks: 167 unit tests with the local ROM; 76 passed and 91 skipped without it;
44 Chromium/WebKit browser tests; native and reference builds; reference,
release and vendor checks; `git diff --check`. Screen routines were reviewed
against pinned PRET source, not compared by executing the original routines.
Changes are uncommitted.
Next: Mr. Pokémon's house and the Mystery Egg; Smart AI before enabling the
rival; capture and the catching tutorial. Joey's post-Egg phone scripts remain
unsupported.
## Mr. Pokémon and the return to Elm: 2026-09-24 (resumed from Codex)
Codex had enabled Mr. Pokémon's house and added `NameRival`, `blackoutmod`,
`reloadmap`, the queued Elm call and the no-handler Smart AI subset. It
stopped with all four quest browser tests failing and without docs. Claude
Code finished the increment:
- The rival scene needed `special FadeOutMusic` (audio only, now accepted).
- The quest fixture's constant `() => 0` RNG hung `BattleCommand_DamageVariation`,
which redraws until the byte is at least 217. The fixture now uses a seeded LCG.
Relocating to Cherrygrove had also carried the house's moved Mr. Pokémon and
Oak onto the Guide Gent and rival (ids 2 and 3), producing an unrestorable
save. The fixture now clears objects and starts off the Guide Gent's tile.
- The browser test expected `specialPhoneCall` 1; `SPECIALCALL_ROBBED` is 2.
- `NameRival` yielded a naming choice with zero pages, because `promptbutton`
had already flushed them. The dialogue never reached its last page, so the
officer scene soft-locked. It now shows the ROM's "RIVAL'S NAME?" and reads
the "SILVER" default from ROM. As in `InitName`, blank names of any number of
spaces take the default. The naming input's accessible label no longer says
"Nickname".
- Elm's call now runs the ROM's `CheckSpecialPhoneCall.script` →
`Script_ReceivePhoneCall` wrapper. It reads the condition and caller script
from `SpecialPhoneCallList` instead of hard-coding id 2, adds `memcall`, and
shows `HangUp`'s timed "Click!". Ringing and the caller name box are still
not presented.
- Added tests: the rival scene for all three starters and both triggers, a
`CANLOSE` loss, reload after each scene, the Elm call (indoors, outdoors,
mid-call reload), the officer naming (named, blank, all-space, reload at
the naming screen), the Egg handover and the aide's Poké Balls, plus a
browser naming-field test.
Checks: 191 unit tests with the local ROM (80 passed, 111 skipped without it);
50 Chromium/WebKit browser tests; native and reference builds; reference,
release and vendor checks; `git diff --check`. Not committed.
Next: capture and the Route 29 catching tutorial (`catchtutorial`), the
first remaining refusal in the opening quest. Then the ringing presentation,
player switching and items in battle. Joey's post-Egg phone scripts remain
unsupported.
## Capture and the catching tutorial: 2026-09-24
Continued the roadmap with the first refusal remaining in the opening quest.
- Battle PACK opens the Balls pocket and runs `PokeBallEffect`: the Poké,
Great, Ultra and Master Ball multipliers, the original HP arithmetic
(including the overflow above 341 maximum HP), the sleep/freeze bonus
(BRN/PSN/PAR add nothing), `HELD_CATCH_CHANCE` and the catch draw.
`GetPokeBallWobble` draws per shake from `WobbleProbabilities` and picks
the break-free text. Trainer battles block the ball but still use it and
the turn. `catchRate` is exported for pure vectors.
- A catch reloads the Pokémon as `LoadEnemyMon` does (the wild item roll is
redrawn, then the original item restored), keeps HP/status/item/DVs,
restores the moves and PP it appeared with, adds it to the party with
`SetCaughtData`, registers it and offers a nickname (`InitName` default
for blank names). `NewDexDataText` shows when the Pokédex is owned.
`caughtData` is now shared with `givepoke`.
- `catchtutorial` runs `BATTLETYPE_TUTORIAL`: "DUDE" from the ROM as
`<PLAYER>`, no send-out, an automatic catch that keeps nothing, and
prompts that continue on their own. Both Route 29 triggers, declining and
`CatchingTutorialDudeScript`'s repeat offer run from the ROM.
- Text: `TX_LOW` ($05) is read as a line break (`ItemUsedText` needs it).
- UI: PACK lists the Balls pocket (B closes it); the in-battle nickname
prompt reuses the naming field. The help dialog and README describe the
current milestone.
- Refused: a catch with six Pokémon (the PC is not ported), other balls,
non-ball items in battle and a transformed target.
- Stress (local, not committed): 2,160 wild battles, 2,047 catches, no
refusals.
Checks: 207 unit tests with the local ROM (82 passed, 125 skipped without
it); 54 Chromium/WebKit browser tests; native and reference builds;
reference, release and vendor checks; `git diff --check`. The existing
keyboard/D-pad test now steps through the enabled PACK button. No
emulator comparison of the capture routines was run. Not committed.
Next: the PC and boxes (catching with a full party, `SendMonIntoBox`, Bill's
call), then player switching and other items in battle, and party and bag
menus. The Dude's back picture, ball animation and Pokédex page are not
presented.
## Overworld foundation recovery: 2026-09-25
Resumed Claude Code session `5d15e2b5-2020-4f49-83e7-eddee08c2315`, whose user
request was “Complete the overworld foundation.” Its last successful check
compared Johto and Ice Path animation with the original VRAM; the session then
hit its usage limit. The previous handoff ended at capture and was stale.
Preserved and validated Claude's uncommitted implementation:
- Turn delays, bumps, ice and currents, forced movement, dynamic return warps,
elevator menus, pits and arrival timing. Six original-engine traces cover
outdoor, connected, ledge, interior, scripted-follow and cave movement.
- Surf, Cut, Strength, Whirlpool/Waterfall interaction paths and cycling rules;
Strength boulders can fall into script-controlled holes. Later maps are
enabled only inside tests, not in the production map list.
- Working map blocks, `changeblock`, `changemapblocks`, rollback and minimap
invalidation; cave/water/Kanto encounter tables and `wildoff`.
- Additional NPC movement, variable sprites and Pokémon icons; all movement
command decoding and teleport/fishing/skyfall/shake/flicker presentation.
- Tileset animations on private graphics, grass sprite priority and special
map palettes. Johto and Ice Path match 96 original VRAM frames after matching
the reference animation state, including the scrolling scratch buffer.
Recovery changes:
- Removed `zz-grid.test.ts` and `zz-explore.spec.ts`, temporary diagnostics with
no assertions. The grid diagnostic also read a private ROM unconditionally.
- Added grass-priority/transparency and special-palette regressions, and ran
all 36 tilesets for 2,048 animation frames each while checking that cached
ROM graphics stay unchanged. `NUM_TILESETS` is verified by `check:reference`.
- Reconciled PLAN, ARCHITECTURE and NATIVE_PORT with the actual implementation
and its remaining gaps.
Validation: 271 unit tests passed with the local ROM. With an absent
`CRYSTAL_ROM_PATH`, 127 passed and 144 skipped. The full Chromium/WebKit run
passed 64 tests with 8 intentional WebKit skips; two of those passes were the
subsequently removed exploratory test, leaving 62 substantive browser checks.
The eight new original-engine comparisons run in Chromium only. Native and
reference builds, reference addresses/constants, release isolation and vendor
checks passed. No physical-device or CI run was performed. Changes remain
uncommitted; no push was made.
The whole foundation milestone is not complete. Continue with:
1. Room decorations: the two decoration specials still implement empty slots;
stored choices, ownership and the selection flow are absent.
2. Player bag-menu access to `GameWorld.useBicycle()`. Its rules are tested,
but it is not reachable through the current game controls.
3. Remaining reference acceptance: movement traces exclude the original's
map-entry tick and compare position/facing/selected actor positions, not
event flags. Most special poses and palettes lack executed-reference or
screenshot comparisons. The Celebi forest animation stays on frame one.
Do not clear those checklist items or enable later maps based on the existing
movement and rendering tests alone.
## Overworld foundation completed: 2026-09-25 (resumed from Codex)
After the recovery above, Codex continued with "Complete the foundation" and
stopped during its final validation, before updating this file. It had
already marked the milestone done in PLAN.md. Its work:
- Room decorations (`world/decorations.ts`): the initial bed and Town Map
poster, ownership through `DecorationAttributes` event flags, the bedroom
PC's selection and put-away flows (including left/right doll placement),
room blocks and variable sprites from saved choices, save validation and
migration of saves without decorations. The poster opens the native map.
- Start/Select opens Key Items; the Bicycle follows the native indoor and
outdoor rules, with keyboard and touch controls (`foundation-browser.spec.ts`).
- Movement traces now compare event flags as well as positions. Pose checks
compare direction, visibility and vertical offset during scripted
movement. An executed `LoadMapPals` matches 192 native palette cases.
Restless Ilex Forest and Tin Tower animation match original VRAM.
- Fixed fishing and flicker pose timing against the original routines.
Claude Code finished the increment. Codex had just added two pose traces.
Tree shake passed. Skyfall departure (`skyfall_top`) failed:
`StepFunction_SkyfallTop` leaves `OBJECT_SPRITE_Y_OFFSET` at $60 on the tick
that ends the command, and the offset persists through later steps. Actors
now keep a `yOffset`. Skyfall, teleports, the fishing bite and jumps clear it,
because those routines write the offset again. In the game, only the player
uses `skyfall_top` (Ruins of Alph chambers, Burned Tower), just before a warp.
The traces now number fourteen. PLAN and NATIVE_PORT were updated to match.
Checks: 276 unit tests with the local ROM (127 passed, 149 skipped without
it); 81 Chromium/WebKit browser tests passed and 19 were skipped intentionally
(the engine parity tests run in Chromium only); native and
reference builds; reference, vendor and release checks; `git diff --check`.
Not committed.
Next (PLAN section 2): the PC and boxes (a catch with a full party,
`SendMonIntoBox`, Bill's call), then the introduction's naming and clock
screens. Also open there: player switching and non-ball items in battle,
controlled-seed comparisons beyond damage and type effectiveness, and a
continuous opening-quest playthrough with save/reload. Later maps stay gated
until their scripts and story are supported.
## The PC and boxes: 2026-09-25
The foundation above was committed and pushed as `451464f`. This increment
continues PLAN section 2 with its first open item.
- `party/storage.ts`: 14 boxes of `BoxMon` (a party struct without status,
HP and stats), the current box, box names (null is the ROM's "BOX" and
number) and 50 PC item stacks. `SendGetMonIntoFromBox` deposit and
withdraw, `RestorePPOfDepositedPokemon` and `CalcLevel` are shared helpers.
`maxPP` (ComputeMaxPP) now also serves `HealParty`.
- `scripts/pc.ts`: `PokemonCenterPC` and the house PC. The Pokémon Center PC
opens Bill's PC (deposit, withdraw, release, change and name boxes) and the
player's PC (item withdraw, deposit and toss, the empty mail box), plus
Oak's rating once the Pokédex is owned. The house PC adds DECORATION. Menu
strings, lists and texts are read from the ROM (`PC` table in the profile,
verified by `check:reference`). The generic menu effect gained per-item
`details` (levels, box counts). A `notice` effect covers unported
options without aborting.
- Facing tiles now use the ROM's `TileCollisionStdScripts` in any direction,
as in the original (the old table only fired when facing up).
- Capture with a full party: `SendMonIntoBox` puts the catch first in the
current box, with nickname and `BallSentToPCText`. A full box keeps the ball
(`Ball_BoxIsFullMessage`). Filling the box sets `BATTLERESULT_BOX_FULL`.
`reloadmapafterbattle` then queues Bill's call through a new `memscript`
effect (`LoadMemScript`). It is kept in memory only and runs before scene
scripts.
- Save format 3 gained `storage` and `pcItems`, migrated by absence.
Not ported: MOVE PKMN W/O MAIL and the stats screen (notices), printing,
`givepoke` with a full party (still refused; unreachable in the open maps),
the save messages after a box switch, and the tutorial with a full party and
box (the original loops forever; refused).
Checks: 290 unit tests with the local ROM (130 passed, 160 skipped without
it); 83 Chromium/WebKit browser tests passed, 19 skipped intentionally
(including a new Pokémon Center PC flow in both browsers); native and
reference builds; reference, vendor and release checks; `git diff --check`.
Not committed.
Next (PLAN section 2): the introduction's naming and clock screens; player
switching and non-ball items in battle; MOVE PKMN W/O MAIL and the stats
screen with the party menu; a continuous opening playthrough.
## The introduction's naming and clock: 2026-09-25
The PC increment above was committed as `3bdca00`. This increment finishes
the next open item of PLAN section 2.
- `scripts/intro.ts` (`IntroRun`): `InitGender`, `OakSpeech` with
`InitClock` and `NamePlayer`, as a native generator the world runs like a
script (`GameWorld.beginIntro`). Texts, menus, preset names and time
strings come from the ROM through a new `INTRO` profile table verified by
`check:reference`.
- New effects: `portrait` (Oak's trainer picture or the player's, decoded
by `PokemonData.trainerPic`, `playerPic` and `trainerPalette`) and a
`clock` stepper choice.
- Game clock: `clock.minuteOffset` is added to the device time for time of
day, weekday, NPC hours and the daily reset. The host's `deviceWeekday`
became `clockDay`.
- `GameSession.load` calls the runtime's optional `newGame()` when nothing
is saved. Saves gained `intro` (true while pending; a save made then
restarts the introduction). The shell's name/character fields are gone.
- Browser fixture specs strip `this.world?.beginIntro();` from the served
runtime, since their worlds stand in for saved sessions. Fresh-load tests
play the intro with `tests/intro-driver.ts`.
Not presented: palette fades, Wooper's cry, the picture slide while
naming, `ShrinkPlayer` and music. Intentional change: the steppers start at
the device time instead of 10:00.
Checks: 298 unit tests with the local ROM; native and reference builds;
reference, vendor and release checks; `git diff --check`; 87
Chromium/WebKit browser tests passed, 19 skipped intentionally (including
the new intro flow and a reload during it, in both browsers). Committed and
pushed to `main` as `b13fd20`.
Next (PLAN section 2): player switching and non-ball items in battle; MOVE
PKMN W/O MAIL and the stats screen with the party menu; a continuous opening
playthrough.
## Remaining opening-milestone items: 2026-09-30
The introduction was committed and pushed as `b13fd20` (handoff note
`eb03a20`). Asked to go over what was left and do the quickest items
first, this session took PLAN section 2's open items in that order:
1. Evolution decoder: `PokemonData.evolutions` returns typed level, item,
trade, happiness and stat records (Tyrogue's are four bytes). A
local-ROM test compares all 251 lists with the pinned
`evos_attacks.asm`. Evolving is not ported.
2. Reference vectors: `tests/reference-parity.spec.ts` now also calls the
original `CalcMonStats` (300 cases, including levels above 100 for the
999 cap; the trampoline sets BC, DE and HL) and `CalcExpAtLevel` (all
600 growth/level pairs). A mutation (removing the cap) was confirmed to
fail. Symbol loading and the ROM hand-off are shared helpers now. The
checklist item stays open for random-driven routines (accuracy,
criticals, damage variation, capture).
3. Continuous playthrough: `tests/playthrough.test.ts` plays a new game
from the intro to the catching tutorial through `GameWorld` inputs with
a seeded RNG, reloading a JSON save after every stage. Wild battles are
fought, or fled below half HP (fighting everything whited out on Route
29). The rival battle is lost with this seed (`CANLOSE`), and the story
continues. The stages share one world, so run the whole file.
4. Switching and healing items in battle (details in NATIVE_PORT):
- PKMN/`TryPlayerSwitch`/`PlayerSwitch`/`BattleMonEntrance` with
`SendOutMonText`/`WithdrawMonText`; `AskUseNextPokemon`,
`ForcePlayerMonChoice`, `DoubleSwitch`; Shift mode's
`OfferSwitch`. New `party` prompt (`menu`, `cancel`) answered through
`battleChoose` (a slot or -1), and `{ action: "pkmn" }`.
- Items pocket: `CheckItemContext`, `ItemEffects` dispatch by ROM
address, `RestoreHPEffect` and `StatusHealingEffect` with the ROM's
tables and party menu texts. `PARTYMENUTEXT_*` values are PRET's
($F0-based; `check:reference` caught a wrong value).
- UI: PKMN party list with the SWITCH/STATS/CANCEL submenu (STATS
disabled), and ITEM/BALL POCKET tabs in PACK (opens on Items). The
keyboard test now passes through the enabled PKMN button.
Known limits: `hBattleTurn` at the battle menu is not modelled everywhere,
so a status heal that would make `CalcPlayerStats` reapply a penalty to a
paralysed or burned enemy is refused. `WithdrawMonText` treats the
multiplicand's leftover high byte as zero. X items, Poké Doll, Full
Restore, revives and the stats screen are not ported.
Checks: 324 unit tests with the local ROM (130 passed, 194 skipped
without it); 93 Chromium/WebKit browser tests passed, 19 skipped
intentionally; native and reference builds; reference, release and
vendor checks; `git diff --check`. Committed and pushed to `main` as
`f8e0f9c`.
Next (PLAN section 2): controlled-seed comparisons of random-driven
battle routines. Then PLAN section 3/4 items: the party and bag menus
outside battle (with the stats screen and MOVE PKMN W/O MAIL), evolution
itself, and the remaining battle items.
## Battle items, hBattleTurn and the stats screen: 2026-09-30
After `f8e0f9c`/`cfd083c`, the user asked for the items the previous
summary listed as refused or not ported:
- Every non-ball item effect usable in battle, found by reading each
item's battle context and `ItemEffects` entry from the ROM: Full
Restore, Revive/Max Revive, Energypowder, Energy Root, Heal Powder,
Revival Herb, Bitter Berry, Ether/Max Ether/Elixer/Max Elixer/
MysteryBerry (new `move` prompt), X items, X Accuracy, Guard Spec, Dire
Hit and Poké Doll. `ChangeHappiness` now reads `HappinessChanges`
(including the `wPartyMenuCursor` quirk), which also fixed level-ups:
`LevelUpHappinessMod`'s larger gain where the Pokémon was caught was
missing (new `landmark` battle option). A second participant set
(`wBattleParticipantsIncludingFainted`) serves revives.
- `hBattleTurn` at the battle menu: `DoBattle` ends on the player's turn,
full turns end on the enemy's, and `chooseTrainerMove` now returns
`enemyTurn` when `AI_Types`, `AI_Status` or `AIDamageCalc` ran. The
status-heal refusal is gone; the enemy's penalty is reapplied as in the
original.
- Stats screen: three pages from ROM strings (`STATS` profile table,
verified by `check:reference`), `TypeNames` and `GetGender`. It opens from
BattleMonMenu and from Bill's PC (a `stats` script effect;
`world.statsScreen`).
- Emulator comparisons: a new reference test runs `SendOutMonText` (with
its final `jp BattleTextbox` patched to `ret` in the in-page ROM copy),
`WithdrawMonText`'s text routine, `RestoreHealth` (with `wHPBuffer1` set
as `ItemRestoreHP` leaves it), `RestorePP`, `ChangeHappiness` and
`CalcPlayerStats` with both turns. A mutation of `calcPlayerStats` was
confirmed to fail. The pure helpers are exported from `battle.ts`.
Remaining: Kurt's and the Park Ball; the leftover multiplicand byte in
`WithdrawMonText` is assumed zero; no whole battle has been compared with
the original.
Checks: 337 unit tests with the local ROM (131 passed, 206 skipped
without it); 97 Chromium/WebKit browser tests passed, 19 skipped
intentionally; native and reference builds; reference, release and
vendor checks; `git diff --check`. Committed and pushed to `main` as
`b13e09e`.
## Controlled random battle comparisons: 2026-09-30
Resumed from `be7575a` with a clean working tree. The user requested a bounded
increment suitable for a limited session. Continued PLAN section 2 with
accuracy, criticals and damage variation; capture stays as the next item.
- `tests/reference-random.spec.ts` runs the original, unpatched
`BattleCommand_Critical`, `BattleCommand_CheckHit` and
`BattleCommand_DamageVariation` in the reference emulator. The native
commands receive the same ten-byte Link RNG stream. Results include the
final RNG stream/cursor, so extra or missing draws are detected too.
- Per browser: 2,560 critical cases (all random bytes at all five reachable
stages and held-item exceptions), 2,914 accuracy cases (all stage pairs,
Foresight, BrightPowder, bypasses, Dream Eater/substitute and Jump Kick
miss damage), and 2,149 variation cases (0/1, rounding, 16-bit boundaries,
rejected draws and RNG stream regeneration).
- Shared ROM input and symbol helpers moved from `reference-parity.spec.ts`
into `tests/reference-fixture.ts`. No ROM bytes or extracted assets added.
- Deliberately changing critical `<` to `<=`, accuracy `>=` to `>`, and the
damage divisor from 255 to 256 each made its comparison fail. All three
mutations were reverted; no production gameplay change was needed.
Boundary: these compare isolated commands with controlled random inputs.
They do not compare a whole battle, the hardware-timed non-Link RNG, or the
Protect text path. Lock-On/Fly/Dig remain refused. This does not enable Link
battles or later maps.
Checks: 337 unit tests passed with the local ROM; 131 passed and 206
skipped with an absent ROM. All 103 Chromium/WebKit browser tests passed,
with the existing 19 intentional WebKit skips. Native and reference builds,
reference, vendor and release checks, and `git diff --check` passed. No CI
or physical-device run. This increment was initially left uncommitted and
is included in the subsequent capture-comparison delivery below.
Next: controlled-input capture comparisons (`PokeBallEffect` and
`GetPokeBallWobble`), then the party/bag menus outside battle, MOVE PKMN W/O
MAIL and evolution. Keep section 2's capture checklist open until verified.
## Controlled capture comparisons: 2026-09-30
Continued PLAN section 2 from the existing uncommitted accuracy, critical and
damage-variation comparisons. Preserved that work and completed its next
capture-comparison item.
- `tests/reference-capture.spec.ts` compares the native capture decision with
the calculation inside `PokeBallEffect`, then its shake loop with
`GetPokeBallWobble`. Per browser: 3,220 capture cases and 7,424 shake cases,
including supported ball multipliers, all catch-rate bytes, HP scaling and
overflow, status bonuses, equality boundaries, every random byte at each
wobble-table entry, every escape count and guaranteed catches. Final rate,
caught/escaped outcome, shake count and random-draw count agree.
- Capture uses hardware `Random`, not `BattleRandom`. The private in-page ROM
copy replaces `Random` with a supplied-byte reader; the fixture enters
after the UI setup and inserts a return after `wWildMon` is written.
Catch arithmetic, multipliers and `GetPokeBallWobble` remain unmodified.
- Extracted `rollCapture` and `ballWobbles` as private production helpers
called by `pokeBallEffect`, without changing gameplay behavior. Added
maximum HP 683 beside 342 in the synthetic divide-by-zero regression.
- Deliberately changing success `<=` to `<`, formula floor to ceiling, and
shake `>=` to `>` each failed the comparisons. Mutations were reverted.
Boundary: isolated capture decision/shakes with supplied random bytes, not
hardware RNG timing, animation, caught-mon reloading, party/PC writes or a
whole battle. No actual item has `HELD_CATCH_CHANCE`; that effect remains
covered by pure vectors. Native division-by-zero refusal remains in place;
the emulator fixture omits the hanging HP inputs. No additional balls or
maps are enabled.
Checks: 337 unit tests passed with the local ROM; 131 passed and 206 skipped
with an absent ROM. All 107 Chromium/WebKit browser tests passed, with the
existing 19 intentional WebKit skips. The four new capture tests skip cleanly
without a ROM. Native and reference builds, reference, vendor and release
checks, and `git diff --check` passed. No CI or physical-device run.
The accuracy, critical, damage-variation and capture comparisons were committed
together as `00972ba` and pushed to `origin/main`; the remote commit was
verified. This handoff-only follow-up records that delivery.
Next: party and bag menus outside battle, MOVE PKMN W/O MAIL and evolution.
PLAN section 2 is complete within its recorded acceptance boundaries.
## Start menu, party menu and pack: 2026-09-30
Resumed from `60e06ae` (clean tree) with "continue" from the handoff: the
next plan items were the party and bag menus outside battle, then
evolution.
- `src/game/scripts/start-menu.ts` (`StartMenuRun`): Start opens the
original start menu (items and descriptions from `StartMenu.Items`, list
from `SetUpMenuItems`). POKéMON: party menu (`PartyMenuStrings`), the
submenu from `MonMenuOptions` (field moves first), STATS, SWITCH,
MOVE (swap moves with PP), ITEM (GIVE from the pack with swap and
pack-full refund, TAKE). PACK: four pockets with left/right, the item
submenus from `MenuHeader_*.MenuData` by permissions and field menu,
TOSS with quantity and confirmation, GIVE, and USE through `ItemEffects`
addresses: HP items, status healers, Full Restore, revives, bitter
herbs, Ethers/Elixers/MysteryBerry and PP Up (one step, because
`wUsePPUp` aliases `wTempPP`), and the Bicycle (queued script after the
menu closes; Oak's warning where it cannot be used).
- It runs on a script draft; saves while open keep the prior state. Menu
cursors persist in memory and reset after battle (`CleanUpBattleRAM`);
the party cursor now reaches the battle's `ChangeHappiness` quirk.
- Select keeps the Key Items shortcut (browser tests moved from Enter to
Shift). New `pack` script effect/choice; `MENU` profile table checked by
`check:reference`; menu, pocket and PP constants moved into the verified
`C` block; `ENGINE_POKEGEAR` added and verified. The text decoder gained
`<POKE>` ($24), needed for "POKéGEAR".
- Notices without changes: POKéDEX, POKéGEAR, trainer card, OPTION, field
moves from the menu, MAIL (giving or reading), SEL, evolution stones,
Rare Candy, vitamins and other field items.
Checks: 353 unit tests with the local ROM (16 new in
`tests/start-menu.test.ts`); 109 Chromium/WebKit browser tests passed, 19
skipped intentionally (one new start-menu flow in both browsers); native
and reference builds, reference, vendor and release checks; `git diff
--check`. No emulator comparison of the menus; no CI or physical-device run.
Next: evolution (`EvolveAfterBattle` after battles and from stones/Rare
Candy, with the level-up flags the battle must record), then vitamins,
MOVE PKMN W/O MAIL, SEL/registered items and field moves from the menu.
The start menu increment was committed and pushed as `1af2bea`.
## Evolution: 2026-09-30
Continued in the same session after `1af2bea`.
- `src/game/scripts/evolution.ts` (`PartyEvolution`): `EvolveAfterBattle`
walking the raw evolution bytes with the original's skip counts
(forced evolutions misread `EVOLVE_STAT` like the original), the texts,
a timed `evolve` effect (40 ticks, then seven wait/flash rounds; B only in
the waits and never for stones), nickname update only for unnicknamed
Pokémon, stats with stat experience, HP rise, `LearnLevelMoves`,
`LearnMove`/`ForgetMove` outside battle, and the Pokédex.
- The battle records `wEvolvableFlags` on level-up and returns them; after a
win the world runs the evolution as a script interlude before resuming
the battle's script (same draft, commits together).
- PACK: Rare Candy (level, exp, HP rise that revives, happiness, level-up
text, stat page, moves, evolution), evolution stones (ABLE/NOT ABLE party
labels with the original 10-byte/3-step check; Everstone and no-match
have no effect) and vitamins (stat experience high byte +10 up to 100).
- UI: the picture box shows the flashing evolution; the pack's pocket tabs
moved into their own compact row (`#mart-tabs`).
Checks: 360 unit tests with the local ROM (5 new start-menu evolution
tests, 2 new battle-world tests); 111 Chromium/WebKit browser tests passed,
19 skipped intentionally (a new Rare Candy evolution flow in both
browsers); native and reference builds, reference, vendor and release
checks; `git diff --check`. No emulator comparison of evolution or the
menus; no CI or physical-device run.
Next: MOVE PKMN W/O MAIL, SEL/registered items with Select, field moves
from the menu (Cut, Surf, Strength already have their ROM scripts), then
the Pokégear. Trade evolutions wait for Link.
The evolution increment was committed and pushed as `45275ba`.
## Field moves, SEL and moving Pokémon: 2026-09-30
Resumed with "Continue" after `45275ba`, taking the recorded next items.
- Field moves from the party menu: the world's `menuFieldMove` runs each
menu function's checks (badges, facing tile/water/object, dark map) and
either queues the ROM's `…FromMenu` script with the chosen Pokémon or
returns the ROM's failure texts. Cut, Surf, Strength, Flash, Whirlpool,
Waterfall and Sweet Scent work; Softboiled/Milk Drink run in the menu;
Fly, Dig, Teleport, Headbutt and Rock Smash give a notice after their
checks. Encounter selection was split out of `randomEncounter` so Sweet
Scent reuses it without the rate roll.
- SEL and Select: `registeredItem` saved (migrated by absence), Select runs
`SelectMenu` (bike via the `_Register` scripts, Oak's warning, "may be
registered", registration cleared when the slot changed). The invented
Key Items shortcut (UI, runtime flag and CSS) is removed.
- Bill's PC MOVE PKMN W/O MAIL, with lists per box.
- `check:reference` caught `SPRITEMOVEDATA_SMASHABLE_ROCK` written as 18
instead of $18; fixed and covered by a Rock Smash test.
Checks: 371 unit tests with the local ROM; 111 Chromium/WebKit browser
tests passed, 19 skipped intentionally (the two Key Items browser tests now
cover SEL/Select); native and reference builds, reference, vendor and
release checks; `git diff --check`. No emulator comparison; no CI or
physical-device run.
Next: the Pokégear (clock, map, phone and radio cards), then the trainer
card and options; later the Fly map and Teleport/Dig warps (they need
script commands after a warp).
## The Pokégear, phone calls and Mom's savings: 2026-10-01
Resumed with "Continue the plan" after `3456aa4`, taking the recorded next
item (the Pokégear).
- `src/game/scripts/pokegear.ts` (`Pokegear`): POKéGEAR in the start menu
runs the clock, map and phone cards with the original joypad rules,
yielding a `pokegear` effect per card answered with a button name. The
world routes A/B/directions/Start/Select to it, keeps `world.pokegear` on
screen during call texts and refreshes the clock every tick. The radio
card (Goldenrod only) gives a notice.
- Calls: no-service text, two "……" rings, out-of-hours or same-map scripts,
otherwise the callee script from `PhoneContacts` in `ScriptRun` on the
start menu's draft (adopted with `Object.assign` when it ends), then
`HangUp`. Elm's and Mom's callee scripts run; Mom's lecture after the Dude
sets `ENGINE_MOM_SAVING_MONEY`.
- Interpreter: `random`, `givemoney`/`takemoney`/`checkmoney`/`getmoney`
for both accounts, `getcurlandmarkname`, `getlandmarkname` (operand order
checked against the macros), `ScriptHost.phoneService`, `landmarkName`.
`HangUp` is now "Click!" 10 ticks, then "……" 60 ticks (it was "Click!" for
140 ticks, double the original's 140 frames); two `mr-pokemon` assertions
were updated.
- Mom's savings: `momsMoney` in `ScriptState` and saves (migrated by absence
to 0). Battles take `mom: { saving, money }` and return `momMoney`;
`WinTrainerBattle.give_money` sends a quarter while she saves some.
`MomTriesToBuySomething` refuses once her balance reaches ¥900 (not
reachable in the open area).
- Map headers decode `phoneService`. New `POKEGEAR` profile table and OW
constants (landmark limits, card/Mom engine flags, money results), all
verified by `check:reference`.
- UI: `#pokegear` panel (card labels, clock, Town Map canvas with the
cursor frame, phone list with ▶/▷ and the submenu); the textbox is the
native dialogue, hidden on the map card; the minimap hides while it is
open. `drawTownMap` takes an optional card (region, player, cursor).
Checks: 381 unit tests with the local ROM (131 passed, 250 skipped without
it; new `tests/pokegear.test.ts`, plus Mom tests in `trainer-battle` and
`trainer-world`); 113 Chromium/WebKit browser tests passed, 19 skipped
intentionally (a new Pokégear flow in both browsers); native and reference
builds; reference, vendor and release checks; `git diff --check`. No
emulator comparison of the Pokégear; no CI or physical-device run. Not
committed.
Next: `BankOfMom` (talking to Mom after the Egg still refuses with special
34), then Mom's purchases (`MomTriesToBuySomething`, `wWhichMomItem`,
`wMomItemTriggerBalance`), the trainer card and options; later the Fly map
and Teleport/Dig warps.
## Mom's bank and her purchases: 2026-10-01
Resumed with "Continue the plan" on top of the uncommitted Pokégear
increment above (still not committed), taking the recorded next items.
- `src/game/scripts/mom.ts` (`bankOfMom`): `BankOfMom` as a special. First
visit: `MomLeavingText1`–`3` and the saving question; later: "Or is this
about your money?", then GET/SAVE/CHANGE/CANCEL from
`BankOfMom_MenuHeader`. The amount box is a `mombank` effect answered
with button names (the world routes A, B and directions to it like the
Pokégear, via `buttonChoice`). `DigitQuantities` is read from the ROM;
`GiveMoney`/`TakeMoney` limits, cursor 0–5 starting on 5, zero or B
cancels, refusals return to a fresh box. Declining runs `DSTChecks`
(near-midnight booklet refusal; `shiftClockHour` wraps `minuteOffset`
into ±720 and moves `weekdayOffset` the other way).
- `MomTriesToBuySomething` is ported (it had to be: deposits make the ¥900
trigger reachable at once, and it refused after every trainer win):
MomItems_2 triggers, the ¥2300 `wMomItemTriggerBalance` steps with the
exact-match `RandomRange(5)` MomItems_1 purchase, PC items (full PC
cancels), doll decoration events, and the call through the existing
`memscript` path (`Script_ReceivePhoneCall` with
`Mom_GetScriptPointer`'s item or doll script). `.ASMFunction`'s cost and
`wWhichMomItem` are applied when the call is queued. `randomRange` is
shared with `Script_random`.
- Saves gained `momItem` (0–10) and `momItemTriggerBalance`, migrated by
absence to 0 and ¥2300.
- New `MOM` profile table and extra `POKEGEAR` entries (MomItems lists,
item/doll scripts), verified by `check:reference`.
- UI: the amount box in the dialogue choice (`.mom-bank`, a blinking digit
that respects reduced motion) with ◀ ▼ ▲ ▶, OK and CANCEL buttons.
Checked visually at desktop and 390px widths.
Checks: 398 unit tests with the local ROM (132 passed, 266 skipped without
it; new `tests/mom.test.ts`, purchase tests replace the refusal test in
`tests/trainer-world.test.ts`); 115 Chromium/WebKit browser tests passed,
19 skipped intentionally (new deposit flow in both browsers); native and
reference builds; reference, vendor and release checks; `git diff --check`.
No emulator comparison of the bank or purchases; no CI or physical-device
run. Not committed.
Next: the trainer card and options; later the Fly map and Teleport/Dig
warps.
## Trainer card and play time: 2026-10-01
Resumed with "continue the plan" after `38b9562` (clean tree), taking the
recorded next item (the trainer card, board card #23). The Pokégear and
Mom's bank above were committed as `209778c`.
- `src/game/scripts/trainer-card.ts` (`TrainerCard`): the player's name in
the start menu runs the card and returns to the start menu. Each page is
a `trainercard` effect answered with a button (a `buttonChoice` like the
Pokégear): page 1 A/right → page 2; page 2 A leaves, left → page 1; B
leaves from both. The Kanto page (unreferenced code) is not shown. Labels
from `.Name_Money`, `.Dex_PlayTime` and `.Badges`; the Pokédex line only
with `ENGINE_POKEDEX`; badges from the Johto badge engine flags.
- `src/game/world/game-time.ts`: `wGameTime` and `GameTimer` (60 frames a
second, capped at 999:59:59.00). `GameWorld.gameTime` advances two
frames per tick unless the introduction is pending; `beginIntro` resets
it. `save()` during a scene returns the committed state with the current
play time. Saves gained `gameTime`, migrated by absence to zero.
- The world refreshes the card's time, blinking colon (every 32 frames on
page 1) and badge step (every 8 frames on page 2, only with badges) each
tick, counting from the page load.
- `src/game/rendering/trainer-card.ts`: the Chris/Kris card picture
(tile $1c left blank, as the corner covers it) and page 2's leader faces
with the owned badges' sprites, coloured by `_CGB_TrainerCard`'s
attribute map. UI: `#trainer-card` panel, frame colour from the other
gender's palette; checked visually at desktop and 390px widths.
- New `TRAINER_CARD` profile table, OW constants (`ENGINE_MINERALBADGE`,
`NUM_JOHTO_BADGES`, `PREDEFPAL_CGB_BADGE`, the leaders' trainer classes).
`check:reference` now understands the `trainerclass` macro.
- Tests that compare a save before and after a refused or rolled-back
action now ignore the play time (`withoutPlayTime` in
`tests/world-driver.ts`), which keeps counting.
Checks: 407 unit tests with the local ROM (134 passed, 273 skipped without
it; new `tests/trainer-card.test.ts`); 117 Chromium/WebKit browser tests
passed, 19 skipped intentionally (a new trainer card flow in both
browsers); native and reference builds; reference, vendor and release
checks; `git diff --check`. No emulator comparison of the card; no CI or
physical-device run.
Next: the options screen (board #24), then the Fly map and Teleport/Dig
warps.
The trainer card increment was committed as `4769856` (`Closes #23`; the
hook moved the card to Fait). Not pushed.
## Options: 2026-10-01
Continued in the same session with board card #24.
- `src/game/scripts/options.ts` (`OptionsMenu`): OPTION in the start menu
runs `_Option` on `state.options` (the start menu's draft) and returns to
the start menu. `options` effect answered with buttons; Start/B leave,
A on CANCEL leaves; `OptionsControl` wrapping; each option routine ported
from its bits (text speed's `.Save` keeps only the high nybble, so bit 3
is lost and bit 4 kept). Labels from `StringOptions` (read with a longer
`readString` limit), settings from the routines' strings.
- `GameOptions` = `wOptions`, `wTextboxFrame`, `wGBPrinterBrightness`,
`wOptions2`; `defaultOptions()` is `DefaultOptions`. In `ScriptState` and
saves (migrated by absence; frame above 7 refused).
- Consumers: `Battle` takes `shift` (from `BATTLE_SHIFT`) and
`CheckWhetherToAskSwitch` honours it; `StartMenu` drops descriptions
without `MENU_ACCOUNT`. Text speed, battle scene, sound, print and frame
are stored only.
- New `OPTIONS` profile table and OW option constants, verified by
`check:reference` (decimal literals: the checker skips `0b` values, which
a deliberately wrong `TEXT_DELAY_MED` confirmed).
- UI: `#options` panel, labels in a fixed column; checked at desktop and
390px widths.
Checks: 413 unit tests with the local ROM (134 passed, 279 skipped without
it; new `tests/options.test.ts` and a Set-mode test in
`tests/battle-switch.test.ts`); 119 Chromium/WebKit browser tests passed,
19 skipped intentionally (a new options flow in both browsers); native and
reference builds; reference, vendor and release checks; `git diff --check`.
No emulator comparison; no CI or physical-device run.
Next: the Fly map and Teleport/Dig/Escape Rope warps (board #25), which
need script commands after a warp; then Headbutt and Rock Smash (#26).
The options increment was committed as `c4cc0e7` (`Closes #24`). Not pushed.
## Fly, Dig, Teleport and the Escape Rope: 2026-10-02
Resumed with "continue" on a clean tree after `c4cc0e7`, taking board card
#25.
- `src/game/scripts/fly-map.ts` (`flyMap`): `_FlyMap`/`FlyMap` inside the
start menu run, before the world queues `FlyFunction.FlyScript`. Region
by the current landmark (Kanto only with Indigo Plateau's flypoint, else
Johto without the player icon), default New Bark/Indigo, up/down over
visited flypoints with wrapping (refused if none is visited, where the
original hangs), A returns the spawn, B goes back to the party menu.
`flymap` effect answered with buttons (`FLY_MAP_BUTTONS`).
- Visits: `assets.spawnEngineFlag` inverts the ROM's `EngineFlags` over
`wVisitedSpawns` (Union Cave and the Fast Ship have no flag).
`assets.spawnPoint`/`spawnIndex` read `SpawnPoints` (`IsSpawnPoint`).
- Interpreter: `newloadmap` handles `MAPSETUP_WARP`/`TELEPORT`/`FLY`
(spawn from `fieldMove.spawn` or `GetWhiteoutSpawn`) and `MAPSETUP_DOOR`
(`fieldMove.warp`, resolved by the world), then stops; the `warp` effect
carries `resume` unless only `end`/`endall` follows, and refuses inside a
call. The world's `finishScript` starts the rest of the script right after
entering the map, with the same field-move context (`ActiveScript.fieldMove`).
`door` warps keep the facing unless `FACE_DOWN_ON_WARP`.
`FieldMoveContext` is now one exported type.
- `fly` effect (`FlyFromAnim` 65 ticks, `FlyToAnim` 33): `poses()` is empty,
`world.flyingIcon` replays `SpriteAnimFunc_FlyFrom`/`FlyTo` with the
ROM sine table, and the renderer draws `assets.monIcon` with
`RED_WALK`'s nine-frame flaps in object palette 0. Leaves not drawn.
- World: `SaveDigWarp` in `prepareWarp` (outdoor to `CheckIndoorMap`, not
from 15:10/15:12); `digWarpMove` for Dig and the Escape Rope (with
`SpecialKabutoChamber`); Teleport through `IsSpawnPoint`; an unported
destination gives a notice. Saves gained `digWarp` (absent → null).
- Start menu: `{ flyMap: true }` verdict, `host.flyTo`, `host.escapeRope`
(`MENU.EscapeRopeEffect`: `UseDisposableItem` only on success; failure
falls to Oak's warning), `host.visitedSpawn`.
- UI: `#fly-map` with the bubble and the Town Map (`drawTownMap` takes an
optional `player` and a `species` icon). Checked visually at desktop and
390px widths, including the flight and landing.
Checks: 423 unit tests with the local ROM (134 passed, 289 skipped without
it; new `tests/fly-dig-teleport.test.ts`); 121 Chromium/WebKit browser tests
passed, 19 skipped intentionally (a new Fly flow in both browsers, with a
"fly" fixture room); native and reference builds; reference, vendor and
release checks; `git diff --check`. No emulator comparison of the flight
paths or warps; no CI or physical-device run. None of it is reachable in
normal play yet (no Storm Badge, caves or further flypoints); Teleport
works after a Pokémon Center visit for a Pokémon that knows it.
Next: Headbutt and Rock Smash encounters (board #26).
The increment was committed as `a7aceda` (`Closes #25`). Not pushed.
## Manual test cards: 2026-10-02
Asked for the checks only a person can do. The board gained an "A tester"
column (between En cours and Fait) with #46 (Fly map, flight and landing),
#47 (Teleport and the Escape Rope outdoors), #48 (touch screens, part of
#40), #49 (screen reader, related to #38) and #50 (Dig and the Escape Rope
in Dark Cave, blocked by #31). `scripts/project-board.mjs` now also reads
that column, so a commit mentioning one of those cards moves it instead of
adding it to a second column.
`tests/human-test-saves.test.ts` writes the backups the cards use, only with
`MAKE_TEST_SAVES=1` and the local ROM: `test-saves/fly-teleport.crystal-save.json`
(ignored by git, as it holds names read from the ROM). It reloads each save
in a fresh world before writing it; importing it through Savegames and using
Teleport then Fly was checked once in Chromium and WebKit.
Not made into cards, because they can be automated: emulator comparisons
of the flight paths and of the Teleport/Dig warps through the reference
harness.
## Headbutt and Rock Smash: 2026-10-02
Resumed with "continue the plan" on a clean tree after `ee55d34`, taking
board card #26.
- `src/game/scripts/treemons.ts`: `treeMonEncounter` (`TreeMonEncounter`
with `GetTreeMonSet`, `GetTreeMons`, `GetTreeScore` and `GetTreeMon`,
including the rare table after the common one), `rockMonEncounter`
(`RockMonEncounter`'s 40%) and `treeScore`, all read from the ROM's
`TreeMonMaps`, `RockMonMaps` and `TreeMons`.
- Interpreter: `callasm` runs `TreeMonEncounter` (facing tile in
`wPlayerMapX/Y` coordinates, +4), `RockMonEncounter`, `HasRockSmash`
(sets `wCurPartyMon`) and `ShakeHeadbuttTree` (a `headbutt` effect after
the text). A `tempWild` local stands for `wTempWildMonSpecies`/
`wCurPartyLevel`: `randomwildmon` reads it, `readmem wTempWildMonSpecies`
answers from it, and the battle clears it with the tree battle type, as
`CleanUpBattleRAM` does. The `battle` effect gained `tree`.
- World: facing a tree runs `AskHeadbuttScript`; HEADBUTT and ROCK SMASH
from the party menu queue `HeadbuttFromMenuScript` and
`RockSmashFromMenuScript`, the latter with the rock as `hLastTalked`
(`MenuFieldMove.lastTalked`). Talking to a rock already ran
`SmashRockScript`. The `headbutt` effect lasts 20 ticks
(`HEADBUTT_TICKS`) and `world.headbuttTree` gives the renderer the square
and frames; objects do not move meanwhile.
- Battle: `BATTLETYPE_TREE` (`tree` option) prints
`PokemonFellFromTreeText` and puts the time of day's `AsleepTreeMons`
species to sleep for `TREEMON_SLEEP_TURNS` (`CheckSleepingTreeMon`). The
skipped cry is not presented anyway.
- Renderer: tile $05 over the tree's four tiles for 36 frames, and the
`HeadbuttTreeGFX` sprite in `PAL_OW_TREE` with the frameset's timing
(three video frames a step, frame 2 mirrored on every other pass). Checked
in Chromium with close-up screenshots across the shake.
- New profile entries (OVERWORLD, BATTLE, `RAM.tempWildMonSpecies`) and
constants (`BATTLETYPE_TREE`, `TREEMON_SLEEP_TURNS`, `NUM_TREEMON_SETS`),
all verified by `check:reference`.
Checks: 435 unit tests with the local ROM (434 passed, 1 skipped; 135
passed, 300 skipped without it; new `tests/headbutt-rock-smash.test.ts`,
and the Rock Smash test in `tests/overworld-rom.test.ts` now smashes the
rock and meets a Krabby); 123 Chromium/WebKit browser tests passed, 19
skipped intentionally (a new Headbutt flow with a "headbutt" fixture room
below a Route 29 tree); native and reference builds; reference, vendor and
release checks; `git diff --check`. No emulator comparison of the tree
scores or the shake; no CI or physical-device run. Unreachable in normal
play so far: no open map has rocks, and Headbutt (the Ilex Forest tutor)
and Rock Smash (a TM on Route 36) are not obtainable yet.
Next: the remaining field items, MAIL and pack reordering (board #27).
The increment was committed as `f259163` (`Closes #26`; the hook moved the
card to Fait). Not pushed. Manual check: card #51 in "A tester" (the shake
and tree battles against the original), with
`test-saves/headbutt.crystal-save.json` from `tests/human-test-saves.test.ts`;
importing it and headbutting was checked once in Chromium and WebKit.
## Field items, fishing, Select and MAIL: 2026-10-03
Resumed with "continue the plan" on a clean tree after `10e1560`, taking
board card #27 in four commits (none pushed):
- `4cb4676`: Repel, Super Repel and Max Repel (`wRepelEffect`, the step
countdown where `CountStep` calls `DoRepelStep`, `RepelWoreOffScript`,
`CheckRepelEffect` after `ChooseWildEncounter`), the Coin Case and Blue
Card balances, Sacred Ash (`SacredAshScript`, queued), the Normal and
Gorgeous Box trophies (`DecorationIDs`), the Itemfinder
(`CheckForHiddenItems` in the world), the Card Key, Basement Key and
Squirtbottle (their maps are not open; tested with a start-menu run and
the interpreter). `text_ram` of the player's, rival's and Mom's names now
reads the text context. Saves gained `repel`, `coins` and
`blueCardBalance`. New `tests/field-items.test.ts` and the shared
`tests/menu-driver.ts`.
- `397dcef`: the three rods (`FishFunction` in the world, `scripts/fish.ts`
for `Fish`/`TimeFishGroups`, the map header's `fishGroup`), `loademote`,
`loadmem hBGMapMode`, `Fishing_CheckFacingUp`, `PutTheRodAway` (after the
pending text), the actor's fishing action and the renderer's fishing
frames (`LoadFishingGFX` tiles in VRAM bank 1; facing offsets are now
signed, which the up and left rods need), `BATTLETYPE_FISH`'s "The hooked
… attacked!". Wild Magikarp battle now: `CalcMagikarpLength` (compared
with the original over 1,509 cases in `tests/reference-parity.spec.ts`)
and `LoadEnemyMon`'s rerolls, Lake of Rage bug included. Unown are still
refused. Swarms need trainer calls. The four directions were checked in
close-up screenshots.
- `e2dc5ba`: Select in the start menu's pack (`SwitchItemsInBag`: move,
merge stacks under 99, B ends the move; GIVE's and the PC's packs ignore
Select) with the prompt and a marked row in the UI; left and right on the
MOVE screen (`.cycle_left`/`.cycle_right`, skipping Eggs) through a `menu`
effect with `sides`. The pack's cursor now stays on the last row chosen.
- `5c1ac9d` (`Closes #27`; the hook moved it to Fait): MAIL. `party/mail.ts`
holds the `mailmsg` struct; it lives on the holding `PartyMon` and in
the state's `mailbox`. Giving MAIL opens a two-line entry (the naming
effect's `mail` mode), the submenu's MAIL reads (a native letter panel,
`#mail-view`) or takes it (to the PC or into the pack), Bill's PC says
"Remove MAIL." for deposits and releases, and the player's PC MAIL BOX
reads, packs and attaches. A scripted `givepoke` with MAIL is refused.
Checked visually at desktop and 390px widths.
Checks: 460 unit tests with the local ROM (3 skipped; 136 passed, 327
skipped without it; new `tests/mail.test.ts`); 131 Chromium/WebKit
browser tests passed, 19 skipped intentionally (new Old Rod, Select and
MAIL flows); native and reference builds; reference, vendor and release
checks; `git diff --check`. "Tin Tower pillars" in
`tests/overworld-parity.spec.ts` failed once in Chromium during a full run
and passed alone and in the later full runs: timing under load. No CI or
physical-device run.
Gaps found on the way, not on #27:
- The TM/HM pocket does not exist yet (`receiveItem` throws for TMs and
the pack shows an empty TM pocket). Falkner gives TM31, so it is needed
with or before #31; no card names it.
- The MAIL stationery (`MailGFXPointers`) and the Portrait MAIL's picture
are not drawn, and the mail entry leaves out `<PK>`, `<MN>`, `<PO>`,
`<KE>` and the 'd-'v ligatures.
- `CountStep`'s poison, happiness, egg, Day-Care and bike steps are not
ported; only the Repel step is.
- #29's description still lists "MAIL storage"; the PC's MAIL BOX was done
here, so only its printing remains there.
Manual checks: cards #52 (fishing against the original), #53 (touch
screens, part of #40) and #54 (screen reader, related to #38) in
"A tester", with `test-saves/field-items.crystal-save.json` from
`tests/human-test-saves.test.ts`; importing it and fishing with the Super
Rod was checked once in Chromium and WebKit.
Next: the remaining battle commands, move effects and Smart AI handlers
(board #28).
## Battle effects, held items and obedience: 2026-10-03
Recovered the latest Claude Code session from
`~/.claude/projects/-Users-kawa-Crystal/d316d957-8ee4-4b55-aa7b-12be1bcd8726.jsonl`.
Its request was "continue the plan", taking card #28. It hit the usage limit
at 09:25 UTC while researching held items, Exp. Share and CheckObedience.
Claude had committed three groups:
- `9e0a2cd`: multi-hit and special-damage effects.
- `c53edb7`: moves spanning several turns.
- `8f5b464`: status and field move effects.
Its next group was uncommitted: weather, Substitute, Transform, Mimic,
Metronome, Mirror Move, Sleep Talk, Snore, Conversion/Conversion2, Sketch,
Baton Pass, Roar/Whirlwind and Teleport. Every move's effect script now has
a handler. Rollback fixtures use level-1 Psywave, which would loop in the
original and remains explicitly refused. These changes were preserved;
the recovered tree passed 546 unit tests before further edits. Claude's
last recorded browser run had 147 passed and 19 skipped.
The resumed increment adds:
- Smoke Ball, Metal Powder (also in the AI damage estimate), MysteryBerry,
Berserk Gene and the original unused held stat-raising effects. Item
order and stored/battle PP follow `engine/battle/core.asm`. Metal Powder
checks original species after Transform and retains byte overflow;
Berserk Gene retains the 256-turn/stale confusion-count bug.
- Exp. Share's separate participant/holder awards, halving bytes before
dividing each pool, including stat experience and existing modifiers.
- Traded Pokémon obedience, including badge thresholds, alternate moves,
nap, self-damage and idle outcomes. The world supplies its Johto badge
byte. RNG rejection loops fail closed for an invalid random source.
- Correct removal of consumed trainer healing/confusion berries from
the stored party, preventing them from returning on a switch.
- `tests/battle-held-items.test.ts` (30 local-ROM tests) and
`tests/reference-held-items.spec.ts` (320 Metal Powder cases per browser,
running the unmodified original routine). The new tests skip without
the private ROM; the emulator test also needs the pinned symbol file.
The reference harness must keep sentinels out of `$c6ff` (player screens).
The move-effect and held-item specs use audio RAM `$c0f0`, with interrupts
disabled, instead.
Validation on the final code: `npm test` passed 576 tests, with 3 skipped;
without the private ROM, 136 passed and 443 skipped. The full Chromium/WebKit
suite passed 149 tests, with 19 intentional skips, including the original
Tin Tower pillars case on this run. Native/reference builds, profile and
pinned-reference verification, vendor integrity, release isolation and
`git diff --check` passed. Metal Powder has direct emulator evidence;
the other new held-item, experience and obedience paths have local unit
coverage and source review. No CI or physical-device validation was run.
Next: complete the Smart AI effect dispatch in `src/game/battle/ai.ts`;
card #28 remains open. Other explicit boundaries include the special balls,
Unown generation, Beat Up with Eggs, and original routines that run off a
move list or loop indefinitely. Johto badge stat boosts are still absent;
the new badge option implements obedience only. Do not claim the battle
engine or the full card is complete. Delivery is tracked by the commits
referencing card #28; its commit list includes the recovered groups above.
## Smart AI handlers: 2026-10-03
Codex resumed card #28 after `32d736e` and stopped at its usage limit at
13:21 UTC with an uncommitted Smart AI translation (`smart-ai.ts`), partial
battle wiring and a failing emulator spec. Claude Code took it over.
- The spec showed 35 of 6,024 states differing, in five handlers. Encore,
Roar/Whirlwind, Baton Pass and Perish Song came from `CheckTypeMatchup`
ignoring the type in `a`: it reads the move type and Foresight flag through
`hBattleTurn`. The AI now tracks the turn and both move structs' types;
`checkPlayerMoveTypeMatchups` takes the type and flag. Conversion2's write
to the player's move type persists in the battle.
- Psych Up sums `NUM_LEVEL_STATS` (8) bytes. The eighth is only ever reset to
7 or copied, so the 7-entry sum is equivalent; the spec now writes it as 7.
- The spec randomizes the starting turn and the player's move type and also
compares them afterwards. It passes 6,024 states per browser; a one-off
run with another seed passed 18,072 per browser.
Validation: 579 unit tests passed (3 skipped); without the ROM 138 passed,
444 skipped. Browser suite: 151 passed, 19 intentional skips. Both builds,
reference/release/vendor checks and `git diff --check` passed. No CI or
physical-device validation.
Next on #28: Kurt's balls, Park and Safari balls, and special battle types.
Other boundaries listed in the previous section are unchanged.
## Kurt's balls: 2026-10-03
Same session, after the Smart AI commit.
- `Battle.kurtBallRate` translates the Heavy, Level, Lure, Fast, Moon and
Love multipliers; `catchRate` adds the Park Ball's 1.5× and the Level
Ball's skip of the HP, status and held-item terms. Friend Ball catches
get 200 happiness in the party or the PC box.
- The Heavy Ball's buggy bank makes Kadabra's (64) and Sunflora's (192)
scans run past $7fff into VRAM, which the port does not model; those
throws raise `UnsupportedBattle`. Tauros (128) reads a wrong in-ROM
weight, which is reproduced.
- `tests/reference-capture.spec.ts` gained a 6,153-state comparison
against the unmodified `PokeBallEffect`. Mutating the Love Ball gender
test or the Fast Ball's byte count fails it. A Heavy Ball light-mon
off-by-one mutation is undetectable: rates 0 and 1 both end at 1.
Validation: 582 unit tests passed (3 skipped); without the ROM 139 passed,
446 skipped. Browser suite: 153 passed, 19 skipped. Both builds,
reference/release/vendor checks and `git diff --check` passed.
Next on #28: the Bug-Catching Contest battle type (Park Ball count,
`BugContest_SetCaughtContestMon`) and the other special battle types.
Neither is reachable yet: National Park and the legendary encounters
are not enabled.
## Special battle types: 2026-10-03
Same session; finishes card #28.
- `BattleOptions.battleType` takes FORCESHINY, TRAP, FORCEITEM, CELEBI,
SUICUNE, CONTEST (with `contest`) and ROAMING (with `roamer`); invalid
combinations raise `UnsupportedBattle` before any draw. `loadvar
VAR_BATTLETYPE` passes the script-set ones; CleanUpBattleRAM's clear
follows `startbattle`.
- `run()` ends with `BattleEnd_HandleRoamMons`: one extra BattleRandom
after every non-roaming battle (`updateRoamMons` in the outcome). The
world ignores the flag until roaming Pokémon exist.
- Contest: `{ kind: "action", parkBalls }` drives the PARKBALL× button;
`{ kind: "contestswitch" }` is answered like Yes/No (world, keyboard and
DOM). `ScriptState.bugContest` is runtime-only (not saved);
`readmem wParkBallsRemaining` reads it. The fixture scenario `contest`
covers it in `tests/battle-world.test.ts` and
`tests/battle-browser.spec.ts`. The browser test found that the switch
text was missing from the text box; it is fixed.
- `tests/battle-types.test.ts` covers each type with the local ROM, plus
a synthetic `loadvar` script.
Validation: 596 unit tests passed (3 skipped); without the ROM 141 passed,
458 skipped. Browser suite: 155 passed, 19 skipped. Both builds,
reference/release/vendor checks and `git diff --check` passed.
Left for the map cards: National Park's contest event (GiveParkBalls,
timer, out-of-balls/time-up scripts, judging, saving the contest state),
Burned Tower's roaming release with UpdateRoamMons/JumpRoamMons and
roaming encounters, and Ilex Forest's CelebiShrineEvent/CheckCaughtCelebi.
## Route 31 to Violet City: 2026-10-03
Card #31 in the same session.
- Maps: 26:2, 26:11, 3:78, 10:5–10:11, 3:1–3:3 in `NATIVE_MAPS`. The
closed-connection test now uses Violet City's south edge (Route 32).
- Script commands added: 0x43 gettrainername, 0x97 askforphonenumber,
0x30 checkpokemail, 0x4f/0x58/0x50 loadmenu/_2dmenu/closewindow,
0x2e giveegg, 0x96 trade; `loadvar VAR_CALLERID` (0x17, wCurCaller)
and the RandomPhoneMon special. `addcellnum` returned success where
the original returns TRUE on failure; fixed.
- Eggs: `PartyMon.egg` with the real species (was species 0xfd). Every
former `species === EGG` check uses the flag. HealParty skips Eggs.
The stats screen has the Egg page (EggPic, EggPalette). Hatching is
not ported (#29).
- TM/HM: `Bag.tmsHms` (57 counts, migrated as zeros for old saves),
item names TM01–HM07, `tmhmMove`, `canLearnTMHM`; the pack's pocket,
USE/GIVE/QUIT submenu and AskTeachTMHM/ChooseMonToLearnTMHM/TeachTMHM.
- `ScriptState.tradeFlags` (saved) for NPCTrade.
- BadgeStatBoosts at InitBattleMon, the in-battle level-up and
CalcPlayerStats; `tests/reference-badges.spec.ts` compares it.
- Event flag numbers for tests: read them from ROM scripts. Counting
`constants/event_flags.asm` by hand drifted by 0x200 for later flags.
- `MAKE_TEST_SAVES=1 npx vitest run tests/playthrough.test.ts` writes
`test-saves/violet-city.crystal-save.json` (Cyndaquil L11 at Violet's
east gate) for the manual check cards.
Validation: 610 unit tests passed (3 skipped); without the ROM 143
passed, 470 skipped. Browser suite: 157 passed, 19 skipped. Both builds,
reference/release/vendor checks and `git diff --check` passed.
Open boundaries: Egg hatching (#29), trainer calls/rematches (#30), the
trade animation, Earl's blackboard as a list rather than a 2×3 grid,
Kenya's MAIL text in the MAIL model, Route 32/36 and Dark Cave's far side.