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

+103
View File
@@ -12,6 +12,7 @@ import {
runScript,
} from "../src/game/scripts/interpreter";
import { generatePartyMon } from "../src/game/party/pokemon";
import type { Evolution } from "../src/game/rom/pokemon";
import { readMovement } from "../src/game/scripts/movement";
import { defaultTextContext, readText } from "../src/game/scripts/text";
import { settle, walkTo } from "./world-driver";
@@ -613,6 +614,108 @@ describe.skipIf(!existsSync(romPath))(
);
},
);
it("decodes evolution lists, including stat and happiness triggers", () => {
const data = load().pokemon;
expect(data.evolutions(155)).toEqual([
{ method: "level", level: 14, species: 156 },
]);
expect(data.evolutions(133).map((e) => e.method)).toEqual([
"item",
"item",
"item",
"happiness",
"happiness",
]);
expect(data.evolutions(133)[4]).toEqual({
method: "happiness",
time: "nite",
species: 197,
});
expect(data.evolutions(61)).toContainEqual({
method: "trade",
item: 82, // KING'S ROCK
species: 186,
});
expect(data.evolutions(64)).toEqual([
{ method: "trade", item: null, species: 65 },
]);
expect(data.evolutions(236)).toEqual([
{ method: "stat", level: 20, attack: "lt", species: 107 },
{ method: "stat", level: 20, attack: "gt", species: 106 },
{ method: "stat", level: 20, attack: "eq", species: 237 },
]);
expect(data.evolutions(251)).toEqual([]);
// The four-byte stat records must not shift the learnset.
expect(data.learnset(236)[0]).toEqual({ level: 1, move: 33 });
});
it.skipIf(
!existsSync(".reference/pokecrystal/data/pokemon/evos_attacks.asm"),
)("matches every evolution list against the pinned PRET source", () => {
const data = load().pokemon;
const root = ".reference/pokecrystal";
const constants = (file: string, first: number) => {
const ids = new Map<string, number>();
let value = first;
for (const line of readFileSync(`${root}/${file}`, "utf8").split(
"\n",
)) {
if (/^DEF NUM_/.test(line)) break;
const name = /^\s*const (\w+)/.exec(line)?.[1];
if (name) ids.set(name, value++);
}
return ids;
};
const species = constants("constants/pokemon_constants.asm", 1),
items = constants("constants/item_constants.asm", 0);
const id = (map: Map<string, number>, name: string) => {
const value = map.get(name);
if (value === undefined) throw new Error(`Unknown ${name}`);
return value;
};
const lists: Evolution[][] = [];
let current: Evolution[] | null = null;
for (const line of readFileSync(
`${root}/data/pokemon/evos_attacks.asm`,
"utf8",
).split("\n")) {
if (/^\w+EvosAttacks:/.test(line)) lists.push((current = []));
const args = /^\s*db (EVOLVE_\w+), (.+)$/.exec(line);
if (!args || !current) {
if (/^\s*db 0 ; no more evolutions/.test(line)) current = null;
continue;
}
const [a, b, c] = args[2].split(",").map((s) => s.trim());
if (args[1] === "EVOLVE_LEVEL")
current.push({ method: "level", level: +a, species: id(species, b) });
else if (args[1] === "EVOLVE_ITEM")
current.push({
method: "item",
item: id(items, a),
species: id(species, b),
});
else if (args[1] === "EVOLVE_TRADE")
current.push({
method: "trade",
item: a === "-1" ? null : id(items, a),
species: id(species, b),
});
else if (args[1] === "EVOLVE_HAPPINESS")
current.push({
method: "happiness",
time: a.slice(3).toLowerCase() as "anytime",
species: id(species, b),
});
else
current.push({
method: "stat",
level: +a,
attack: b.slice(4, 6).toLowerCase() as "gt",
species: id(species, c),
});
}
expect(lists).toHaveLength(251);
lists.forEach((list, i) => expect(data.evolutions(i + 1)).toEqual(list));
});
it("gives the chosen starter and the aide's Potion, then saves them", () => {
let seed = 7;
const random = () => (seed = (seed * 73 + 41) & 255);