Infinitely reusable TMs - pret/pokecrystal GitHub Wiki

Starting in Gen 5, TMs became infinitely reusable, just like HMs. Let's review what would have to change to implement this in Gen 2:

  • Don't consume TMs when they're used (obviously)
  • Don't allow TMs to be held or tossed (just like HMs) or be deposited
  • Don't show quantities next to TMs (just like HMs)
  • Don't let the player buy TMs they already have
  • Don't have redundant ways of acquiring the same TM

All of those are relatively simple to do.

Contents

  1. Don't consume TMs when they're used
  2. Don't allow TMs to be held or tossed
  3. Don't show quantities next to TMs
  4. Don't let the player buy TMs they already have
  5. Do the same thing for Game Corner prize TMs
  6. Don't have redundant ways of acquiring the same TM
  7. Prevent depositing TM/HMs into the PC
  8. Don't let Time Capsule traded Pokémon hold TMs
  9. Remove text references to TMs being single-use

1. Don't consume TMs when they're used

Edit engine/items/tmhm.asm:

 	ld c, HAPPINESS_LEARNMOVE
 	callfar ChangeHappiness
-	call ConsumeTM
 	jr .learned_move

 ...

-ConsumeTM:
-	call ConvertCurItemIntoCurTMHM
-	ld a, [wTempTMHM]
-	dec a
-	ld hl, wTMsHMs
-	ld b, 0
-	ld c, a
-	add hl, bc
-	ld a, [hl]
-	and a
-	ret z
-	dec a
-	ld [hl], a
-	ret nz
-	ld a, [wTMHMPocketScrollPosition]
-	and a
-	ret z
-	dec a
-	ld [wTMHMPocketScrollPosition], a
-	ret

That was easy! Technically we're already done… you could just tell the player to ignore the TM quantities, and don't throw them away, and don't waste money on duplicates. But that would be sloppy, so let's keep going.

2. Don't allow TMs to be held or tossed

Edit data/items/attributes.asm:

-; TM01
-	item_attribute 3000, HELD_NONE, 0, CANT_SELECT, TM_HM, ITEMMENU_PARTY, ITEMMENU_NOUSE
-...
-; TM50
-	item_attribute 2000, HELD_NONE, 0, CANT_SELECT, TM_HM, ITEMMENU_PARTY, ITEMMENU_NOUSE
+; TM01
+	item_attribute 3000, HELD_NONE, 0, CANT_SELECT | CANT_TOSS, TM_HM, ITEMMENU_PARTY, ITEMMENU_NOUSE
+...
+; TM50
+	item_attribute 2000, HELD_NONE, 0, CANT_SELECT | CANT_TOSS, TM_HM, ITEMMENU_PARTY, ITEMMENU_NOUSE

We just changed CANT_SELECT to CANT_SELECT | CANT_TOSS for all 50 TMs, just like the HMs already had.

3. Don't show quantities next to TMs

Edit engine/items/tmhm.asm again:

 .okay
 	predef GetTMHMMove
 	ld a, [wNamedObjectIndex]
 	ld [wPutativeTMHMMove], a
 	call GetMoveName
 	pop hl
 	ld bc, 3
 	add hl, bc
-	push hl
 	call PlaceString
-	pop hl
 	pop bc
-	ld a, c
-	push bc
-	cp NUM_TMS + 1
-	jr nc, .hm2
-	ld bc, SCREEN_WIDTH + 9
-	add hl, bc
-	ld [hl], "×"
-	inc hl
-	ld a, "0" ; why are we doing this?
-	pop bc
-	push bc
-	ld a, b
-	ld [wTempTMHM], a
-	ld de, wTempTMHM
-	lb bc, 1, 2
-	call PrintNum
-.hm2
-	pop bc
 	pop de
 	pop hl
 	dec d
 	jr nz, .loop2
 	jr .done

The cp NUM_TMS + 1 and jr nc, .hm2 in there skipped the quantity-printing code for HMs, so we just deleted the quantity-printing code along with the now-redundant HM check.

4. Don't let the player buy TMs they already have

If you didn't know how to do this, you could get away with just not selling TMs in Marts. But in fact, we do know how to do this. ;)

Edit engine/items/mart.asm:

 MartAskPurchaseQuantity:
