Skip to content
Hentenmon edited this page Aug 7, 2024 · 10 revisions

Today we are going to do a tutorial of how to add a hard mode to the game. This means that we will force set mode, disable the use of items in battle, and add a hard level cap to the game, if the player chooses the mode.

Contents

1. Define difficulty

To start, we define the difficulty in ram/wram.asm. This will ensure that our scripts will be understood.

...
wRoute18Gate1FCurScript:: db
-        ds 78
+	; ds 78
wGameProgressFlagsEnd::

+wDifficulty::
+	; $00 = normal
+	; $01 = hard
+		ds 1
...

Let's also define wGameStage for checking if it's post game or not, as level caps will be disabled post game.

...
wFirstLockTrashCanIndex:: db
wSecondLockTrashCanIndex:: db

-	ds 2
+       ds 1
	
+wGameStage:: db
+	; $00 = before champion fight
+	; $01 = post game
...

Lastly, let's define wMaxDaycareLevel to prevent the player from overleveling in the daycare.

...
; the address of the menu cursor's current location within wTileMap
wMenuCursorLocation:: dw

+; index in party of currently battling mon
+wMaxDaycareLevel:: db

+	ds 1
...

2. Force Set Mode

We will force the player to play on Set Mode when on Hard Mode. Open engine/battle/core.asm and make the following changes.

...
        cp LINK_STATE_BATTLING
        jr z, .next4
+       ld a, [wDifficulty] ; Check if player is on hard mode
+       and a
+       jr z, .DontForceSetMode
-       jr z, .next4 
+       jr .next4 ; skip switch request if on hard mode
+ .DontForceSetMode
        ld a, [wOptions]
	bit BIT_BATTLE_SHIFT, a
...

3. No items in battle

We'll prevent the player from using items in battle, and it will say in atrainer battle "Items can't be used right now."To do this, stay in engine/battle/core.asm and make the following changes:

...
; normal battle
	call DrawHUDsAndHPBars
.next
	ld a, [wBattleType]
-	dec a ; is it the old man tutorial?
+       cp BATTLE_TYPE_OLD_MAN ; is it the old man battle?
+       jr z, .simulatedInputBattle

+       ld a, [wDifficulty] ; Check if player is on hard mode
+	and a
+	jr z, .NormalMode

+	ld a, [wIsInBattle] ; Check if this is a wild battle or trainer battle
+	dec a
+	jr z, .NormalMode ; Not a trainer battle

+	ld hl, ItemsCantBeUsedHereText ; items can't be used during trainer battles in hard mode
+	call PrintText
+	jp DisplayBattleMenu
+.NormalMode
	jr DisplayPlayerBag ; no, it is a normal battle
+.simulatedInputBattle
	ld hl, OldManItemList
	ld a, l
	ld [wListPointer], a
	ld a, h
	ld [wListPointer + 1], a
	jr DisplayBagMenu

OldManItemList:
	db 1 ; # items
+       ; optional: changes the number of poke balls from 50 to 1 to maintain logic
-	db POKE_BALL, 50
+       db POKE_BALL, 1
	db -1 ; end
...

NOTE: Use make after this change to check, do you get the the "jr target out of reach" error on line 2169? If you do, follow this and if no, skip this.

...
.throwSafariBallWasSelected
	ld a, SAFARI_BALL
	ld [wCurItem], a
-	jr UseBagItem
+       jp UseBagItem
...

I got this error while making this, and if you get the error here's how to fix it.

The last one is optional, but I made the old man only have 1 Poke ball for logical consistency. You can keep it at 50 if you want to.

4. Gym Level Caps

We will level cap the gyms so that you can't overlevel past them with a Rare Candy, Gaining Exp via battles, or Daycare. Start by staying in engine/battle/core.asm, to make gaining EXP via battles impossible.

...
    ld a, [wPlayerID]
    cp [hl]
    jr nz, .monIsTraded

