Port the fishing rods and wild Magikarp

The Old, Good and Super Rods run FishFunction facing water: the map's fish
group and Fish pick no fish, no bite, or a Pokémon from FishGroups and the
time of day's TimeFishGroups. The player casts and jiggles with the
fishing frames drawn from LoadFishingGFX's tiles, shows "!" on a bite, and
the hooked Pokémon attacks as BATTLETYPE_FISH. Facing pieces now use
signed offsets, which the rod above and left of the player needs.

Wild Magikarp no longer stop a battle: LoadEnemyMon redraws their DVs
through CalcMagikarpLength and its filters, as the original does. The
length routine matches the original in the reference emulator.

Part of #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-10-03 00:49:35 +02:00
1 parent 4cb4676c7d
commit 397dcef44c
20 files changed
+681 -47

No files matched your search

+73
View File
@@ -6,6 +6,7 @@ import {
Battle,
applyTypeMatchups,
calculateDamage,
magikarpLength,
restoreHealth,
restoredPP,
sendOutText,
@@ -638,3 +639,75 @@ test("native switch texts, healing, PP, happiness and CalcPlayerStats match the
).toEqual(results.stats[index]);
});
});
test("native Magikarp lengths match CalcMagikarpLength", async ({ page }) => {
test.skip(
!existsSync(romPath) || !existsSync(symbolsPath),
"Requires the local user ROM and the private PRET rebuild.",
);
const { at, routine } = loadSymbols();
const reader = new CrystalAssets(new Uint8Array(readFileSync(romPath))).reader;
let seed = 0x4a39;
const next = (limit: number) => {
seed = (seed * 1103515245 + 12345) >>> 0;
return (seed >>> 8) % limit;
};
// Random DVs and IDs, plus indexes below 10 and at the table's edges.
const cases = [
...Array.from({ length: 1500 }, () => ({ dvs: next(0x10000), id: next(0x10000) })),
...[0x0000, 0x0009, 0x000a, 0x00ff, 0x0100, 0xfdff, 0xfe00, 0xff00, 0xffff].map((dvs) => ({
dvs,
id: 0,
})),
];
await openReference(page);
const addresses = {
calc: routine("CalcMagikarpLength"),
length: at("wMagikarpLength"),
};
const results = await page.evaluate(
async ({ addresses, cases }) => {
const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts");
const file = (document.querySelector("#reference-fixture") as HTMLInputElement).files![0];
const reference = new BinjgbEmulator();
await reference.load(new Uint8Array(await file.arrayBuffer()), document.createElement("canvas"));
const write = (address: number, value: number) => reference.writeMemory(address, value & 0xff);
// Trampoline: DI; SP=$c7fe; switch to the routine's bank; set BC (the
// ID) and DE (the DVs); CALL CalcMagikarpLength; write a sentinel; loop.
const dvs = 0xc100,
id = 0xc102;
const program = [
0xf3, 0x31, 0xfe, 0xc7, 0x3e, addresses.calc.bank, 0xe0, 0x9d, 0xea, 0x00, 0x20,
0x01, id & 0xff, id >> 8, 0x11, dvs & 0xff, dvs >> 8,
0xcd, addresses.calc.address & 0xff, addresses.calc.address >> 8,
0x3e, 0x42, 0xea, 0xff, 0xc6, 0x18, 0xfe,
];
write(0xff70, 1);
const lengths = cases.map((input) => {
write(dvs, input.dvs >> 8);
write(dvs + 1, input.dvs);
write(id, input.id >> 8);
write(id + 1, input.id);
write(0xc6ff, 0);
program.forEach((value, index) => write(0xc000 + index, value));
reference.setProgramCounter(0xc000);
reference.advanceFrames(2);
return {
returned: reference.readMemory(0xc6ff) === 0x42,
feet: reference.readMemory(addresses.length),
inches: reference.readMemory(addresses.length + 1),
};
});
reference.dispose();
return lengths;
},
{ addresses, cases },
);
cases.forEach((input, index) => {
const { returned, ...original } = results[index];
expect(returned, `case ${index} returned`).toBe(true);
expect(magikarpLength(reader, input.dvs, input.id), JSON.stringify(input)).toEqual(original);
});
const lengths = new Set(results.map(({ feet, inches }) => feet * 12 + inches));
expect(lengths.size).toBeGreaterThan(30);
});