Skip to content

Overhaul Fishing Rod Encounters

PokemonTrainerTroy edited this page Aug 3, 2026 · 2 revisions

Introduction

This is my first tutorial submission. I am not familiar with assembly code outside of rom hacking, and I'm only familiar with rom hacking thanks to this communities other pokered and pokecrystal tutorials. What follows was created out of trail, error, and help from chatgpt. That said, this is perfectly functional, but there are likely optimizations that can made.

In any case, my goal was to wholly overhaul how the game generates fishing encounters by mirroring the logic already in place for Grass and Surf encounters.

I started with Lotsob's tutorial below, which greatly improves on the fishing system by copying the routines and encounter groups for the Super Rod into the Old Rod and Good Rod effects, allowing for more versatile fishing encounters. It also includes an excellent write up on the current fishing system's shortcomings. I suggest giving this a read before proceeding.

https://github.com/pret/pokered/wiki/Improving-Fishing-Rod-Encounters

That solution is extremely reasonable, but it leaves a few very minor things to be desired:

  • We cannot set unique encounter rates per map. Since the percent chance of hooking a bite is determined by the rod, we couldn't make it so Viridian City has a 75% chance and Cerulean Cave has a 25% chance of hooking a fish. Admittedly pretty unnecessary, but still. Grass and Surf encounters have encounter rates defined per map, why shouldn't fish?
  • We cannot set unique probabilities per encounter slot. The fishing groups all have equal distribution. If you have 4 FishMons per group, then each are 25% rare. Now, you can double up Pokemon in multiple encounter slots to create rarity, but you're still bound by the equal distribution. This means no Pokemon can be rarer than 6.25% (1/16th) if you put 16 Pokemon in a given fish group. If we want a 1% rare Omanyte lurking around, we're out of luck.

The changes below will allow Fishing to use the same WildMonEncounterSlotChances table in probabilities.asm and call unique encounter data per map. This is probably overkill for most purposes, but I wanted to see if it was possible, and it's nice to have the encounter data match grass and water cleanly instead of using an entirely separate fishing groups file.

Let's get started.


Step 1

First, we'll need to create wFishRate and wFishMons in wram.asm to match the existing Grass and Water code.

; This union spans 293 bytes.
UNION
wGrassRate:: db
wGrassMons:: ds WILDDATA_LENGTH - 1
-
-	ds 8

wWaterRate:: db
wWaterMons:: ds WILDDATA_LENGTH - 1
+
+wFishRate:: db
+wFishMons:: ds WILDDATA_LENGTH - 1

NEXTU
; linked game's trainer name

Step 2

Second, we'll need to create def_fish_wildmons and end_fish_wildmons Macros in asserts.asm to match the existing Grass and Water code.

MACRO? def_water_wildmons
;\1: encounter rate
	DEF CURRENT_WATER_WILDMONS_RATE = \1
	REDEF CURRENT_WATER_WILDMONS_LABEL EQUS "._def_water_wildmons_\1"
{CURRENT_WATER_WILDMONS_LABEL}:
	db \1
ENDM

MACRO? end_water_wildmons
	DEF x = @ - {CURRENT_WATER_WILDMONS_LABEL}
	IF CURRENT_WATER_WILDMONS_RATE == 0
		ASSERT 1 == x, \
			"def_water_wildmons {d:CURRENT_WATER_WILDMONS_RATE}: expected 1 byte, got {d:x}"
	ELSE
		ASSERT WILDDATA_LENGTH == x, \
			"def_water_wildmons {d:CURRENT_WATER_WILDMONS_RATE}: expected {d:WILDDATA_LENGTH} bytes, got {d:x}"
	ENDC
ENDM

+MACRO? def_fish_wildmons
+;\1: encounter rate
+	DEF CURRENT_FISH_WILDMONS_RATE = \1
+	REDEF CURRENT_FISH_WILDMONS_LABEL EQUS "._def_fish_wildmons_\1"
+{CURRENT_FISH_WILDMONS_LABEL}:
+	db \1
+ENDM
+
+MACRO? end_fish_wildmons
+	DEF x = @ - {CURRENT_FISH_WILDMONS_LABEL}
+	IF CURRENT_FISH_WILDMONS_RATE == 0
+		ASSERT 1 == x, \
+			"def_fish_wildmons {d:CURRENT_FISH_WILDMONS_RATE}: expected 1 byte, got {d:x}"
+	ELSE
+		ASSERT WILDDATA_LENGTH == x, \
+			"def_fish_wildmons {d:CURRENT_FISH_WILDMONS_RATE}: expected {d:WILDDATA_LENGTH} bytes, got {d:x}"
+	ENDC
+ENDM	

