Adding hard mode - pret/pokered GitHub Wiki

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
wGameProgressFlagsEnd::

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

-	ds 56
+	ds 55

wObtainedHiddenItemsFlags:: flag_array MAX_HIDDEN_ITEMS
...

Next, let's define wMaxLevel to store the value of the level cap through far calls.

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

+wMaxLevel:: db

-	ds 2
+	ds 1
...

Let's also define a constant in constants/event_constants.asm for checking if it's post game or not, as level caps will be disabled post game.

...
	const EVENT_BEAT_ARTICUNO
	
+; Post Game events
+	const_next $9E0
+	const EVENT_PLAYER_IS_CHAMPION

; End of events
	const_next $A00
DEF NUM_EVENTS EQU const_value

We need to set an appropriate place for this event to trigger. Head over to scripts/HallOfFame.asm and add the following near the bottom.

...
	ld a, HS_CERULEAN_CAVE_GUY
	ld [wMissableObjectIndex], a
	predef HideObject
+	SetEvent EVENT_PLAYER_IS_CHAMPION
	ld a, SCRIPT_HALLOFFAME_RESET_EVENTS_AND_SAVE
...

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 nz, .next4 ; if hard mode is enabled (nz), skip switch request
        ld a, [wOptions]
	bit BIT_BATTLE_SHIFT, a
...

We can prevent the toggle for set and switch from being moved in the options menu by making some edits to engine/menus/main_menu.asm

Prevent the cursor from moving:

...
.cursorInBattleStyle
+	ld a, [wDifficulty]
+	and a
+	jr nz, .lockedToSet
	ld a, [wOptionsBattleStyleCursorX] ; battle style cursor X coordinate
	xor 1 ^ 10 ; toggle between 1 and 10
	ld [wOptionsBattleStyleCursorX], a
	jp .eraseOldMenuCursor
+.lockedToSet
+	ld a, 10 ; SET mode cursor position
+	ld [wOptionsBattleStyleCursorX], a
+	jp .eraseOldMenuCursor
.pressedLeftInTextSpeed
...

Ensure the correct bit is set:

...
.battleStyleShift
	res BIT_BATTLE_SHIFT, d
+	ld a, [wDifficulty]
+	and a
+	jr nz, .battleStyleSet
.storeOptions
...

Ensure the correct starting position of the arrow:

...
.storeBattleAnimationCursorX
	ld [wOptionsBattleAnimCursorX], a ; battle animation cursor X coordinate
	hlcoord 0, 8
	call .placeUnfilledRightArrow
	sla c
+	ld a, [wDifficulty]
+	and a
+	ld a, 10
+	jr nz, .storeBattleStyleCursorX
	ld a, 1
	jr nc, .storeBattleStyleCursorX
	ld a, 10
.storeBattleStyleCursorX
...

2A. Force Set Mode- Yellow Version

To start, we look in another location for Options menu for Yellow Version. Proceed into engine/menus/options and make the following changes

...
OptionsMenu_BattleStyle:
	ldh a, [hJoy5]
	and PAD_LEFT | PAD_RIGHT
	jr nz, .asm_41d6b
+	ld a, [wDifficulty]
+       and a
+       jr nz, .lockedToSet
	ld a, [wOptions]
	and $40 ; mask other bits
	jr .asm_41d73
.asm_41d6b
+       ld a, [wDifficulty]
+       and a
+       jr nz, .lockedToSet
	ld a, [wOptions]
	xor $40
	ld [wOptions], a
+       jr .asm_41d73
+.lockedToSet
+       ld a, [wOptions]
+       or $40
+       ld [wOptions], a
.asm_41d73
	ld bc, $0
	sla a
	sla a
	rl c
	ld hl, BattleStyleOptionStringsPointerTable
	add hl, bc
	add hl, bc
	ld e, [hl]
	inc hl
	ld d, [hl]
	hlcoord 14, 6
	call PlaceString
	and a
	ret
...

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.

3b. Restrict Items in battle

