Make wild Pokémon encounter levels vary - pret/pokecrystal GitHub Wiki
Pokémon Gold, Silver and Crystal use a slot-based encounter table, rolling against the current area's table to generate a wildmon for the player to fight.
You can only fit so many Pokémon in an area's wild encounter table, and specify the level of each slot. If you wanted to have variance in levels, you would have to use another slot for the same Pokémon at a different level, thus limiting the amount of unique wildmon in that location. (Example: a Lv. 3 Rattata and a Lv. 4 Rattata require two slots, one for each level).
This tutorial will teach you how to get around that and be able to use fewer slots (freeing up more space for more unique species!), via three different methods:
- Custom encounter table with level variation
- Custom probabilities and level ranges for each encounter table.
- Hijacking the surf variance code
Each has their benefits: the first method allows more fine-tuned control over the level variance, albeit a more invested change; the second one allows for even more control at the cost of space; finally, the last one is much simpler to implement across the game, requiring less setup and lacking more control over the levels.
I'd recommend starting with the easier method first, to understand how this works and what exactly it does, before moving on to a more complex implementation.
You decide what works best for you and your vision for your fangame!
Contents
- Method #1: Understanding Bug Catching Contest Encounter Code
- Method #2: Custom probabilities and level ranges for each encounter table
- Method #3: Hijack the surf variance code
1. Method #1: Understanding Bug Catching Contest Encounter Code
First, we look at a snippet used in engine/overworld/events.asm because we'll use a portion of the code to replace a portion of wild encounter code.
ChooseWildEncounter_BugContest::
; Pick a random mon out of ContestMons.
.loop
call Random
cp 100 << 1
jr nc, .loop
srl a
ld hl, ContestMons
ld de, 4
.CheckMon:
sub [hl]
jr c, .GotMon
add hl, de
jr .CheckMon
.GotMon:
inc hl
; Species
ld a, [hli]
ld [wTempWildMonSpecies], a
; Min level
ld a, [hli]
ld d, a
; Max level
ld a, [hl]
sub d
jr nz, .RandomLevel
; If min and max are the same.
ld a, d
jr .GotLevel
.RandomLevel:
; Get a random level between the min and max.
ld c, a
inc c
call Random
ldh a, [hRandomAdd]
call SimpleDivide
add d
.GotLevel:
ld [wCurPartyLevel], a
xor a
ret
This code is used to randomize wild encounters in the Bug Catching Contest. We then look at data/wild/bug_contest_mons.asm.
ContestMons:
; %, species, min, max
db 20, CATERPIE, 7, 18
db 20, WEEDLE, 7, 18
db 10, METAPOD, 9, 18
db 10, KAKUNA, 9, 18
db 5, BUTTERFREE, 12, 15
db 5, BEEDRILL, 12, 15
db 10, VENONAT, 10, 16
db 10, PARAS, 10, 17
db 5, SCYTHER, 13, 14
db 5, PINSIR, 13, 14
db -1, VENOMOTH, 30, 40
We can see that there's a minimum and maximum level for every wild Pokémon. The last slot, occupied by Venomoth, signals the end of the list. We can use a part of the algorithm of Bug Catching Contest encounters in engine/overworld/events.asm to randomize regular wild encounters.
1.1. Add max levels in probabilities
Edit data/wild/probabilities.asm.
...
mon_prob 100, 2 ; 10% chance
assert_table_length NUM_WATERMON
+MaxLevelGrass:
+ db 2
+ db 3
+ db 0
+ db 2
+ db 1
+ db 2
+ db 3
+
+MaxLevelWater:
+ db 2
+ db 3
+ db 4
Both MaxLevelGrass
and MaxLevelWater
contain the level buffs to obtain the highest-leveled encounter on that area. For example, if the first slot in the encounter table is Lv. 2, then the max level for the wild Pokémon will be Lv. 4.
1.2. Change wild encounter algorithm
Finally, we change the wild encounter algorithm in engine/overworld/wildmons.asm:
...
ld h, d
ld l, e
+ call CheckOnWater
+ ld de, MaxLevelWater
+ jr z, .prob_bracket_loop
+ ld de, MaxLevelGrass
; This next loop chooses which mon to load up.
.prob_bracket_loop
ld a, [hli]
cp b
jr nc, .got_it
inc hl
+ inc de
jr .prob_bracket_loop
.got_it
ld c, [hl]
ld b, 0
pop hl
add hl, bc ; this selects our mon
+; Min Level
ld a, [hli]
ld b, a
-; If the Pokemon is encountered by surfing, we need to give the levels some variety.
- call CheckOnWater
- jr nz, .ok
-; Check if we buff the wild mon, and by how much.
- call Random
- cp 35 percent
- jr c, .ok
- inc b
- cp 65 percent
- jr c, .ok
- inc b
- cp 85 percent
- jr c, .ok
- inc b
- cp 95 percent
- jr c, .ok
- inc b
+; Max Level
+ ld a, [de]
+; Min Level
+ ld d, b
+ ld b, a
+ and a
+ jr nz, .RandomLevel
+; If min and max are the same.
+ ld b, d
+ jr .ok
+
+.RandomLevel:
+; Get a random level between the min and max.
+ ld c, a
+ inc c
+ call Random
+ ldh a, [hRandomAdd]
+ call SimpleDivide
+ add d
+ ld b, a
+
; Store the level
.ok
ld a, b
And there you have it! Wild encounters have been randomized.
1.3. Other notes for method #1
Take note that the slots in MaxLevelGrass
and MaxLevelWater
in data/wild/probabilities.asm correspond to the probability slots in the same file. Also, each slot is tied to the wild Pokémon slots in each map where there are wild encounters. Thus if you use the tutorial to add a wild Pokémon slot, you also need to add new slots in MaxLevelGrass
and/or MaxLevelWater
.
Sometimes, you might want to create custom encounter tables. For example:
If you add wild Pokémon slots to accommodate all encounters in the morning, you'd have to use 19 slots because each level is placed in one slot. So for example, you need to use 3 slots for Abra, one for each level: Lv. 13, Lv. 14, and Lv. 15. After following this tutorial, you no longer need so many of the same Pokémon occupying multiple slots in the table, so extra slots can now mean more unique species in an area!
2. Method #2: custom probabilities and level ranges for each encounter table
Method #1 has some inconveniences, though: each slot still has a fixed probability and the same level variation. For example, the first slot of each encounter table will always have a 30% probability and will vary at most by 2 levels from the minumum. What if we wanted to have custom probabilities and individual level ranges for each slot for each encounter table? It's totally possible!
With the following method, you'll have even more control over these two aspects, albeit it'll take more space than method #1.
First, go and edit engine/overworld/wildmons.asm:
ChooseWildEncounter:
...
call CheckOnWater
- ld de, WaterMonProbTable
jr z, .watermon
inc hl
inc hl
ld a, [wTimeOfDay]
- ld bc, NUM_GRASSMON * 2
+ ld bc, NUM_GRASSMON * 4
call AddNTimes
- ld de, GrassMonProbTable
.watermon
-; hl contains the pointer to the wild mon data, let's save that to the stack
- push hl
.randomloop
call Random
cp 100
jr nc, .randomloop
- inc a ; 1 <= a <= 100
- ld b, a
- ld h, d
- ld l, e
+ ld de, 4
; This next loop chooses which mon to load up.
.prob_bracket_loop
- ld a, [hli]
- cp b
- jr nc, .got_it
- inc hl
+ sub [hl]
+ jr c, .got_it
+ add hl, de
jr .prob_bracket_loop
.got_it
- ld c, [hl]
- ld b, 0
- pop hl
- add hl, bc ; this selects our mon
- ld a, [hli]
- ld b, a
-; If the Pokemon is encountered by surfing, we need to give the levels some variety.
- call CheckOnWater
- jr nz, .ok
-; Check if we buff the wild mon, and by how much.
- call Random
- cp 35 percent
- jr c, .ok
- inc b
- cp 65 percent
- jr c, .ok
- inc b
- cp 85 percent
- jr c, .ok
- inc b
- cp 95 percent
- jr c, .ok
- inc b
-; Store the level
-.ok
- ld a, b
- ld [wCurPartyLevel], a
- ld b, [hl]
- ; ld a, b
+ inc hl
+ ld a, [hli]
+ ld b, a
call ValidateTempWildMonSpecies
jr c, .nowildbattle
- ld a, b ; This is in the wrong place.
cp UNOWN
- jr nz, .done
+ jr nz, .load_species
ld a, [wUnlockedUnowns]
and a
jr z, .nowildbattle
+ ld a, b
+.load_species
+ ld [wTempWildMonSpecies], a
+
+; Min level
+ ld a, [hli]
+ ld d, a
+
+; Max level
+ ld a, [hl]
+ sub d
+ jr nz, .RandomLevel
+
+; If min and max are the same.
+ ld a, d
+ jr .GotLevel
+
+.RandomLevel:
+; Get a random level between the min and max.
+ ld c, a
+ inc c
+ call Random
+ ldh a, [hRandomAdd]
+ call SimpleDivide
+ add d
+
+.GotLevel:
+ ld [wCurPartyLevel], a
+
+.startwildbattle
+ xor a
+ ret
-.done
- jr .loadwildmon
.nowildbattle
ld a, 1
and a
ret
-.loadwildmon
- ld a, b
- ld [wTempWildMonSpecies], a
-
-.startwildbattle
- xor a
- ret
-INCLUDE "data/wild/probabilities.asm"
Let's give a quick explanation. The code works similarly to ChooseWildEncounter_BugContest
, where each Pokémon slot contains the probability, the species, the minimum level and the maximum level. Also, since we're not going to use data/wild/probabilities.asm anymore, we can safely delete it.
You might have noticed that we did ld bc, NUM_GRASSMON * 4
at some point and it's because each slot will now contain four bytes (% chance, species, min. level and max. level) instead of two (level, species). We need to make similar changes in other places, too.
2.1. Adjust data related to Wildata constants
Let's edit data related to NUM_GRASSMON
and NUM_WATERMON
. In the same file:
FindNest:
...
.ScanMapLoop:
push af
ld a, [wNamedObjectIndex]
cp [hl]
jr z, .found
inc hl
inc hl
+ inc hl
+ inc hl
pop af
dec a
jr nz, .ScanMapLoop
and a
ret
RandomUnseenWildMon:
...
.GetGrassmon:
push hl
- ld bc, 5 + 4 * 2 ; Location of the level of the 5th wild Pokemon in that map
+ ld bc, 5 + 4 * 4 ; Location of the level of the 5th wild Pokemon in that map
add hl, bc
call GetTimeOfDayNotEve
- ld bc, NUM_GRASSMON * 2
+ ld bc, NUM_GRASSMON * 4
call AddNTimes
.randloop1
...
ld c, [hl] ; Contains the species index of this rare Pokemon
pop hl
- ld de, 5 + 0 * 2
+ ld de, 5 + 0 * 4
add hl, de
...
RandomPhoneWildMon:
...
.ok
- ld bc, 5 + 0 * 2
+ ld bc, 5 + 0 * 4
add hl, bc
call GetTimeOfDayNotEve
inc a
- ld bc, NUM_GRASSMON * 2
+ ld bc, NUM_GRASSMON * 4
.loop
...
Also in engine/pokegear/radio.asm:
OaksPKMNTalk4:
...
.loop2
call Random
maskbits NUM_DAYTIMES
cp EVE_F
jr z, .loop2
- ld bc, 2 * NUM_GRASSMON
+ ld bc, 4 * NUM_GRASSMON
call AddNTimes
Finally, edit constants/pokemon_data_constants.asm:
-DEF GRASS_WILDDATA_LENGTH EQU 2 + 3 + NUM_GRASSMON * 2 * 3
-DEF WATER_WILDDATA_LENGTH EQU 2 + 1 + NUM_WATERMON * 2
+DEF GRASS_WILDDATA_LENGTH EQU 2 + 3 + NUM_GRASSMON * 4 * 3
+DEF WATER_WILDDATA_LENGTH EQU 2 + 1 + NUM_WATERMON * 4
About this last edit, since the encounter tables now have more bytes the data length of each one is bigger, so we needed to adjust the related constants.
2.2. Edit the encounter tables
This is the most tedious part. You'll now have to edit each encounter table to match the new format. For example, this is how it'd look like for Sprout Tower 2F:
JohtoGrassWildMons:
def_grass_wildmons SPROUT_TOWER_2F
db 2 percent, 2 percent, 2 percent ; encounter rates: morn/day/nite
- ; morn
- db 3, RATTATA
- db 4, RATTATA
- db 5, RATTATA
- db 3, RATTATA
- db 6, RATTATA
- db 5, RATTATA
- db 5, RATTATA
- ; day
- db 3, RATTATA
- db 4, RATTATA
- db 5, RATTATA
- db 3, RATTATA
- db 6, RATTATA
- db 5, RATTATA
- db 5, RATTATA
- ; nite
- db 3, GASTLY
- db 4, GASTLY
- db 5, GASTLY
- db 3, RATTATA
- db 6, GASTLY
- db 5, RATTATA
- db 5, RATTATA
+ ; morn
+ ; %, species, min, max
+ db 30, RATTATA, 3, 6
+ db 30, RATTATA, 3, 6
+ db 20, RATTATA, 3, 6
+ db 10, RATTATA, 3, 6
+ db 5, RATTATA, 3, 6
+ db 4, RATTATA, 3, 6
+ db 1, RATTATA, 3, 6
+
+ ; day
+ ; %, species, min, max
+ db 30, RATTATA, 3, 6
+ db 30, RATTATA, 3, 6
+ db 20, RATTATA, 3, 6
+ db 10, RATTATA, 3, 6
+ db 5, RATTATA, 3, 6
+ db 4, RATTATA, 3, 6
+ db 1, RATTATA, 3, 6
+
+ ; nite
+ ; %, species, min, max
+ db 30, GASTLY, 3, 6
+ db 30, GASTLY, 3, 6
+ db 20, GASTLY, 3, 6
+ db 10, RATTATA, 3, 5
+ db 5, GASTLY, 3, 6
+ db 4, RATTATA, 3, 5
+ db 1, RATTATA, 3, 5
end_grass_wildmons
Note: You can change the percentages freely, as long as they add 100.
You have five places to edit: data/wild/johto_grass.asm, data/wild/kanto_grass.asm, data/wild/johto_water.asm, data/wild/kanto_water.asm and data/wild/swarm_grass.asm. Have fun.
2.3. Fix space issues
Congrats! You edited each encounter table and it probably took you some hours, but you have a new problem: one of your banks is now full and you can't assemble your ROM. An easy way to fix this is to put engine/overworld/wildmons.asm in a new section. Go to main.asm:
...
SECTION "bankA", ROMX
INCLUDE "engine/link/link.asm"
-INCLUDE "engine/overworld/wildmons.asm"
INCLUDE "engine/battle/link_result.asm"
...
+SECTION "Overworld Wildmon Data", ROMX
+
+INCLUDE "engine/overworld/wildmons.asm"
Now RGBDS (our development tool) will take care of it and put the new section where there's free space.
3. Method #3: Hijack the surf variance code
Method #1 made use of the surf variance code in a more complex way, allowing for more finer control over the tables, and method #2 went even further, making the code similar to how the Bug Contest handles wild encounters. The following method is a lot simpler to implement, and has a similar effect to method #1, but lacks that deeper control of the encounter levels.
Head on over to ChooseWildEncounter
in engine/overworld/wildmons.asm:
; If the Pokemon is encountered by surfing, we need to give the levels some variety.
call CheckOnWater
jr nz, .ok
; Check if we buff the wild mon, and by how much.
call Random
cp 35 percent
jr c, .ok
inc b
cp 65 percent
jr c, .ok
inc b
cp 85 percent
jr c, .ok
inc b
cp 95 percent
jr c, .ok
inc b
Let's break it down.
As you can see, there is level variance code right in the original game! It calls CheckOnWater
to see if player is currently surfing and if so, falls through to call Random
, which will randomly choose one of these percentages, and then add that percentage to b
, adding the rolled variance level to the level of the currently encountered surfing Pokémon inside b
.
The slots for surfing Pokémon are the minimum level for that slot, as is the logic for any encounter table slot. Through the use of Random
, however, surfing Pokémon have a chance to be encountered at levels higher than their set level in the encounter slot.
See below:
- +0: 35% chance
- +1: 30%
- +2: 20%
- +3: 10%
- +4: 5%
These probabilities are approximate, as an 8-bit register is limited in the range of values it can take!
Well, now we know what exactly the game does when you encounter a surfing Pokémon...
What happens if we just... remove that CheckOnWater
?
-; If the Pokemon is encountered by surfing, we need to give the levels some variety.
- call CheckOnWater
- jr nz, .ok
; Check if we buff the wild mon, and by how much.
call Random
cp 35 percent
jr c, .ok
inc b
cp 65 percent
jr c, .ok
inc b
cp 85 percent
jr c, .ok
inc b
cp 95 percent
jr c, .ok
inc b
Wow! Now any encountered wild Pokémon can vary up to 0-4 levels, in grass, in caves, in water... anywhere! That is all you need to do to achieve this functionality, but it also has some caveats. Anywhere does mean anywhere... Including Legendary Pokémon.
What if you don't want special wild encounters to have level variance?
Just load wBattleType
into a
, and check for the specific special encounter type before the first Random
call.
+; If the Pokemon is encountered in a special way, skip randomizing level.
+ ld a, [wBattleType]
+ cp BATTLETYPE_SUICUNE
+ jr z, .ok
; Check if we buff the wild mon, and by how much.
call Random
cp 35 percent
jr c, .ok
inc b
cp 65 percent
jr c, .ok
inc b
cp 85 percent
jr c, .ok
inc b
cp 95 percent
jr c, .ok
inc b
Now when the player encounters a wildmon, it will now check the wBattleType
and see if it is BATTLETYPE_SUICUNE
. If it is BATTLETYPE_SUICUNE
, then it will skip the random level variance and jump down to .ok
.
You can add multiple checks for different encounter types, or even specify special encounter types that DO make use of the level variance.
Armed with this new level variance technology, entering an unedited Route 29 during the day and rummaging through the grass, you could now encounter:
- Rattata: Levels 2-6
- Sentret: Levels 2-7
- Pidgey: Levels 2-7
- Hoppip: Levels 3-7
Whichever way you decide to implement level variance, it makes for a much more diverse experience exploring the overworld.