Step 3

Rewrite LoadWildData in wild_mons.asm to handle our new Fish data. The old routine would check for Grass encounters, then skip to Water encounters, then close. Now, we're including one more check for Fish using the same logic.

LoadWildData::
	ld hl, WildDataPointers
	ld a, [wCurMap]

	; get wild data for current map
	ld c, a
	ld b, 0
	add hl, bc
	add hl, bc
	ld a, [hli]
	ld h, [hl]
	ld l, a              ; hl now points to wild data for current map
	ld a, [hli]
	ld [wGrassRate], a 
	and a
	jr z, .NoGrassData   ; if no grass data, skip to surfing data
	push hl
	ld de, wGrassMons    ; otherwise, load grass data
	ld bc, WILDDATA_LENGTH - 1
	call CopyData
	pop hl
	ld bc, WILDDATA_LENGTH - 1
	add hl, bc
.NoGrassData
	ld a, [hli]
	ld [wWaterRate], a
	and a
-       ret z                ; if no water data, we're done
+	jr z, .NoWaterData   ; if no water data, skip to fishing data
+	push hl
	ld de, wWaterMons    ; otherwise, load surfing data
	ld bc, WILDDATA_LENGTH - 1
-       jp CopyData
+	call CopyData
+	pop hl
+	ld bc, WILDDATA_LENGTH - 1
+	add hl, bc
+.NoWaterData
+	ld a, [hli]
+	ld [wFishRate], a
+	and a
+	ret z                        ; if no fish data, we're done
+	ld de, wFishMons             ; otherwise, load fish data
+	ld bc, WILDDATA_LENGTH - 1
+	jp CopyData

INCLUDE "data/wild/grass_water.asm"

Step 4

Here's where things get tedious. Instead of using one file super_rod.asm we are going to put fish data onto EVERY SINGLE MAP file in data > wild > maps. This also means we need to create a few new map files for areas (towns) that want fish encounters but that didn't previously have a map file in the wild folder, since no grass or surf encounters occur there.

Let's use Viridian City as an example. We need to create a pointer and include the file in grass_water.asm.

WildDataPointers:
	table_width 2
	dw NothingWildMons         ; PALLET_TOWN
-       dw NothingWildMons         ; VIRIDIAN_CITY
+	dw ViridianCityWildMons    ; VIRIDIAN_CITY
	dw NothingWildMons         ; PEWTER_CITY

. . . 

INCLUDE "data/wild/maps/nothing.asm"
INCLUDE "data/wild/maps/Route1.asm"
+INCLUDE "data/wild/maps/ViridianCity.asm"
INCLUDE "data/wild/maps/Route2.asm"
INCLUDE "data/wild/maps/Route22.asm"

We then have to actually make that file.

Create ViridianCity.asm in data > wild > maps. Notice def_fish_wildmons uses the exact same format as def_grass_wildmons and def_water_wildmons. It will follow the same percentages for each encounter slot defined in the WildMonEncounterSlotChances table in probabilities.asm. This allows us to set 10 (or more if you follow this guide) encounters with internally consistent rarities to our existing grass and surf WildMons.

ViridianCityWildMons:
	def_grass_wildmons 0 ; encounter rate
	end_grass_wildmons

	def_water_wildmons 0 ; encounter rate
	end_water_wildmons

    def_fish_wildmons 192 ; encounter rate
    db 5, MAGIKARP
	db 10, POLIWAG
	db 10, MAGIKARP
	db 20, POLIWAG
	db 10, MAGIKARP
	db 20, POLIWAG
	db 15, MAGIKARP
	db 25, POLIWAG
	db 15, MAGIKARP
	db 25, POLIWHIRL
	end_fish_wildmons

This also means EVERY SINGLE MAP file should be appended with fish data. The routine we updated in Step 3 expects each file to end with Fish data now. Skipping this may lead to unintended encounters, presumably along the lines of the Cinnabar coast glitch, since data wouldn't be cleared properly. Or it could be totally fine. I really don't know.

     def_water_wildmons 0 ; encounter rate
     end_water_wildmons
+
+    def_fish_wildmons 0 ; encounter rate 
+    end_fish_wildmons

