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

+23 -2
View File
@@ -126,6 +126,7 @@ export class CrystalAssets {
environment: r.byte(header + 2),
paletteMode: r.byte(header + 7) & 15,
phoneService: r.byte(header + 7) >> 4,
fishGroup: r.byte(header + 8),
attributes,
blocks: r.slice(r.far(attributes + 3), blockWidth * blockHeight),
warps: [],
@@ -500,6 +501,24 @@ export class CrystalAssets {
r.byte(entry + 2),
);
}
/** The first vTiles0 tile an emote's graphics are loaded to. */
emoteTile(index: number): number {
if (index < 0 || index > 11) throw new Error("Unknown emote.");
return (this.reader.word(SYMBOLS.emotes + index * 6 + 4) - 0x8000) >> 4;
}
/** LoadFishingGFX: the player's sprite (24 tiles) with the lower halves
* of the standing frames replaced by FishingGFX or KrisFishingGFX, then
* the two rod tiles that the fishing frames place at $fc and $fd. All
* of them are in VRAM bank 1, as the player's sprite is. */
fishingSprite(graphics: Uint8Array, kris: boolean): Uint8Array {
const source = kris ? OVERWORLD.KrisFishingGFX : OVERWORLD.FishingGFX;
const result = new Uint8Array(26 * 16);
result.set(graphics.subarray(0, 24 * 16));
[2, 6, 10, 24].forEach((tile, chunk) =>
result.set(this.reader.slice(source + chunk * 32, 32), tile * 16),
);
return result;
}
/** HeadbuttTreeGFX: the shaking tree's two 2x2 frames, eight tiles. */
headbuttTree(): Uint8Array {
return this.reader.slice(OVERWORLD.HeadbuttTreeGFX, 8 * 16);
@@ -553,9 +572,11 @@ export class CrystalAssets {
pointer = r.near(SYMBOLS.facings + facing * 2, r.bank(SYMBOLS.facings));
return Array.from({ length: r.byte(pointer) }, (_, i) => {
const p = pointer + 1 + i * 4;
// The offsets are added to the sprite's position modulo 256, so
// they act as signed bytes.
return {
y: r.byte(p),
x: r.byte(p + 1),
y: (r.byte(p) << 24) >> 24,
x: (r.byte(p + 1) << 24) >> 24,
attributes: r.byte(p + 2),
tile: r.byte(p + 3),
};
+17
View File
@@ -161,6 +161,8 @@ export const RAM = {
visitedSpawns: 0xdca5,
tempWildMonSpecies: 0xd22e,
coins: 0xd855,
/** hBGMapMode: HRAM, like hMoneyTemp below. */
bgMapMode: 0xffd4,
blueCardBalance: 0xdc4b,
/** hMoneyTemp: HRAM, not WRAM like the other entries above. */
moneyTemp: 0xffc3,
@@ -274,6 +276,16 @@ export const OVERWORLD = {
"_BasementKey.BasementKeyScript": 0x507e1,
"_Squirtbottle.SquirtbottleScript": 0x5073c,
"_Squirtbottle.CheckCanUseSquirtbottle": 0x50753,
FishGroups: 0x92488,
TimeFishGroups: 0x9266f,
Script_GotABite: 0xd035,
Script_NotEvenANibble: 0xd01e,
Script_NotEvenANibble2: 0xd027,
Fishing_CheckFacingUp: 0xd06c,
LoadFishingGFX: 0xb84b3,
PutTheRodAway: 0xd095,
FishingGFX: 0xb84f2,
KrisFishingGFX: 0xb8582,
Flypoints: 0x91c5e,
"TownMapBubble.Where": 0x91bf4,
RuinsOfAlphKabutoChamber_MapAttributes: 0x958d0,
@@ -531,6 +543,9 @@ export const MENU = {
CardKeyEffect: 0xf745,
BasementKeyEffect: 0xf74c,
SquirtbottleEffect: 0xf73e,
OldRodEffect: 0xf5a5,
GoodRodEffect: 0xf5a9,
SuperRodEffect: 0xf5ad,
VitaminEffect: 0xee3d,
RareCandyEffect: 0xef14,
ItemStatRoseText: 0xeea6,
@@ -548,6 +563,8 @@ export const MENU = {
/** Battle engine tables and texts, keyed by their PRET labels so that
* \`npm run check:reference\` can verify each address by name. */
export const BATTLE = {
HookedPokemonAttackedText: 0x8075c,
MagikarpLengths: 0xfbca8,
ItemUsedText: 0xf83d,
BallBrokeFreeText: 0xedb5,
BallAppearedCaughtText: 0xedba,