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:

  1. Custom encounter table with level variation
  2. Custom probabilities and level ranges for each encounter table.
  3. 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

  1. Method #1: Understanding Bug Catching Contest Encounter Code
    1. Add max levels in probabilities
    2. Change wild encounter algorithm
    3. Other notes for Method 1
  2. Method #2: Custom probabilities and level ranges for each encounter table
    1. Adjust data related to wildata constants
    2. Edit the encounter tables
    3. Fix space issues
  3. 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:

Custom Encounter Rates

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.