Skip to content
Grate Oracle Lewot edited this page Dec 28, 2024 · 2 revisions

Badge Level Caps

Part of making a Pokemon game competitive is removing the ability for players to over-level and brute force their way to a win. If you're interested in making level caps for player's Pokemon based on which badges they've obtained, this is your tutorial. Let's get started.

Table of Contents

  1. Badge Level Caps File
  2. Import the Badge Level Caps File
  3. Update Rare Candy Routine
  4. Update Battle Engine
  5. Update Daycare

Badge Level Caps File

First, we need to make a new file that will be the source of truth for our level caps. This file will store the level caps by badge and also allow retrieval via a subroutine. I made the file at: engine/pokemon/get_max_level.asm.

SECTION "Reserved Bytes", ROMX
ds 4  ; reserve 4 bytes

GetMaxLevel:
  ld hl, wKantoBadges

  bit EARTHBADGE, [hl]
  ld a, 8
  jr nz, .exit

  ld hl, wJohtoBadges

  bit RISINGBADGE, [hl]
  ld a, MAX_LEVEL + 1
  jr nz, .exit

  ; stormbadge
  bit STORMBADGE, [hl]
  ld a, 70
  jr nz, .exit

  ; fogbadge
  bit FOGBADGE, [hl]
  ld a, 50
  jr nz, .exit

  ; hivebadge
  bit HIVEBADGE, [hl]
  ld a, 10
  jr nz, .exit

  ; no badges
  ld a, 5

.exit
  ld b, a
  ret

This file does not contain all the badges at this point because it's up to the game designer which badges to include in this list. You can include all 16 badges here.

Here's how the routine works.

GetMaxLevel:
  ; omitted code

  bit HIVEBADGE, [hl]
  ld a, 10
  jr nz, .exit

  ; no badges
  ld a, 5

.exit
  ld b, a
  ret

In this code snippet, we check the bit for HIVEBADGE first. This is simply checking for 0 or 1, which means: is the badge flag enabled (badge received by player)? If it is, we jump to the .exit routine with 10 loaded into the a register.

If the HIVEBADGE wasn't obtained yet, the execution of the routine would go to ld a, 5 next, which would make the level cap 5, instead of 10 with the HIVEBADGE.

So, define the badge level caps in descending order (descending in terms of when the player receives them in the game). A hypothetical order would be EARTHBADGE, then VOLCANOBADGE, then the rest of the Kanto gyms, and then the Johto gyms.

Import the Badge Level Caps file

This is the simplest step. Go to the file main.asm and go down to the last SECTION titled "Crystal Events". Beneath that and its imports, make a new section and import the file.

+SECTION "Level Caps", ROMX
+
+INCLUDE "engine/pokemon/get_max_level.asm"
+
+
SECTION "Stadium 2 Checksums", ROMX[$7DE0], BANK[$7F]

Now we can call the subroutine GetMaxLevel from anywhere in our code using callfar GetMaxLevel.

Update Rare Candy Routine

Rare candy leveling up should respect badge level caps. To do so, we need to change the routine for how rare candies are used.

In the file engine/items/item_effects.asm go to the label RareCandyEffect. There, modify the code as follows:

RareCandyEffect:
	ld b, PARTYMENUACTION_HEALING_ITEM
	call UseItem_SelectMon
	jp c, RareCandy_StatBooster_ExitMenu
	call RareCandy_StatBooster_GetParameters
	ld a, MON_LEVEL
	call GetPartyParamLocation

+	push hl
+	farcall GetMaxLevel
+	pop hl

	ld a, [hl]
-	cp MAX_LEVEL
+	cp b
	jp nc, NoEffectMessage

Instead of comparing to the constant MAX_LEVEL (which is 100) we are using GetMaxLevel to get the current level cap for the latest badge the player obtained.

Update Battle Engine

This is the most complicated part of the tutorial. Most leveling up occurs during battle and we need to make a lot of edits to the battle engine to respect dynamic level caps.

Go to the file engine/battle/core.asm. Make the following changes:

@@ -6973,6 +6973,18 @@ GiveExperiencePoints:
 	inc de
 	dec c
 	jr nz, .stat_exp_loop
+	pop bc
+	ld hl, MON_LEVEL
+	add hl, bc
+	push bc
+	push hl
+	callfar GetMaxLevel
+	pop hl
+	ld a, [hl]
+	cp b
+	pop bc
+	jp nc, .next_mon
+	push bc
 	xor a
 	ldh [hMultiplicand + 0], a
 	ldh [hMultiplicand + 1], a