+	ld a, [wCurItem]
+	cp TM01
+	jr nc, .PurchaseQuantityOfTM
 	call GetMartDialogGroup ; gets a pointer from GetMartDialogGroup.MartTextFunctionPointers
 	inc hl
 	inc hl
 	ld a, [hl]
 	and a
 	jp z, StandardMartAskPurchaseQuantity
 	cp 1
 	jp z, BargainShopAskPurchaseQuantity
 	jp RooftopSaleAskPurchaseQuantity
+
+.PurchaseQuantityOfTM:
+	push de
+	ld hl, wNumItems
+	call CheckItem
+	pop de
+	jp c, .AlreadyHaveTM
+	farcall GetItemPrice
+	ld a, d
+	ld [wBuySellItemPrice + 0], a
+	ld a, e
+	ld [wBuySellItemPrice + 1], a
+	ld a, 1
+	ld [wItemQuantityChange], a
+	ld a, 99
+	ld [wItemQuantity], a
+	farcall BuySell_MultiplyPrice
+	push hl
+	ld hl, hMoneyTemp
+	ldh a, [hProduct + 1]
+	ld [hli], a
+	ldh a, [hProduct + 2]
+	ld [hli], a
+	ldh a, [hProduct + 3]
+	ld [hl], a
+	pop hl
+	ret
+
+.AlreadyHaveTM:
+	ld hl, .AlreadyHaveTMText
+	call PrintText
+	call JoyWaitAorB
+	scf
+	ret
+
+.AlreadyHaveTMText:
+	text_far AlreadyHaveTMText
+	text_end

And edit data/text/common_3.asm:

_MartHowManyText::
	text "How many?"
	done
+
+AlreadyHaveTMText::
+	text "You already have"
+	line "that TM."
+	done

If you already have a TM, you'll just get a message saying so; if not, there's no need to pick a quantity to buy, so it just finds the cost of buying one.

5. Do the same thing for Game Corner prize TMs

We just took care of buying TMs in Marts for cash; but what about in the Game Corners for coins? Those are implemented as event scripts, not assembly code, so they'll be easier to fix.

Edit maps/GoldenrodGameCorner.asm:

 .Thunder:
+	checkitem TM_THUNDER
+	iftrue GoldenrodGameCornerPrizeVendor_AlreadyHaveTMScript
 	checkcoins GOLDENRODGAMECORNER_TM25_COINS
 	ifequal HAVE_LESS, GoldenrodGameCornerPrizeVendor_NotEnoughCoinsScript
 	...

 .Blizzard:
+	checkitem TM_BLIZZARD
+	iftrue GoldenrodGameCornerPrizeVendor_AlreadyHaveTMScript
 	checkcoins GOLDENRODGAMECORNER_TM14_COINS
 	ifequal HAVE_LESS, GoldenrodGameCornerPrizeVendor_NotEnoughCoinsScript
 	...

 .FireBlast:
+	checkitem TM_FIRE_BLAST
+	iftrue GoldenrodGameCornerPrizeVendor_AlreadyHaveTMScript
 	checkcoins GOLDENRODGAMECORNER_TM38_COINS
 	ifequal HAVE_LESS, GoldenrodGameCornerPrizeVendor_NotEnoughCoinsScript
 	...

 ...

 GoldenrodGameCornerTMVendor_FinishScript:
 	waitsfx
 	playsound SFX_TRANSACTION
 	writetext GoldenrodGameCornerPrizeVendorHereYouGoText
 	waitbutton
 	sjump GoldenrodGameCornerTMVendor_LoopScript
+
+GoldenrodGameCornerPrizeVendor_AlreadyHaveTMScript:
+	writetext GoldenrodGameCornerPrizeVendorAlreadyHaveTMText
+	waitbutton
+	sjump GoldenrodGameCornerTMVendor_LoopScript

 ...

 GoldenrodGameCornerPrizeVendorHereYouGoText:
 	text "Here you go!"
 	done
+
+GoldenrodGameCornerPrizeVendorAlreadyHaveTMText:
+	text "But you already"
+	line "have that TM!"
+	done

And edit maps/CeladonGameCornerPrizeRoom.asm:

 .DoubleTeam:
+	checkitem TM_DOUBLE_TEAM
+	iftrue CeladonPrizeRoom_alreadyhavetm
 	checkcoins CELADONGAMECORNERPRIZEROOM_TM32_COINS
 	ifequal HAVE_LESS, CeladonPrizeRoom_notenoughcoins
 	...

 .Psychic:
