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
- Don't consume TMs when they're used
- Don't allow TMs to be held or tossed
- Don't show quantities next to TMs
- Don't let the player buy TMs they already have
- Do the same thing for Game Corner prize TMs
- Don't have redundant ways of acquiring the same TM
- Prevent depositing TM/HMs into the PC
- Don't let Time Capsule traded Pokémon hold TMs
- 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!
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.)