If you dont want to remove all items in battle you can restrict certain items by taking advantage of fall throughs built into the code of engine/items/Item_Effects.asm

To start we will need to create an alternate list of Item Use Pointers. Please note how this list differs from the original just above it. Items that are set as ItemUseMedicine can be restricted from use in battle by setting them as ItemUseVitamin. This is because ItemUseVitamin only checks if the user is in a battle. If the player is in a battle, it will give a message about it not being the right time to use that item, else if the player is not in a battle, it will fall through to ItemUseMedicine and the items normal effect will be loaded. A similar relationship is found between ItemUsePPRestore and ItemUsePPUp, with the latter being restricted in battle otherwise falling through to the former.

...
	dw ItemUsePPRestore  ; MAX_ELIXER

ItemUsePtrTableHard:
; entries correspond to item ids
+	dw ItemUseBall       ; MASTER_BALL
+	dw ItemUseBall       ; ULTRA_BALL
+	dw ItemUseBall       ; GREAT_BALL
+	dw ItemUseBall       ; POKE_BALL
+	dw ItemUseTownMap    ; TOWN_MAP
+	dw ItemUseBicycle    ; BICYCLE
+	dw ItemUseSurfboard  ; SURFBOARD
+	dw ItemUseBall       ; SAFARI_BALL
+	dw ItemUsePokedex    ; POKEDEX
+	dw ItemUseEvoStone   ; MOON_STONE
+	dw ItemUseMedicine   ; ANTIDOTE
+	dw ItemUseMedicine   ; BURN_HEAL
+	dw ItemUseMedicine   ; ICE_HEAL
+	dw ItemUseMedicine   ; AWAKENING
+	dw ItemUseMedicine   ; PARLYZ_HEAL
+	dw ItemUseMedicine   ; FULL_RESTORE
+	dw ItemUseMedicine   ; MAX_POTION
+	dw ItemUseMedicine   ; HYPER_POTION
+	dw ItemUseMedicine   ; SUPER_POTION
+	dw ItemUseMedicine   ; POTION
+	dw ItemUseBait       ; BOULDERBADGE
+	dw ItemUseRock       ; CASCADEBADGE
+	dw UnusableItem      ; THUNDERBADGE
+	dw UnusableItem      ; RAINBOWBADGE
+	dw UnusableItem      ; SOULBADGE
+	dw UnusableItem      ; MARSHBADGE
+	dw UnusableItem      ; VOLCANOBADGE
+	dw UnusableItem      ; EARTHBADGE
+	dw ItemUseEscapeRope ; ESCAPE_ROPE
+	dw ItemUseRepel      ; REPEL
+	dw UnusableItem      ; OLD_AMBER
+	dw ItemUseEvoStone   ; FIRE_STONE
+	dw ItemUseEvoStone   ; THUNDER_STONE
+	dw ItemUseEvoStone   ; WATER_STONE
+	dw ItemUseVitamin    ; HP_UP
+	dw ItemUseVitamin    ; PROTEIN
+	dw ItemUseVitamin    ; IRON
+	dw ItemUseVitamin    ; CARBOS
+	dw ItemUseVitamin    ; CALCIUM
+	dw ItemUseVitamin    ; RARE_CANDY
+	dw UnusableItem      ; DOME_FOSSIL
+	dw UnusableItem      ; HELIX_FOSSIL
+	dw UnusableItem      ; SECRET_KEY
+	dw UnusableItem      ; ITEM_2C
+	dw UnusableItem      ; BIKE_VOUCHER
+	dw ItemUseXAccuracy  ; X_ACCURACY
+	dw ItemUseEvoStone   ; LEAF_STONE
+	dw ItemUseCardKey    ; CARD_KEY
+	dw UnusableItem      ; NUGGET
+	dw UnusableItem      ; ITEM_32
+	dw ItemUsePokeDoll   ; POKE_DOLL
+	dw ItemUseMedicine   ; FULL_HEAL
+	dw ItemUseVitamin    ; REVIVE
+	dw ItemUseVitamin    ; MAX_REVIVE
+	dw ItemUseGuardSpec  ; GUARD_SPEC
+	dw ItemUseSuperRepel ; SUPER_REPEL
+	dw ItemUseMaxRepel   ; MAX_REPEL
+	dw ItemUseDireHit    ; DIRE_HIT
+	dw UnusableItem      ; COIN
+	dw ItemUseVitamin    ; FRESH_WATER
+	dw ItemUseVitamin    ; SODA_POP
+	dw ItemUseVitamin    ; LEMONADE
+	dw UnusableItem      ; S_S_TICKET
+	dw UnusableItem      ; GOLD_TEETH
+	dw ItemUseXStat      ; X_ATTACK
+	dw ItemUseXStat      ; X_DEFEND
+	dw ItemUseXStat      ; X_SPEED
+	dw ItemUseXStat      ; X_SPECIAL
+	dw ItemUseCoinCase   ; COIN_CASE
+	dw ItemUseOaksParcel ; OAKS_PARCEL
+	dw ItemUseItemfinder ; ITEMFINDER
+	dw UnusableItem      ; SILPH_SCOPE
+	dw ItemUsePokeFlute  ; POKE_FLUTE
+	dw UnusableItem      ; LIFT_KEY
+	dw UnusableItem      ; EXP_ALL
+	dw ItemUseOldRod     ; OLD_ROD
+	dw ItemUseGoodRod    ; GOOD_ROD
+	dw ItemUseSuperRod   ; SUPER_ROD
+	dw ItemUsePPUp       ; PP_UP
+	dw ItemUsePPUp       ; ETHER
+	dw ItemUsePPUp       ; MAX_ETHER
+	dw ItemUsePPUp       ; ELIXER
+	dw ItemUsePPUp       ; MAX_ELIXER

