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.