Let's pause here, there are a few considerations to call out.

  • You will need to make files like the one above for several towns that have ponds or rivers like Viridian, Cerulean, Celadon, and Fuchsia.

  • The placement of the new MapNameWildMons pointer MUST MATCH the placement of the bank file in map_header_banks.asm. This is not a concern for the towns above since they're already defined in the right order, but for new files like Vermilion City Port (yes, the port is a separate map file) and for Cerulean City Gym (if you want to fish there, why not) which are not yet included in grass_water.asm it's critical to order them correctly.

  • Somewhat unrelated, this is a good time to revisit surfing encounters via water_wildmons. In the base game, you won't find any surfing encounters outside of "SeaRoutes" like Route 19, 20, and 21. Seems silly that the ponds and rivers scattered over the rest of Kanto don't have surf encounters. As you go through and add fish encounter data for each map, you may also want to add more surfing encounters to those same bodies of water.


Step 5

Now, all we've done so far is create architecture for FishMons mirroring that of GrassMons and WaterMons, but we can't actually access yet. In order to make use of all this, we need to edit ReadSuperRodData: in item_effects.asm. You may notice ItemUseSuperRod: above in the same file, that routine only governs the player using the rod out of the bag and loading the wild battle. All of the encounter data is handled within ReadSuperRodData: so there's no need to change anything in ItemUseSuperRod: itself. Let's replace ReadSuperRodData:.

This new routine takes inspiration from wild_encounters.asm. It accomplishes two new things. One, instead of having the Super Rod do a 50/50 chance if we get a bite, it uses the new FishRate from the given map to determine how frequently we'll get a Pokemon on the hook. Two, instead of reading the fishing groups, it now pulls from the FishMons encounter table per map, using the same logic as GrassMons, WaterMons, and probabilities.asm.

ReadSuperRodData:
-   ; return e = 2 if no fish on this map
-   . . .
-   Delete Full Code
-   . . .
-   INCLUDE "data/wild/super_rod.asm"
+
+	ld a, [wFishRate]
+	and a
+	jr z, .NoFish
+
+; determine if there is a bite
+	call Random
+	ld b, a
+
+	ld a, [wFishRate]
+	cp b
+	jr c, .NoBite
+
+; determine encounter slot
+	call Random
+	ld b, a
+
+	ld hl, HardcodedFishEncounterSlotChances
+
+.determineEncounterSlot
+	ld a, [hli]
+	cp b
+	jr nc, .gotEncounterSlot
+	inc hl
+	jr .determineEncounterSlot
+
+.gotEncounterSlot
+	; HL points to slot offset byte
+	ld c, [hl]
+	ld b, 0
+
+	ld hl, wFishMons
+	add hl, bc
+
+	ld b, [hl]
+	inc hl
+	ld c, [hl]
+
+	ld e, 1
+	ret
+
+.NoBite
+	ld e, 0
+	ret
+
+.NoFish
+	ld e, 2
+	ret
+
+; hardcode table became necessary since calling regular WildMonEncounterSlotChances in probabilities.asm wouldn't start the battle
+HardcodedFishEncounterSlotChances:
+	db 51, 0
+	db 103, 2
+	db 129, 4
+	db 155, 6
+	db 181, 8
+	db 207, 10
+	db 220, 12
+	db 233, 14
+	db 244, 16
+	db 255, 18

Here's where my inexperience shows. I could not get the actual WildMonEncounterSlotChances in probabilities.asm to work in this routine. From my understanding, the data came through one byte off or was shifted one byte off either here or in ItemUseSuperRod:. This would typically cause no encounter to follow "Oh, a bite!", instead it would just return to a normal game state without launching into battle. I have not been able to find the correct workaround, but hardcoding the encounter table (and this allows you to either match the one in probabilities.asm or set your own rates unique to fishing) into the routine seems to work just fine.


Additional Considerations and Implications

  1. We have only overwritten the Super Rod code. This means the Old Rod and Good Rod can still use their original routines, the routines from Lotsob's Tutorial, or simply be removed from the game entirely.

  2. You'll need to consider what you want from a gameplay perspective. Personally, I removed the Old and Good Rods entirely and then renamed the Super Rod just "Fishing Rod" and made it available earlier in the game. If you do the same, the scripts and text for each of the fishing guru's need to be updated. I prefer this method to reduce redundant items, save bag space, shrink item_effects.asm, delete good_rod.asm and super_rod.asm and create operational symmetry with other encounters. All that said, I'm not sure if I've saved any space or not since each of the map files has grown.

Clone this wiki locally