ItemUseBall:
...

Now we will need some logic to load the correct list based on the game's difficulty setting. Look further up in the same file and make the following edit.

...
UseItem_::
	ld a, 1
	ld [wActionResultOrTookBattleTurn], a ; initialise to success value
	ld a, [wCurItem]
	cp HM01
	jp nc, ItemUseTMHM
+	ld a, [wDifficulty]
+	and a
+	ld hl, ItemUsePtrTableHard
+	jr nz, .hardItems
	ld hl, ItemUsePtrTable
+.hardItems
	ld a, [wCurItem]
	dec a
	add a
...

Now certain items will need a more nuanced approach for in-battle restriction. Let's use Poke-Flute as an example. Unlimited Awakens the item isn't very hard mode, is it? Search further down in the same file for ItemUsePokeFlute:.

...
UseItem_::
ItemUsePokeFlute:
	ld a, [wIsInBattle]
	and a
	jr nz, .inBattle
; if not in battle
	call ItemUseReloadOverworldData
	ld a, [wCurMap]
...

Here we see that the item already checks if it is being used in battle. We can use that. Look lower for .inBattle and add the following.

...
	jp PrintText
.inBattle
+	ld a, [wDifficulty]
+	and a
+	jp nz, ItemUseNotTime
	xor a
	ld [wWereAnyMonsAsleep], a
...

This will restrict the use of the PokeFlute in battle, but only in hard mode.

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 in 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 to make gaining EXP via battles impossible.

...
	ld [wCurSpecies], 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
+	call GetLevelCap
+	ld a, [wMaxLevel]
+	ld d, a
+.next1
	callfar CalcExperience ; get max exp

...

At the end of experience.asm add these lines to make the functions GetBadgesObtained and GetLevelCap.

...
+; function to count the set bits in wObtainedBadges
+; returns the number of badges in wNumSetBits
+GetBadgesObtained::
+	push de
+	ld hl, wObtainedBadges
+	ld b, $1
+	call CountSetBits
+	pop de
+	ret