+       ld a, [wDifficulty] ; Check if player is on hard mode
+	and a
+	jr z, .NormalMode2
+; what level might disobey?
+	ld a, [wGameStage] ; Check if player has beat the game
+	and a
+	ld a, 101
+	jr nz, .next
+	farcall GetBadgesObtained
+	ld a, [wNumSetBits]
+	cp 8
+	ld a, 65 ; Venusaur/Charizard/Blastoise's level
+	jr nc, .next
+	cp 7
+	ld a, 50 ; Rhydon's level
+	jr nc, .next
+	cp 6
+	ld a, 47 ; Arcanine's level
+	jr nc, .next
+	cp 5
+	ld a, 43 ; Alakazam's level
+	jr nc, .next
+       cp 4
+	ld a, 43 ; Weezing's level
+	jr nc, .next
+	cp 3
+	ld a, 29 ; Vileplume's level
+	jr nc, .next
+	cp 2
+       ld a, 24 ; Raichu's level
+	jr nc, .next
+	cp 1
+	ld a, 21 ; Starmie's level
+	jr nc, .next
+	ld a, 14 ; Onix's level
+	jp .next
+.NormalMode2
+	inc hl
+	ld a, [wPlayerID + 1]
+	cp [hl]
+	jp z, .canUseMove ; on normal mode non traded pokemon will always obey
+	; it was traded
...

Open engine/battle/experience.asm and add the following lines. This will reset gained EXP back to 0 if on hard mode and above the level cap

...
ld [wd0b5], a
	call GetMonHeader
	ld d, MAX_LEVEL

+       ld a, [wDifficulty] ; Check if player is on hard mode
+	and a
+	jr z, .next1 ; no level caps if not on hard mode

+	ld a, [wGameStage] ; Check if player has beat the game
+	and a
+	ld d, 100
+	jr nz, .next1
+	call GetBadgesObtained
+	ld a, [wNumSetBits]
+	cp 8
+	ld d, 65 ; Venusaur/Charizard/Blastoise's level
+	jr nc, .next1
+	cp 7
+	ld d, 50 ; Rhydon's level
+	jr nc, .next1
+	cp 6
+	ld d, 47 ; Arcanine's level
+	jr nc, .next1
+	cp 5
+	ld d, 43 ; Alakazam's level
+	jr nc, .next1
+       cp 4
+	ld d, 43 ; Weezing's level
+	jr nc, .next1
+	cp 3
+	ld d, 29 ; Vileplume's level
+	jr nc, .next1
+	cp 2
+       ld d, 24 ; Raichu's level
+	jr nc, .next1
+	cp 1
+	ld d, 21 ; Starmie's level
+	jr nc, .next1
+	ld d, 14 ; Onix's level
+.next1
	callfar CalcExperience ; get max exp

...

At the end of experience.asm add these lines to make the function GetBadgesObtained.

...
+; function to count the set bits in wObtainedBadges
+; OUTPUT:
+; a = set bits in wObtainedBadges
+GetBadgesObtained::
+	push hl
+	push bc
+	push de
+	ld hl, wObtainedBadges
+	ld b, $1
+	call CountSetBits
+	pop de
+	pop bc
+	pop hl
+	ld a, [wNumSetBits]
+	ret
...

Now go to engine/items/item_effects.asm to make the Rare Candy not work on Pokémon that are above the level cap.

...
.useRareCandy
	push hl
	ld bc, wPartyMon1Level - wPartyMon1
	add hl, bc ; hl now points to level
	ld a, [hl] ; a = level
-	cp MAX_LEVEL
+       ld b, MAX_LEVEL

