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

+66
View File
@@ -38,6 +38,25 @@ export interface MoveData {
/** Item types, which select a bag pocket (constants/item_data_constants.asm). */
export type Pocket = "item" | "key" | "ball" | "tm";
const POCKETS: readonly Pocket[] = ["item", "key", "ball", "tm"];
/** One entry of a species' evolution list (data/pokemon/evos_attacks.asm).
* A trade's `item` is null for the ROM's -1 (no held item needed). */
export type Evolution =
| { method: "level"; level: number; species: number }
| { method: "item"; item: number; species: number }
| { method: "trade"; item: number | null; species: number }
| {
method: "happiness";
time: "anytime" | "mornday" | "nite";
species: number;
}
| {
method: "stat";
level: number;
attack: "gt" | "lt" | "eq";
species: number;
};
const HAPPINESS_TIMES = ["anytime", "mornday", "nite"] as const;
const STAT_TRIGGERS = ["gt", "lt", "eq"] as const;
/** Decoders for species, moves and items. All data is read from the
* user's ROM on demand and cached in memory only. */
@@ -175,6 +194,53 @@ export class PokemonData {
};
}
/** The evolution list before the learnset, in ROM order (EvolveAfterBattle
* tries entries in this order). EVOLVE_STAT records are four bytes long;
* the others three. */
evolutions(species: number): Evolution[] {
this.checkSpecies(species);
const r = this.reader;
let p = r.near(
SYMBOLS.evosAttacks + (species - 1) * 2,
r.bank(SYMBOLS.evosAttacks),
);
const evolutions: Evolution[] = [];
for (let method = r.byte(p); method !== 0; method = r.byte(p)) {
if (evolutions.length > 8) throw new Error("Invalid evolution data.");
const a = r.byte(p + 1);
let evolution: Evolution;
if (method === 1)
evolution = { method: "level", level: a, species: r.byte(p + 2) };
else if (method === 2)
evolution = { method: "item", item: a, species: r.byte(p + 2) };
else if (method === 3)
evolution = {
method: "trade",
item: a === 0xff ? null : a,
species: r.byte(p + 2),
};
else if (method === 4) {
const time = HAPPINESS_TIMES[a - 1];
if (!time) throw new Error("Invalid evolution data.");
evolution = { method: "happiness", time, species: r.byte(p + 2) };
} else if (method === 5) {
const attack = STAT_TRIGGERS[r.byte(p + 2) - 1];
if (!attack) throw new Error("Invalid evolution data.");
evolution = {
method: "stat",
level: a,
attack,
species: r.byte(p + 3),
};
p++;
} else throw new Error("Invalid evolution data.");
this.checkSpecies(evolution.species);
evolutions.push(evolution);
p += 3;
}
return evolutions;
}
/** Level-up moves (data/pokemon/evos_attacks.asm). Like FillMoves, the
* evolution list is skipped up to its terminating zero byte. */
learnset(species: number): { level: number; move: number }[] {
+27
View File
@@ -157,6 +157,7 @@ export const RAM = {
callerContact: 0xd03f,
surfingPlayerState: 0xd1eb,
strengthSpecies: 0xd1ef,
curHPAnimDeltaHP: 0xd1f3,
/** hMoneyTemp: HRAM, not WRAM like the other entries above. */
moneyTemp: 0xffc3,
} as const;
@@ -468,6 +469,32 @@ export const BATTLE = {
HasNoPPLeftText: 0x80fba,
HeldStatusHealingEffects: 0x3de44,
HPIsFullText: 0x81534,
DoItMonText: 0x3f2d8,
GoForItMonText: 0x3f2df,
YourFoesWeakGetmMonText: 0x3f2e6,
"WithdrawMonText.WithdrawMonText": 0x3f2fa,
ThatsEnoughComeBackText: 0x3f348,
OKComeBackText: 0x3f34d,
GoodComeBackText: 0x3f352,
ComeBackText: 0x3f35b,
BattleText_UseNextMon: 0x80a83,
BattleText_MonIsAlreadyOut: 0x80c0d,
BattleText_MonCantBeRecalled: 0x80c22,
BattleText_TheresNoWillToBattle: 0x80b0b,
BattleText_AnEGGCantBattle: 0x80b26,
BattleText_EnemyIsAboutToUseWillPlayerChangeMon: 0x80aca,
ChooseAMonString: 0x504e4,
WhichPKMNString: 0x50504,
ItemEffects: 0xe73c,
RestoreHPEffect: 0xf186,
StatusHealingEffect: 0xefcc,
StatusHealingActions: 0xf071,
HealingHPAmounts: 0xf3af,
"PrintPartyMenuActionText.MenuActionTexts": 0x5057b,
ItemWontHaveEffectText: 0xf81f,
ItemCantUseOnEggText: 0xf810,
OakThisIsntTheTimeText: 0x10af3,
UseOnWhichPKMNString: 0x504f3,
HungOnText: 0x80e99,
HurtByBurnText: 0x807f8,
HurtByPoisonText: 0x807e2,