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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user