+	ld a, [wDifficulty] ; Check if player is on hard mode
+	and a
+	jr z, .next1 ; no level caps if not on hard mode
+
+	ld a, [wGameStage] ; Check if player has beat the game
+	and a
+	jr nz, .next1
+	farcall GetBadgesObtained
+	ld a, [wNumSetBits]
+	cp 8
+	ld b, 65 ; Venusaur/Charizard/Blastoise's level
+	jr nc, .next1
+	cp 7
+	ld b, 50 ; Rhydon's level
+	jr nc, .next1
+	cp 6
+	ld b, 47 ; Arcanine's level
+	jr nc, .next1
+	cp 5
+	ld b, 43 ; Alakazam's level
+	jr nc, .next1
+       cp 4
+	ld b, 43 ; Weezing's level
+	jr nc, .next1
+	cp 3
+	ld b, 29 ; Vileplume's level
+	jr nc, .next1
+	cp 2
+       ld b, 24 ; Raichu's level
+	jr nc, .next1
+	cp 1
+	ld b, 21 ; Starmie's level
+	jr nc, .next1
+	ld b, 14 ; Onix's level
+.next1
+
+	pop hl
+	ld a, [hl] ; a = level
+	cp b ; MAX_LEVEL on normal mode, level cap on hard mode
        jr z, .vitaminNoEffect ; can't raise level above 100
	inc a
	ld [hl], a ; store incremented level

...

Now for the daycare. Open scripts/Daycare.asm and make the following edits. This will make the daycare unable to level up your pokemon past the level cap.

...

.daycareInUse
	xor a
	ld hl, wDayCareMonName
	call GetPartyMonName
	ld a, DAYCARE_DATA
	ld [wMonDataLocation], a
	call LoadMonData
	callfar CalcLevelFromExperience
-	ld a, d
-	cp MAX_LEVEL
-	jr c, .skipCalcExp
- ld d, MAX_LEVEL
-	callfar CalcExperience
-	ld hl, wDayCareMonExp
-	ldh a, [hExperience]
-	ld [hli], a
-	ldh a, [hExperience + 1]
-	ld [hli], a
-	ldh a, [hExperience + 2]
-	ld [hl], a
-	ld d, MAX_LEVEL

+	push bc
+	ld a, [wDifficulty] ; Check if player is on hard mode
+	and a
+	ld b, MAX_LEVEL
+	jr z, .next1 ; no level caps if not on hard mode
+
+	ld a, [wGameStage] ; Check if player has beat the game
+	and a
+	jr nz, .next1
+	farcall GetBadgesObtained
+	ld a, [wNumSetBits]
+	cp 8
+	ld b, 65 ; Venusaur/Charizard/Blastoise's level
+	jr nc, .next1
+	cp 7
+	ld b, 50 ; Rhydon's level
+	jr nc, .next1
+	cp 6
+	ld b, 47 ; Arcanine's level
+	jr nc, .next1
+	cp 5
+	ld b, 43 ; Alakazam's level
+	jr nc, .next1
+       cp 4
+	ld b, 43 ; Weezing's level
+	jr nc, .next1
+	cp 3
+	ld b, 29 ; Vileplume's level
+	jr nc, .next1
+	cp 2
+       ld b, 24 ; Raichu's level
+	jr nc, .next1
+	cp 1
+	ld b, 21 ; Starmie's level
+	jr nc, .next1
+	ld b, 14 ; Onix's level
+.next1
+	ld a, b
+	ld [wMaxDaycareLevel], a
+	ld a, d
+	cp b
+	pop bc
+	jr c, .skipCalcExp
+
+	ld a, [wMaxDaycareLevel]
+	ld d, a
+	callfar CalcExperience
+	ld hl, wDayCareMonExp
+	ldh a, [hExperience]
+	ld [hli], a
+	ldh a, [hExperience + 1]
+	ld [hli], a
+	ldh a, [hExperience + 2]
+	ld [hl], a
+	ld a, [wMaxDaycareLevel]
+	ld d, a

...

5. Difficulty menu at start

We will ask the player the difficulty to give them a choice between Normal and Hard Mode. Open constants/menu_constants.asm and add these lines to make a constant for the difficulty selection menu:

...

        const TRADE_CANCEL_MENU ; 5
	const HEAL_CANCEL_MENU  ; 6
	const NO_YES_MENU       ; 7
+	const DIFFICULTY_SELECTION_MENU ; 8

...

Now go to data/yes_no_menu_strings.asm and add this line to make the menu. (7, 3) means the size of the window, and FALSE means that the first option will be false.

...
        two_option_menu 7, 4, TRUE,  .HealCancelMenu
	two_option_menu 4, 3, FALSE, .NoYesMenu
