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

  1. Adding the item
  2. Fixing the success message
  3. Fixing Oak's message
  4. Using the item as default on the overworld
  5. Including other HMs
  6. 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:

constants/item_constants.asm:

	const WATER_STONE  ; 18
-	const ITEM_19      ; 19
+	const CHAINSAW     ; 19
	const HP_UP        ; 1a

data/items/names.asm:

	db "WATER STONE@"
-	db "TERU-SAMA@"
+	db "CHAINSAW@"
	db "HP UP@"

data/items/descriptions.asm:

 	dw WaterStoneDesc
-	dw TeruSama2Desc
+	dw ChainsawDesc
	dw HPUpDesc

 	...

-TeruSama2Desc:
-	db   "?@"
+ChainsawDesc:
+	db   "Cuts down pesky"
+	next "plants.@"

data/items/attributes.asm:

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