+; returns the level cap in wMaxLevel
+GetLevelCap::	
+	CheckEvent EVENT_PLAYER_IS_CHAMPION
+	ld a, 100
+	jr nz, .storeValue
+	call GetBadgesObtained
+	ld a, [wNumSetBits]
+	ld hl, BadgeLevelRestrictions
+	ld b, 0
+	ld c, a
+	add hl, bc
+	ld a, [hl]
+.storeValue
+	ld [wMaxLevel], a
+	ret

+BadgeLevelRestrictions:
+    db 14 ; Onix
+    db 21 ; Starmie
+    db 24 ; Raichu
+    db 29 ; Vileplume
+    db 43 ; Alakazam
+    db 43 ; Weezing
+    db 47 ; Arcanine
+    db 50 ; Rhydon
+    db 65 ; Champion's starter

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

...
.useRareCandy
	push hl
	ld bc, wPartyMon1Level - wPartyMon1
	add hl, bc ; hl now points to level
+	push hl ; store mon's level
+	ld b, MAX_LEVEL
+	ld a, [wDifficulty]
+	and a
+	jr z, .next1 ; no level caps if not on hard mode
+	callfar GetLevelCap
+	ld a, [wMaxLevel]
+	ld b, a
+.next1
+	pop hl ; retrieve mon's level
	ld a, [hl] ; a = level
-	cp MAX_LEVEL
+	cp b ; MAX_LEVEL on normal mode, level cap on hard mode
-	jr z, .vitaminNoEffect ; can't raise level above 100
+	jr nc, .vitaminNoEffect ; can't raise level above cap ; Carry is better than zero here.
...

...

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 b, MAX_LEVEL
+	ld a, [wDifficulty]
+	and a
+	ld a, b
+	ld [wMaxLevel], a
+	jr z, .next1 ; no level cap on normal mode
+	callfar GetLevelCap
+	ld a, [wMaxLevel]
+	ld b, a
+.next1
	ld a, d
-	cp MAX_LEVEL
+	cp b
	jr c, .skipCalcExp

-	ld d, MAX_LEVEL
+	ld a, [wMaxLevel]
+	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 d, MAX_LEVEL
+	ld a, [wMaxLevel]
+	ld d, a

.skipCalcExp
...

This logic can also be applied to the disobey code in engine/battle/core.asm as well. This wont do anything unless you set the disobey cutoff below the level cap. This example will result in Pokemon at the level cap occasionally disobeying. Add more dec a to make them disobey at lower levels.

	jr nz, .monIsTraded
-	inc hl
-	ld a, [wPlayerID + 1]
-	cp [hl]
+	ld a, [wDifficulty] ; Check if player is on hard mode
+	and a
	jp z, .canUseMove
+	callfar GetLevelCap
+	ld a, [wMaxLevel]
+	cp 100 ; prevent pokemon from disobeying at level 100
+	jr z, .next
+	dec a ; Lowers cut off by 1 so pokemon at the cap might disobey
+	jr .next
; it was traded
.monIsTraded

Finally, we will want to prevent the player from catching mons above the level cap, because if they do, they will see their level rollback as soon as they gain exp. Head back to engine/items/items_effects.asm.

...
	jp z, BoxFullCannotThrowBall

+; Hard mode, can't throw balls at pokemon above level cap
+	ld a, [wDifficulty]
+	and a
+	jr z, .canUseBall ; skip on normal mode
+	callfar GetLevelCap
+	ld a, [wMaxLevel]
+	ld b, a
+	ld a, [wEnemyMonLevel]
+	dec a ; force a carry if values are equal
+	cp b
+	jp nc, TooStrongToCatch

.canUseBall
...

That handles the logic, but we need to code our new message. Scroll down and add these two snippets where shown.

...
ItemUseNotTime:
	ld hl, ItemUseNotTimeText
	jr ItemUseFailed
	
+TooStrongToCatch:
+	ld hl, TooStrongToCatchText
+	jr ItemUseFailed

ItemUseNotYoursToUse:
	ld hl, ItemUseNotYoursToUseText
...
...
ItemUseNotTimeText:
	text_far _ItemUseNotTimeText
	text_end
	