+	checkitem TM_PSYCHIC_M
+	iftrue CeladonPrizeRoom_alreadyhavetm
 	checkcoins CELADONGAMECORNERPRIZEROOM_TM29_COINS
 	ifequal HAVE_LESS, CeladonPrizeRoom_notenoughcoins
 	...

 .HyperBeam:
+	checkitem TM_HYPER_BEAM
+	iftrue CeladonPrizeRoom_alreadyhavetm
 	checkcoins CELADONGAMECORNERPRIZEROOM_TM15_COINS
 	ifequal HAVE_LESS, CeladonPrizeRoom_notenoughcoins
 	...

 ...

 CeladonPrizeRoom_purchased:
 	waitsfx
 	playsound SFX_TRANSACTION
 	writetext CeladonPrizeRoom_HereYouGoText
 	waitbutton
 	sjump CeladonPrizeRoom_tmcounterloop
+
+CeladonPrizeRoom_alreadyhavetm:
+	writetext CeladonPrizeRoom_AlreadyHaveTMText
+	waitbutton
+	sjump CeladonPrizeRoom_tmcounterloop

 ...

 CeladonPrizeRoom_HereYouGoText:
 	text "Here you go!"
 	done
+
+CeladonPrizeRoom_AlreadyHaveTMText:
+	text "You already have"
+	line "that TM."
+	done

Pretty self-explanatory.

6. Don't have redundant ways of acquiring the same TM

If TMs are untossable and infinite-use, it's wasteful to have multiple ways of getting the same TM.

Eleven TMs can be acquired more than once in Pokémon Crystal:

  • TM02 Headbutt: Ilex Forest (gift); Goldenrod Dept. Store (¥2000 after receiving the gift)
  • TM08 Rock Smash: Route 36 (gift); Goldenrod Dept. Store (¥2000 after receiving the gift)
  • TM10 Hidden Power: Lake of Rage (gift); Celadon Dept. Store (¥3000)
  • TM11 Sunny Day: Radio Tower (gift); Celadon Dept. Store (¥2000)
  • TM13 Snore: Route 39 (gift); Dark Cave (item ball)
  • TM18 Rain Dance: Slowpoke Well (item ball); Celadon Dept. Store (¥2000)
  • TM21 Frustration: Goldenrod Dept. Store (weekly gift)
  • TM27 Return: Goldenrod Dept. Store (weekly gift)
  • TM29 Psychic: Mr Psychic (gift); Celadon Game Corner Prize (3500 coins)
  • TM37 Sandstorm: Route 27 (gift); Celadon Dept. Store (¥2000)
  • TM47 Steel Wing: Route 28 (gift); Rock Tunnel (item ball)

First, edit maps/MrPsychicsHouse.asm:

MrPsychic:
 	faceplayer
 	opentext
-	checkevent EVENT_GOT_TM29_PSYCHIC
+	checkitem TM_PSYCHIC_M
 	iftrue .AlreadyGotItem
 	writetext MrPsychicText1
 	buttonsound
 	verbosegiveitem TM_PSYCHIC_M
 	iffalse .Done
-	setevent EVENT_GOT_TM29_PSYCHIC
 	...

This change just makes the check an item check instead of event check, so that if TM29 has been purchased from the Celadon Game Corner, Mr. Psychic won't give the player another one.

Now edit maps/DarkCaveBlackthornEntrance.asm:

 DarkCaveBlackthornEntranceTMSnore:
-	itemball TM_SNORE
+	itemball AWAKENING

And edit maps/RockTunnel1F.asm:

 RockTunnel1FTMSteelWing:
-	itemball TM_STEEL_WING
+	itemball METAL_COAT