+	two_option_menu 7, 3, FALSE, .DifficultyMenu
...

And at the end, add this line:

...
.HealCancelMenu:
	db   "HEAL"
	next "CANCEL@"

+.DifficultyMenu:
+	db   "NORMAL"
+	next "HARD@"
...

In data/text/text_2.asm, add these at the end to create selecting text and adding "Are you sure?" to make sure they don't make it accidentally:

...
+_DifficultyText::
+   text "Select Difficulty"
+   done

+_AreYouSureText::
+	text "Are you sure?"
+	done
...

In data/text/text_3.asm, add these to describe the difficulties after choice:

...
+_NormalModeText::
+	text "Are you sure?"

+	para "Classic #MON"
+	line "rules."
+	done

+_HardModeText::
+	text "Are you sure?"
	
+	para "Set mode, no"
+	line "items in battle,"
+	cont "gym level caps."
+	done
...

Finally, let's open engine/movie/oak_speech/oak_speech.asm and make the following changes. This will activate the difficulty menu when starting a new file.

...
        ld a, [wStatusFlags6]
	bit BIT_DEBUG_MODE, a
	jp nz, .skipSpeech
+.MenuCursorLoop ; difficulty menu
+	ld hl, DifficultyText
+ 	call PrintText
+ 	call DifficultyChoice
+	ld a, [wCurrentMenuItem]
+	ld [wDifficulty], a
+	cp 0 ; normal
+	jr z, .SelectedNormalMode
+	cp 1 ; hard
+	jr z, .SelectedHardMode
+	; space for more game modes down the line
+.SelectedNormalMode
+	ld hl, NormalModeText
+	call PrintText
+	jp .YesNoNormalHard
+.SelectedHardMode
+	ld hl, HardModeText
+	call PrintText
+.YesNoNormalHard ; Give the player a brief description of each game mode and make sure that's what they want
+  	call YesNoNormalHardChoice
+	ld a, [wCurrentMenuItem]
+	cp 0
+	jr z, .doneLoop
+	jp .MenuCursorLoop ; If player says no, back to difficulty selection
+.doneLoop
+   	call ClearScreen ; clear the screen before resuming normal intro
...

Go a little bit more down and add these to import the text from the text files:

...

+NormalModeText:
+	text_far _NormalModeText
+	text_end
+HardModeText:
+	text_far _HardModeText
+	text_end
+DifficultyText:
+	text_far _DifficultyText
+	text_end
+YesNoNormalHardText:
+	text_far _AreYouSureText
+	text_end
...

Finally, add these at the bottom to make the functions from the first script we did to this file:

...

+; displays difficulty choice
+DifficultyChoice::
+	call SaveScreenTilesToBuffer1
+	call InitDifficultyTextBoxParameters
+	jr DisplayDifficultyChoice
+
+InitDifficultyTextBoxParameters::
+  	ld a, $8 ; loads the value for the difficulty menu
+	ld [wTwoOptionMenuID], a
+	coord hl, 5, 5
+	ld bc, $606 ; Cursor Pos
+	ret
+	
+DisplayDifficultyChoice::
+	ld a, $14
+	ld [wTextBoxID], a
+	call DisplayTextBoxID
+	jp LoadScreenTilesFromBuffer1
+
+; display yes/no choice
+YesNoNormalHardChoice::
+	call SaveScreenTilesToBuffer1
+	call InitYesNoNormalHardTextBoxParameters
+	jr DisplayYesNoNormalHardChoice
+
+InitYesNoNormalHardTextBoxParameters::
+  	ld a, $0 ; loads the value for the difficulty menu
+	ld [wTwoOptionMenuID], a
+	coord hl, 7, 5
+	ld bc, $608 ; Cursor Pos
+	ret
+	
+DisplayYesNoNormalHardChoice::
+	ld a, $14
+	ld [wTextBoxID], a
+	call DisplayTextBoxID
+	jp LoadScreenTilesFromBuffer1

...

If you've done this, feel free to update. Thanks for seeing this and hope it works for you.

Clone this wiki locally