@@ -7062,7 +7074,8 @@ GiveExperiencePoints:
 	ld [wCurSpecies], a
 	call GetBaseData
 	push bc
-	ld d, MAX_LEVEL
+	callfar GetMaxLevel
+	ld d, b
 	callfar CalcExpAtLevel
 	pop bc
 	ld hl, MON_EXP + 2
@@ -7089,8 +7102,9 @@ GiveExperiencePoints:
 	ld [hld], a
 
 .not_max_exp
-; Check if the mon leveled up
-	xor a ; PARTYMON
+	call GetMaxLevel
+	ld e, b
+	xor a
 	ld [wMonType], a
 	predef CopyMonToTempMon
 	callfar CalcLevel
@@ -7098,7 +7112,7 @@ GiveExperiencePoints:
 	ld hl, MON_LEVEL
 	add hl, bc
 	ld a, [hl]
-	cp MAX_LEVEL
+	cp e
 	jp nc, .next_mon
 	cp d
 	jp z, .next_mon
@@ -7268,12 +7282,33 @@ GiveExperiencePoints:
 	ld a, [wBattleParticipantsNotFainted]
 	ld b, a
 	ld c, PARTY_LENGTH
-	ld d, 0
+	ld de, 0
 .count_loop
+	push bc
+	push de
+	callfar GetMaxLevel
+	ld a, b
+	ld [wTempByteValue], a
+	ld a, e
+	ld hl, wPartyMon1Level
+	call GetPartyLocation
+	ld a, [wTempByteValue]
+	ld b, a
+	ld a, [hl]
+	cp b
+	pop de
+	pop bc
+	jr c, .gains_exp
+	srl b
+	ld a, d
+	jr .no_exp
+.gains_exp
 	xor a
 	srl b
 	adc d
 	ld d, a
+.no_exp
+	inc e
 	dec c
 	jr nz, .count_loop
 	cp 2
@@ -7339,13 +7374,16 @@ ExpPointsText:
 AnimateExpBar:
 	push bc
 
+	callfar GetMaxLevel
+	ld e, b
+
 	ld hl, wCurPartyMon
 	ld a, [wCurBattleMon]
 	cp [hl]
 	jp nz, .finish
 
 	ld a, [wBattleMonLevel]
-	cp MAX_LEVEL
+	cp e
 	jp nc, .finish
 
 	ldh a, [hProduct + 3]
@@ -7382,7 +7420,10 @@ AnimateExpBar:
 	ld [hl], a
 
 .NoOverflow:
-	ld d, MAX_LEVEL
+	callfar GetMaxLevel
+	ld d, b
+	pop bc
+	push bc
 	callfar CalcExpAtLevel
 	ldh a, [hProduct + 1]
 	ld b, a
@@ -7417,8 +7458,17 @@ AnimateExpBar:
 	ld d, a
 
 .LoopLevels:
+	push bc
+	callfar GetMaxLevel
+	ld a, b
+	ld [wTempByteValue], a
+	pop bc
+
+	ld a, [wTempByteValue]
+	ld l, a
+
 	ld a, e
-	cp MAX_LEVEL
+	cp l
 	jr nc, .FinishExpBar
 	cp d
 	jr z, .FinishExpBar

The essential idea of these changes is to replace the constant MAX_LEVEL with the dynamic value from GetMaxLevel. We have to make a bunch of changes here to ensure that we don't overwrite any of the registers used for other parts of the battle routine.

If you are encountering errors here, you probably mistyped something and a register is being overwritten that shouldn't be. Double check that your diff matches the diff in this tutorial.

Update Daycare

The daycare is an easy update. We are just changing the MAX_LEVEL check to be our dynamic value returned from GetMaxLevel.

Go to the file engine/events/happiness_egg.asm. Match the following diff:

@@ -146,8 +146,9 @@ DayCareStep::
 	bit DAYCAREMAN_HAS_MON_F, a
 	jr z, .day_care_lady
 
+	callfar GetMaxLevel
 	ld a, [wBreedMon1Level] ; level
-	cp MAX_LEVEL
+	cp b
 	jr nc, .day_care_lady
 	ld hl, wBreedMon1Exp + 2 ; exp
 	inc [hl]
@@ -168,8 +169,9 @@ DayCareStep::
 	bit DAYCARELADY_HAS_MON_F, a
 	jr z, .check_egg
 
+	callfar GetMaxLevel
 	ld a, [wBreedMon2Level] ; level
-	cp MAX_LEVEL
+	cp b
 	jr nc, .check_egg
 	ld hl, wBreedMon2Exp + 2 ; exp
 	inc [hl]

That's it! Let me know if you have any questions.

Clone this wiki locally