(I'm not bothering to update all the label and constant names from "Snore" and "Steel Wing" to their new items.)

Now edit maps/GoldenrodDeptStore5F.asm:

 GoldenrodDeptStore5FClerkScript:
 	faceplayer
 	opentext
-	checkevent EVENT_GOT_TM02_HEADBUTT
-	iftrue .headbutt
-	checkevent EVENT_GOT_TM08_ROCK_SMASH
-	iftrue .onlyrocksmash
-	sjump .neither
-
-.headbutt
-	checkevent EVENT_GOT_TM08_ROCK_SMASH
-	iftrue .both
-	sjump .onlyheadbutt
-
-.neither
-	pokemart MARTTYPE_STANDARD, MART_GOLDENROD_5F_1
-	closetext
-	end
-
-.onlyheadbutt
-	pokemart MARTTYPE_STANDARD, MART_GOLDENROD_5F_2
-	closetext
-	end
-
-.onlyrocksmash
-	pokemart MARTTYPE_STANDARD, MART_GOLDENROD_5F_3
-	closetext
-	end
-
-.both
-	pokemart MARTTYPE_STANDARD, MART_GOLDENROD_5F_4
+	pokemart MARTTYPE_STANDARD, MART_GOLDENROD_5F
	closetext
	end

 ...

 .VeryHappy:
 	writetext UnknownText_0x5615a
 	buttonsound
+	checkitem TM_RETURN
+	iftrue .AlreadyGotTM
 	verbosegiveitem TM_RETURN
-	iffalse .Done
 	setflag ENGINE_GOLDENROD_DEPT_STORE_TM27_RETURN
 	closetext
 	end

 ...

 .NotVeryHappy:
 	writetext UnknownText_0x561d8
 	buttonsound
+	checkitem TM_FRUSTRATION
+	iftrue .AlreadyGotTM
 	verbosegiveitem TM_FRUSTRATION
-	iffalse .Done
 	setflag ENGINE_GOLDENROD_DEPT_STORE_TM27_RETURN
 	closetext
 	end
+
+.AlreadyGotTM:
+	writetext GoldenrodDeptStore5FAlreadyGotTMText
+	waitbutton
+	closetext
+	end

 ...

 GoldenrodDeptStore5FReceptionistThereAreTMsPerfectForMonText:
 	text "There are sure to"
 	line "be TMs that are"

 	para "just perfect for"
 	line "your #MON."
 	done
+
+GoldenrodDeptStore5FAlreadyGotTMText:
+	text "Oh, you already"
+	line "have this TM…"
+	done

We did two things there: simplify the clerk script to just use a single Mart which won't have either redundant TM; and add clauses to the happiness-check lady that only allow one of each TM.

That introduced the single MART_GOLDENROD_5F constant, so edit constants/mart_constants.asm (or constants/item_data_constants.asm in older versions of pokecrystal):

-	const MART_GOLDENROD_5F_1
-	const MART_GOLDENROD_5F_2
-	const MART_GOLDENROD_5F_3
-	const MART_GOLDENROD_5F_4
+	const MART_GOLDENROD_5F

Finally, edit data/items/marts.asm:

-	dw MartGoldenrod5F1
-	dw MartGoldenrod5F2
-	dw MartGoldenrod5F3
-	dw MartGoldenrod5F4
+	dw MartGoldenrod5F
 	...

-MartGoldenrod5F1:
+MartGoldenrod5F:
 	db 3 ; # items
 	db TM_THUNDERPUNCH
 	db TM_FIRE_PUNCH
 	db TM_ICE_PUNCH
 	db -1 ; end
-
-MartGoldenrod5F2:
-	db 4 ; # items
-	db TM_THUNDERPUNCH
-	db TM_FIRE_PUNCH
-	db TM_ICE_PUNCH
-	db TM_HEADBUTT
-	db -1 ; end
-
-MartGoldenrod5F3:
-	db 4 ; # items
-	db TM_THUNDERPUNCH
-	db TM_FIRE_PUNCH
-	db TM_ICE_PUNCH
-	db TM_ROCK_SMASH
-	db -1 ; end
-
-MartGoldenrod5F4:
-	db 5 ; # items
-	db TM_THUNDERPUNCH
-	db TM_FIRE_PUNCH
-	db TM_ICE_PUNCH
-	db TM_HEADBUTT
-	db TM_ROCK_SMASH
-	db -1 ; end

 ...

 MartCeladon3F:
 	db 5 ; # items
-	db TM_HIDDEN_POWER
-	db TM_SUNNY_DAY
+	db TM_PSYCH_UP
 	db TM_PROTECT
-	db TM_RAIN_DANCE
-	db TM_SANDSTORM
+	db TM_THUNDERPUNCH
+	db TM_FIRE_PUNCH
+	db TM_ICE_PUNCH
 	db -1 ; end

We simplified the Goldenrod Mart data, and revised the Celadon Mart's inventory: they still sell TM17 Protect, but also the same three Punch TMs as Goldenrod, as well as TM09 Psych Up (which was only available held by Abra or Kadabra traded from Gen 1).

Speaking of trading from Gen 1…

7. Prevent depositing TM/HMs into the PC

In the previous part, we have replaced some script checks like checkevent with checkitem. But we need to keep in mind that the player can still deposit TMs into the PC, which would make checkitem fail!

Even without those checks, if the player accidentally gets the same TM twice or more, it won't show in the pack (as we've previously hidden the display of the quantity) but trying to deposit a TM would deposit only 1 of those, making the TM appear as a duplicate between the pack and the PC.