+TooStrongToCatchText:
+	text_far _TooStrongToCatchText
+	text_end

ItemUseNotYoursToUseText:
	text_far _ItemUseNotYoursToUseText
...

Finally, we need text for it to display. This should be placed in data/text/text_6.asm.

...
	prompt
	
+_TooStrongToCatchText::
+	text "This #MON is"
+	line "too strong for"
+	cont "you to catch"
+	cont "right now!"
+	prompt

_ItemUseNotYoursToUseText::
...

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 change these lines to make a constant for the difficulty selection menu:

...
	const_def
	const YES_NO_MENU       ; 0
	const NORTH_WEST_MENU   ; 1
-	const SOUTH_EAST_MENU   ; 2
+	const DIFFICULTY_MENU   ; 2
	const WIDE_YES_NO_MENU  ; 3
...

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

...
TwoOptionMenuStrings:
; entries correspond to *_MENU constants
	table_width 5
	; width, height, blank line before first menu item?, text pointer
	two_option_menu 4, 3, FALSE, .YesNoMenu
	two_option_menu 6, 3, FALSE, .NorthWestMenu
-	two_option_menu 6, 3, FALSE, .SouthEastMenu
+	two_option_menu 7, 3, FALSE, .DifficultyMenu
	two_option_menu 6, 3, FALSE, .YesNoMenu
...

And at the end, add this line:

...
-.SouthEastMenu:
-	db   "SOUTH"
-	next "EAST@"

+.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

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

...
+_NormalModeText::
+	text "You have chosen"
+	line "NORMAL MODE!"

+	para "Classic #MON."
+	line "No difficulty"
+	cont "changes made."
	
+	para "Continue on"
+	line "NORMAL MODE?"
+	done

+_HardModeText::
+	text "You have chosen"
+	line "HARD MODE!"

+	para "Increased"
+	line "difficulty."
	
+	para "Continue on"
+	line "HARD MODE?"
+	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.

...
+.MenuCursorLoop ; difficulty menu
+	ld hl, DifficultyText
+ 	call PrintText
+ 	call DifficultyChoice
+	ld a, [wCurrentMenuItem]
+	ld [wDifficulty], a
+	and a
+	jr z, .SelectedNormalMode
+	ld hl, HardModeText
+	call PrintText
+	jp .YesNoNormalHard
+.SelectedNormalMode
+	ld hl, NormalModeText
+	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]
+	and a
+	jr z, .done
+	jp .MenuCursorLoop ; If player says no, back to difficulty selection
+.done
+	call ClearScreen ; clear the screen before resuming normal intro
        ld a, [wStatusFlags6]
	bit BIT_DEBUG_MODE, a
	jp nz, .skipSpeech
...

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

...
OakSpeechText3:
	text_far _OakSpeechText3
	text_end
+NormalModeText:
+	text_far _NormalModeText
+	text_end
+HardModeText:
+	text_far _HardModeText
+	text_end
+DifficultyText:
+	text_far _DifficultyText
+	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, DIFFICULTY_MENU
+	ld [wTwoOptionMenuID], a
+	hlcoord 5, 5
+	lb bc, 6, 6 ; Cursor Pos
+	ret
+	
+DisplayDifficultyChoice::
+	ld a, TWO_OPTION_MENU
+	ld [wTextBoxID], a
+	call DisplayTextBoxID
+	jp LoadScreenTilesFromBuffer1
+
+; display yes/no choice
+YesNoNormalHardChoice::
+	call SaveScreenTilesToBuffer1
+	call InitYesNoNormalHardTextBoxParameters
+	jr DisplayYesNoNormalHardChoice
+
+InitYesNoNormalHardTextBoxParameters::
+  	ld a, YES_NO_MENU
+	ld [wTwoOptionMenuID], a
+	hlcoord 7, 5
+	lb bc, 6, 8 ; Cursor Pos
+	ret
+	
+DisplayYesNoNormalHardChoice::
+	ld a, TWO_OPTION_MENU
+	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.