Adding items that act like HMs - pret/pokecrystal GitHub Wiki
This tutorial will cover how to add items that act like HM Field moves, using CUT as an example.
Contents
- Adding the item
- Fixing the success message
- Fixing Oak's message
- Using the item as default on the overworld
- Including other HMs
- Getting Fly to work
1. Adding the item
Follow the guide on adding new items to create the item that will act like an HM. I changed ITEM_19 into CHAINSAW with the following:
const WATER_STONE ; 18
- const ITEM_19 ; 19
+ const CHAINSAW ; 19
const HP_UP ; 1a
db "WATER STONE@"
- db "TERU-SAMA@"
+ db "CHAINSAW@"
db "HP UP@"
dw WaterStoneDesc
- dw TeruSama2Desc
+ dw ChainsawDesc
dw HPUpDesc
...
-TeruSama2Desc:
- db "?@"
+ChainsawDesc:
+ db "Cuts down pesky"
+ next "plants.@"
-; ITEM_19
- item_attribute $9999, HELD_NONE, 0, NO_LIMITS, ITEM, ITEMMENU_NOUSE, ITEMMENU_NOUSE
+; CHAINSAW
+ item_attribute 0, HELD_NONE, 0, CANT_TOSS, KEY_ITEM, ITEMMENU_CLOSE, ITEMMENU_NOUSE
data/items/catch_rate_items.asm:
TimeCapsule_CatchRateItems:
- db ITEM_19, LEFTOVERS
In this example, Chainsaw will act like the HM Cut, so we'll use CutFunction
in engine/events/overworld.asm. A list of all HM Functions is included later this tutorial. Since these functions are in a different bank from the item effect functions, we just need to write a small function that can farcall our HM Function. engine/items/item_effects.asm:
dw EvoStoneEffect ; WATER_STONE
- dw NoEffect ; ITEM_19
+ dw ChainsawEffect ; CHAINSAW
...
+ChainsawEffect:
+ farcall CutFunction
+ ret
2. Fixing the success message
The above works, but it has a few flaws. Firstly, a message will pop up saying a Pokémon used the relevant HM Effect. There are several ways to change this, but the easiest is to edit the text that's displayed when your HM's effect script is run.
First, skip loading the Pokémon's name. At best this finds the first Pokémon that has Cut, at worse it fails and the text is incorrect. engine/events/overworld.asm:
Script_Cut:
- callasm GetPartyNickname
writetext UseCutText
Then, we can change the text to be a bit more neutral. UseCutText
points to _UseCutText
in data/text/common_2.asm:
_UseCutText::
- text_ram wStringBuffer2
- text " used"
- line "CUT!"
+ text "You cut some"
+ line "plants!"
prompt
3. Fixing Oak's message
As it stands, if you use your item and it fails, you'll be told the HM's error message AND be yelled at by Oak. To avoid this, we can introduce a new temporary var that can be used to skip Oak's message for HM-like-items.
First off, we need to create a new label in wram. There's a nice chunk of free space in wram bank 0 right after wDaysSince
. ram/wram.asm
wDaysSince:: db
+wUsingHMItem:: db
SECTION "WRAM 1", WRAMX
Next, we'll change our item effect so that it sets wUsingHMItem
to 1, signifying that it's okay to skip Oak's Message. engine/items/item_effects.asm
ChainsawEffect:
+ ld a, 1
+ ld [wUsingHMItem], a
farcall CutFunction
ret
Now we'll need to set wUsingHMItem
back to 0 after we're done with it (otherwise Oak won't yell at us when he should). There are two places we need to do this: engine/items/pack.asm...
UseItem:
farcall CheckItemMenu
ld a, [wItemAttributeValue]
ld hl, .dw
rst JumpTable
+ xor a
+ ld [wUsingHMItem], a
ret
...and engine/overworld/select_menu.asm:
UseRegisteredItem:
farcall CheckItemMenu
ld a, [wItemAttributeValue]
ld hl, .SwitchTo
rst JumpTable
+ xor a
+ ld [wUsingHMItem], a
ret
Finally, we'll skip Oak's message if wUsingHMItem
is set. Once again, we need to put this in the same two files: engine/items/pack.asm
.dw
; entries correspond to ITEMMENU_* constants
dw .Oak ; ITEMMENU_NOUSE
dw .Oak
dw .Oak
dw .Oak
dw .Current ; ITEMMENU_CURRENT
dw .Party ; ITEMMENU_PARTY
dw .Field ; ITEMMENU_CLOSE
.Oak:
+ ld a, [wUsingHMItem]
+ and a
+ ret nz
ld hl, OakThisIsntTheTimeText
call Pack_PrintTextNoScroll
ret
.Current:
call DoItemEffect
ret
...and engine/overworld/select_menu.asm:
UseRegisteredItem:
...
.CantUse:
call ReanchorMap
._cantuse
+ ld a, [wUsingHMItem]
+ and a
+ jr nz, .skip_oak
call CantUseItem
+.skip_oak
call CloseText
and a
ret
4. Using the item as default on the overworld
Even with the item added and it being functional, the game will still check for a Pokémon in the party to use the HM move, as well as the necessary badge, when attempting to interact with a cuttable tree on the overworld.
There are two places where it does this in engine/events/overworld.asm: CutFunction.CheckAble
and TryCutOW
.
CutFunction:
...
.CheckAble:
ld de, ENGINE_HIVEBADGE
call CheckBadge
jr c, .nohivebadge
call CheckMapForSomethingToCut
jr c, .nothingtocut
ld a, $1
ret
.nohivebadge
ld a, $80
ret
...
TryCutOW::
ld d, CUT
call CheckPartyMove
jr c, .cant_cut
ld de, ENGINE_HIVEBADGE
call CheckEngineFlag
jr c, .cant_cut
ld a, BANK(AskCutScript)
ld hl, AskCutScript
call CallScript
scf
ret
.cant_cut
ld a, BANK(CantCutScript)
ld hl, CantCutScript
call CallScript
scf
ret
First, let's say that you're relying on the Chainsaw as your only cutting tool. Cut is no longer a move, and you don't care about badges, you just want the player to be able to cut stuff as soon as they find the Chainsaw. In that scenario, you could edit these functions like this:
CutFunction:
...
.CheckAble:
- ld de, ENGINE_HIVEBADGE
- call CheckBadge
- jr c, .nohivebadge
call CheckMapForSomethingToCut
jr c, .nothingtocut
ld a, $1
ret
-.nohivebadge
- ld a, $80
- ret
...
TryCutOW::
- ld d, CUT
- call CheckPartyMove
- jr c, .cant_cut
-
- ld de, ENGINE_HIVEBADGE
- call CheckEngineFlag
- jr c, .cant_cut
+ ld a, CHAINSAW
+ ld [wCurItem], a
+ ld hl, wNumItems
+ call CheckItem
+ jr nc, .cant_cut
ld a, BANK(AskCutScript)
ld hl, AskCutScript
call CallScript
scf
ret
.cant_cut
ld a, BANK(CantCutScript)
ld hl, CantCutScript
call CallScript
scf
ret
That removes both the move and badge checks, and just checks if you have the Chainsaw in your pack.
However, you may still want to have the option to use Cut with a Pokémon, or to limit it based on badges. So you might do something like this instead:
CutFunction:
...
.CheckAble:
ld de, ENGINE_HIVEBADGE
call CheckBadge
jr c, .nohivebadge
call CheckMapForSomethingToCut
jr c, .nothingtocut
ld a, $1
ret
.nohivebadge
ld a, $80
ret
...
TryCutOW::
- ld d, CUT
- call CheckPartyMove
- jr c, .cant_cut
-
ld de, ENGINE_HIVEBADGE
call CheckEngineFlag
jr c, .cant_cut
+ ld a, CHAINSAW
+ ld [wCurItem], a
+ ld hl, wNumItems
+ call CheckItem
+ jr c, .can_cut
+
+ ld d, CUT
+ call CheckPartyMove
+ jr c, .cant_cut
+
+.can_cut
ld a, BANK(AskCutScript)
ld hl, AskCutScript
call CallScript
scf
ret
.cant_cut
ld a, BANK(CantCutScript)
ld hl, CantCutScript
call CallScript
scf
ret
This version first makes sure you have the badge, then checks if you're carrying the Chainsaw, and finally, if you're not, it checks if you have Cut in your party. There are different ways to organize this, but from these examples you should be able to make it work the way you want it to.
5. Including other HMs
The same process can be followed for most of the other HM effects with little difference except for the function names and usage text.
HM Function List
All of these are in engine/events/overworld.asm.
CutFunction
FlyFunction
SurfFunction
StrengthFunction
OWFlash
WhirlpoolFunction
WaterfallFunction
Additionally, functions like TeleportFunction
can be used as well.
HM Effect Scripts
All of these are in engine/events/overworld.asm.
Script_Cut
- Fly does not show a message
UsedSurfScript
Script_UsedStrength
- Flash does not show the Pokémon's name
Script_UsedWhirlpool
Script_UsedWaterfall
6. Getting Fly to work
Fly will take some more effort than the other HMs, mostly because it brings up the map for you to choose where to fly to, which involves loading different tilesets. It also normally pulls the icon of the Pokémon using it, which will look glitchy if there's no valid Pokémon in your party. And since that icon is used for the overworld taking-off and landing animations, we need it to carry over to the next map, so we're going to need to create a second variable to use for it that won't get reset like wUsingHMItem
does. How about this time, we insert it into some unused space instead of tacking it onto the end. ram/wram.asm
wNumHits:: db
- ds 1
+wFlyingWithHMItem:: db
wOptions::
Now that we have that, let's make sure it gets zeroed out if we're actually flying with a Pokémon... engine/pokemon/mon_menu.asm
MonMenu_Fly:
+ xor a
+ ld [wFlyingWithHMItem], a
farcall FlyFunction
ld a, [wFieldMoveSucceeded]
cp $2
jr z, .Fail
cp $0
jr z, .Error
farcall StubbedTrainerRankings_Fly
ld b, $4
ld a, $2
ret
.Fail:
ld a, $3
ret
.Error:
ld a, $0
ret
And then, of course, we'll need to set it to 1 when we use our item. Let's assume we've called the item AIRPLANE. We can use the above MonMenu_Fly
as a basis for our Airplane function. engine/items/item_effects.asm
AirplaneEffect:
ld a, 1
ld [wUsingHMItem], a
ld [wFlyingWithHMItem], a
farcall FlyFunction
ld a, [wFieldMoveSucceeded]
cp $2
jr z, .Fail
cp $0
jr z, .Error
farcall StubbedTrainerRankings_Fly
ld b, $4
ld a, $2
ret
.Fail:
ld a, $3
ret
.Error:
ld a, $0
ret
All of that extra stuff at the end there will make sure that menus and things are fed the correct values to avoid a game crash. But we still have more to do.
We're going to need to slot a new label in the middle of ExitAllMenus
in home/map.asm:
ExitAllMenus::
call ClearBGPalettes
call Call_ExitMenu
+ExitFlyMap::
call ReloadTilesetAndPalettes
call UpdateSprites
call GSReloadPalettes
FinishExitMenu::
ld b, SCGB_MAPPALS
call GetSGBLayout
farcall LoadOW_BGPal7
call WaitBGMap2
farcall FadeInFromWhite
call EnableSpriteUpdates
ret
Now we'll make use of it in FlyFunction
at engine/events/overworld.asm:
FlyFunction:
...
.illegal
call CloseWindow
+ ld a, [wFlyingWithHMItem]
+ and a
+ jr z, .done_tiles
+ ld a, [wUsingItemWithSelect]
+ and a
+ jr nz, .overworld
+ farcall Pack_InitGFX ; gets the pack GFX when exiting out of Fly by pressing B
+ farcall WaitBGMap_DrawPackGFX
+ farcall Pack_InitColors
+.done_tiles
call WaitBGMap
ld a, $80
ret
+.overworld
+ call ExitFlyMap
+ jr .done_tiles
+
.DoFly:
+ ld a, [wUsingItemWithSelect]
+ and a
+ jr z, .done_select
+ call ExitFlyMap
+.done_select
ld hl, .FlyScript
call QueueScript
ld a, $81
ret
...
The above will make sure that if we use Fly with the Airplane, or if we open up the Fly map and then back out of it with B, the appropriate tiles will be loaded, whether we were using the item from the pack or with Select.
Okay, now let's finish up with the Pokémon icon. Find FlyFunction_GetMonIcon
in engine/gfx/mon_icons.asm:
FlyFunction_GetMonIcon:
push de
+ ld a, [wFlyingWithHMItem]
+ and a
+ jr z, .flying_with_mon
+ ld a, ICON_BIRD
+ jr .finish
+
+.flying_with_mon
ld a, [wTempIconSpecies]
call ReadMonMenuIcon
+.finish
ld [wCurIcon], a
pop de
ld a, e
call GetIcon_a
ret
And then we need to make this small change to TownMapMon
in engine/pokegear/pokegear.asm:
TownMapMon:
; Draw the FlyMon icon at town map location
; Get FlyMon species
ld a, [wCurPartyMon]
ld hl, wPartySpecies
ld e, a
ld d, 0
add hl, de
ld a, [hl]
ld [wTempIconSpecies], a
; Get FlyMon icon
ld e, $08 ; starting tile in VRAM
- farcall GetSpeciesIcon
+ farcall FlyFunction_GetMonIcon
; Animation/palette
depixel 0, 0
ld a, SPRITE_ANIM_OBJ_PARTY_MON
call InitSpriteAnimStruct
ld hl, SPRITEANIMSTRUCT_TILE_ID
add hl, bc
ld [hl], $08
ld hl, SPRITEANIMSTRUCT_ANIM_SEQ_ID
add hl, bc
ld [hl], SPRITE_ANIM_FUNC_NULL
ret
This will force the icon to be ICON_BIRD
if we're flying with the Airplane. You can use whichever icon you want there, depending on the name of your Fly item, or you can create new graphics for it. (Note that it may still look screwy if used when you have no Pokémon in your party, so be careful about giving it to the player before they grab their starter.)
And with that, your Fly item will work even when registered to Select! Air travel has never been faster.
NOTE: StubbedTrainerRankings_Fly
is one of several functions meant to track player statistics for ranking over mobile communications. If you look at the definition of these functions in mobile/mobile_41.asm, you'll see that they begin with ret
, meaning they no longer do anything. You can remove that initial ret
to make them tally their values again, but you'd have to decide how to display this information to the player for it to be of any use. Long story short, you can delete that farcall StubbedTrainerRankings_Fly
line from your Fly item function if you don't plan to do all that.