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:
1 parent
eb03a20d8f
commit
f8e0f9c65a
20 files changed
+1902
-96
No files matched your search
@@ -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 }[] {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user