An easy to prevent those errors from happening is to prevent the player from depositing TMs (and HMs) in the PC.

Edit engine/events/pokecenter_pc.asm

.TryDepositItem:
+	farcall CheckItemPocket
+	ld a, [wItemAttributeValue]
+	cp TM_HM
+	jr z, .CantDeposit
+
	ld a, [wSpriteUpdatesEnabled]
	push af
	ld a, FALSE
	ld [wSpriteUpdatesEnabled], a
	farcall CheckItemMenu
	ld a, [wItemAttributeValue]
	ld hl, .dw
	rst JumpTable
	pop af
	ld [wSpriteUpdatesEnabled], a
	ret

+.CantDeposit
+	ld hl, .CantDepositText
+	call MenuTextboxBackup ; push text to queue
+	ret
+
+.CantDepositText
+	text_far _CantDepositText
+	text_end

And add the refusal text at the bottom of data/text/common_2.asm

+_CantDepositText::
+	text "Can't deposit"
+	line "this item."
+	prompt
+

Preventing the player from depositing items won't be an issue as long as the pack's pocket is big enough to store all of the concerned items.

8. Don't let Time Capsule traded Pokémon hold TMs

This is a minor issue that you might not even care about, but it's worth knowing about and simple to fix.

Pokémon traded from Gen 1 via the Time Capsule hold items based on their "catch rate" byte. Seven Pokémon have catch rates equivalent to TMs:

  • Abra and Kadabra: 200 = $C8 = TM_PSYCH_UP (although Kadabra's catch rate in Yellow is 96 = $60 = TWISTEDSPOON)
  • Krabby, Horsea, Goldeen, and Staryu: 225 = $E1 = TM_ICE_PUNCH
  • Nidoran♀ and Nidoran♂: 235 = $EB = TM_DETECT

Some other Pokémon have catch rates equivalent to unused item slots, so the TimeCapsule_CatchRateItems table exists to convert those catch rate values into valid items. We can reuse that mechanism to convert held TMs into ordinary items.

Edit data/items/catch_rate_items.asm:

 TimeCapsule_CatchRateItems:
 	db ITEM_19, LEFTOVERS
 	db ITEM_2D, BITTER_BERRY
 	db ITEM_32, GOLD_BERRY
 	db ITEM_5A, BERRY
 	db ITEM_64, BERRY
 	db ITEM_78, BERRY
 	db ITEM_87, BERRY
 	db ITEM_BE, BERRY
 	db ITEM_C3, BERRY
 	db ITEM_DC, BERRY
 	db ITEM_FA, BERRY
+	db TM_PSYCH_UP, BERRY
+	db TM_ICE_PUNCH, BERRY
+	db TM_DETECT, BERRY
 	db -1,      BERRY
 	db 0 ; end

9. Remove text references to TMs being single-use

Edit maps/VioletGym.asm:

 FalknerTMMudSlapText:
 	text "By using a TM, a"
 	line "#MON will"

 	para "instantly learn a"
 	line "new move."

-	para "Think before you"
-	line "act--a TM can be"
-	cont "used only once."
+	para "A TM can be used"
+	line "as many times as"
+	cont "you like."

 	para "TM31 contains"
 	line "MUD-SLAP."

 	para "It reduces the"
 	line "enemy's accuracy"

 	para "while it causes"
 	line "damage."

 	para "In other words, it"
 	line "is both defensive"
 	cont "and offensive."
 	done

And edit maps/CeladonDeptStore3F.asm:

 CeladonDeptStore3FYoungsterText:
 	text "I can't decide"
 	line "which #MON I"
 
 	para "should use this TM"
 	line "on…"
+
+	para "Lucky for me,"
+	line "it's reusable!"
 	done

That's everything!

Screenshot

There's still room for improvement here. The TM Pocket still uses a byte to store each TM's quantity, even though they should all be 0 or 1. It could be rewritten to work like the Key Items pocket, with just the item IDs stored, thus saving 57 bytes of WRAM.

(Actually, since TMs also have a fixed order from TM01 to HM07, the pocket could use a flag_array with NUM_TMS + NUM_HMS bits. But that would be even more complex to implement than a Key Items–style byte array.)