Add a Move Relearner - pret/pokecrystal GitHub Wiki
Thank you to the Twitch Plays Pokemon team from whose code this was adapted.
1. The main script
Create a new file in engine/events/
named move_relearner.asm
and paste the following code within.
const_def
const MOVERELEARNERTEXT_INTRO
const MOVERELEARNERTEXT_WHICHMON
const MOVERELEARNERTEXT_WHICHMOVE
const MOVERELEARNERTEXT_COMEAGAIN
const MOVERELEARNERTEXT_EGG
const MOVERELEARNERTEXT_NOTAPOKEMON
const MOVERELEARNERTEXT_NOTENOUGHMONEY
const MOVERELEARNERTEXT_NOMOVESTOLEARN
MoveRelearner:
ld a, MOVERELEARNERTEXT_INTRO
call PrintMoveRelearnerText
farcall PlaceMoneyTopRight
call YesNoBox
jp c, .cancel
ld hl, .cost_to_relearn
ld de, hMoneyTemp
ld bc, 3
call CopyBytes
ld bc, hMoneyTemp
ld de, wMoney
farcall CompareMoney
jp c, .not_enough_money
ld a, MOVERELEARNERTEXT_WHICHMON
call PrintMoveRelearnerText
call JoyWaitAorB
ld b, $6
farcall SelectMonFromParty
jr c, .cancel
ld a, [wCurPartySpecies]
cp EGG
jr z, .egg
call IsAPokemon
jr c, .no_mon
call GetRelearnableMoves
jr z, .no_moves
ld a, MOVERELEARNERTEXT_WHICHMOVE
call PrintMoveRelearnerText
call JoyWaitAorB
call ChooseMoveToLearn
jr c, .skip_learn
ld a, [wMenuSelection]
ld [wTempSpecies], a
call GetMoveName
ld hl, wStringBuffer1
ld de, wStringBuffer2
ld bc, wStringBuffer2 - wStringBuffer1
call CopyBytes
ld b, 0
predef LearnMove
ld a, b
and a
jr z, .skip_learn
ld hl, .cost_to_relearn
ld de, hMoneyTemp
ld bc, 3
call CopyBytes
ld bc, hMoneyTemp
ld de, wMoney
farcall TakeMoney
ld de, SFX_TRANSACTION
call PlaySFX
call WaitSFX
.skip_learn
call CloseSubmenu
call SpeechTextbox
.cancel
ld a, MOVERELEARNERTEXT_COMEAGAIN
call PrintMoveRelearnerText
ret
.egg
ld a, MOVERELEARNERTEXT_EGG
call PrintMoveRelearnerText
ret
.not_enough_money
ld a, MOVERELEARNERTEXT_NOTENOUGHMONEY
call PrintMoveRelearnerText
ret
.no_mon
ld a, MOVERELEARNERTEXT_NOTAPOKEMON
call PrintMoveRelearnerText
ret
.no_moves
ld a, MOVERELEARNERTEXT_NOMOVESTOLEARN
call PrintMoveRelearnerText
ret
.cost_to_relearn
dt 1000
GetRelearnableMoves:
; Get moves relearnable by CurPartyMon
; Returns z if no moves can be relearned.
ld hl, wd002
xor a
ld [hli], a
ld [hl], $ff
ld a, MON_SPECIES
call GetPartyParamLocation
ld a, [hl]
ld [wCurPartySpecies], a
push af
ld a, MON_LEVEL
call GetPartyParamLocation
ld a, [hl]
ld [wCurPartyLevel], a
ld b, 0
ld de, wd002 + 1
; based on GetEggMove in engine/pokemon/breeding.asm
ld a, [wCurPartySpecies]
dec a
push bc
ld b, 0
ld c, a
ld hl, EvosAttacksPointers
add hl, bc
add hl, bc
ld a, BANK(EvosAttacksPointers)
call GetFarWord
.skip_evos
ld a, BANK(EvosAttacks)
call GetFarByte
inc hl
and a
jr nz, .skip_evos
.loop_moves
ld a, BANK(EvosAttacks)
call GetFarByte
inc hl
and a
jr z, .done
ld c, a
ld a, [wCurPartyLevel]
cp c
ld a, BANK(EvosAttacks)
call GetFarByte
inc hl
jr c, .loop_moves
ld c, a
call CheckAlreadyInList
jr c, .loop_moves
call CheckPokemonAlreadyKnowsMove
jr c, .loop_moves
ld a, c
ld [de], a
inc de
ld a, $ff
ld [de], a
pop bc
inc b
push bc
jr .loop_moves
.done
pop bc
pop af
ld [wCurPartySpecies], a
ld a, b
ld [wd002], a
and a
ret
CheckAlreadyInList:
push hl
ld hl, wd002 + 1
.loop
ld a, [hli]
inc a
jr z, .nope
dec a
cp c
jr nz, .loop
pop hl
scf
ret
.nope
pop hl
and a
ret
CheckPokemonAlreadyKnowsMove:
; Check if move c is in the selected pokemon's movepool already.
; Returns c if yes.
push hl
push bc
ld a, MON_MOVES
call GetPartyParamLocation
ld b, 4
.loop
ld a, [hli]
cp c
jr z, .yes
dec b
jr nz, .loop
pop bc
pop hl
and a
ret
.yes
pop bc
pop hl
scf
ret
ChooseMoveToLearn:
; Open a full-screen scrolling menu
; Number of items stored in wd002
; List of items stored in wd002 + 1
; Print move names, PP, details, and descriptions
; Enable Up, Down, A, and B
; Up scrolls up
; Down scrolls down
; A confirms selection
; B backs out
call FadeToMenu
farcall BlankScreen
call UpdateSprites
ld hl, .MenuHeader
call CopyMenuHeader
xor a
ld [wMenuCursorPosition], a
ld [wMenuScrollPosition], a
call ScrollingMenu
call SpeechTextbox
ld a, [wMenuJoypad]
cp B_BUTTON
jr z, .carry
ld a, [wMenuSelection]
ld [wPutativeTMHMMove], a
and a
ret
.carry
scf
ret
.MenuHeader:
db MENU_BACKUP_TILES ; flags
menu_coords 1, 1, SCREEN_WIDTH - 1, 11
dw .MenuData
db 1 ; default option
.MenuData:
db SCROLLINGMENU_DISPLAY_ARROWS | SCROLLINGMENU_ENABLE_FUNCTION3 ; item format
db 5, 8 ; rows, columns
db SCROLLINGMENU_ITEMS_NORMAL ; horizontal spacing
dba wd002
dba .PrintMoveName
dba .PrintDetails
dba .PrintMoveDesc
.PrintMoveName
push de
ld a, [wMenuSelection]
ld [wTempSpecies], a
call GetMoveName
pop hl
call PlaceString
ret
.PrintDetails
ld hl, wStringBuffer1
ld bc, wStringBuffer2 - wStringBuffer1
ld a, " "
call ByteFill
ld a, [wMenuSelection]
inc a
ret z
dec a
push de
dec a
ld bc, MOVE_LENGTH
ld hl, Moves + MOVE_TYPE
call AddNTimes
ld a, BANK(Moves)
call GetFarByte
ld [wTempSpecies], a
; get move type
; 6 characters
ld c, a ;character width loaded into c
add a ;double a (two characters)
add c ;add c to a (three charaters)
add a ;double a (six characters)
add c ;add c to a (seven charaters, needed for blank space at the end)
ld c, a
ld b, 0
ld hl, .Types
add hl, bc
ld d, h
ld e, l
ld hl, wStringBuffer1
ld bc, 7
call PlaceString
ld hl, wStringBuffer1 + 7
ld [hl], "P"
inc hl
ld [hl], "P"
inc hl
ld [hl], ":"
ld a, [wMenuSelection]
dec a
ld bc, MOVE_LENGTH
ld hl, Moves + MOVE_PP
call AddNTimes
ld a, BANK(Moves)
call GetFarByte
ld [wCurCoordEvent], a ;Changed from wEngineBuffer1
ld hl, wStringBuffer1 + 10
ld de, wCurCoordEvent ;Changed from wEngineBuffer1
ld bc, $102
call PrintNum
ld hl, wStringBuffer1 + 12
ld [hl], "@"
ld hl, SCREEN_WIDTH - 5
pop de
add hl, de
push de
ld de, wStringBuffer1
call PlaceString
pop de
ret
.Types
db "NORMAL@"
db "FIGHT@@"
db "FLYING@"
db "POISON@"
db "GROUND@"
db "ROCK@@@"
db "BIRD@@@"
db "BUG@@@@"
db "GHOST@@"
db "STEEL@@"
db "UNUSED@"
db "UNUSED@"
db "UNUSED@"
db "UNUSED@"
db "UNUSED@"
db "UNUSED@"
db "UNUSED@"
db "UNUSED@"
db "UNUSED@"
db "CURSE@@"
db "FIRE@@@"
db "WATER@@"
db "GRASS@@"
db "ELECTR@"
db "PSYCH@@"
db "ICE@@@@"
db "DRAGON@"
db "DARK@@@"
.PrintMoveDesc
push de
call SpeechTextbox
ld a, [wMenuSelection]
inc a
pop de
ret z
dec a
ld [wCurSpecies], a
hlcoord 1, 14
predef PrintMoveDescription
ret
PrintMoveRelearnerText:
ld e, a
ld d, 0
ld hl, .TextPointers
add hl, de
add hl, de
ld a, [hli]
ld h, [hl]
ld l, a
call PrintText
ret
.TextPointers
dw .Intro
dw .WhichMon
dw .WhichMove
dw .ComeAgain
dw .Egg
dw .NotMon
dw .NotEnoughMoney
dw .NoMovesToLearn
.Intro
text "Hello! I am the"
line "MOVE RELEARNER."
para "I know all the"
line "moves that can be"
para "learned for each"
line "#MON."
para "For just ¥1000, I"
line "can share that"
para "knowledge with"
line "you. How about it?"
done
.WhichMon
text "Excellent! Which"
line "#MON should"
cont "remember a move?"
done
.WhichMove
text "Which move should"
line "it remember?"
done
.ComeAgain
text "If you want your"
line "#MON to remem-"
cont "ber moves, come"
cont "back to me."
done
.Egg
text "An EGG can't learn"
line "moves."
done
.NotMon
text "What?! That's not"
line "a #MON!"
done
.NotEnoughMoney
text "You don't have"
line "enough money."
done
.NoMovesToLearn
text "This #MON can't"
line "learn any moves"
cont "from me."
done
This script will contain nearly everything needed for the Relearner to function. Lets break down some parts. First, it defines some contstants. These are used throughout the script to call particular text lines from PrintMoveRelearnerText:
at the end of the script. If you wish to change anything the Relearner says, find the line there and make your desired edits.
One of the first thing the code does is check if you have enough to afford the Relearner's services. In the code above, he charges Y1,000 per session. If you want to remove these costs, you can comment out the following lines.
MoveRelearner:
ld a, MOVERELEARNERTEXT_INTRO
call PrintMoveRelearnerText
- farcall PlaceMoneyTopRight
call YesNoBox
jp c, .cancel
- ld hl, .cost_to_relearn
- ld de, hMoneyTemp
- ld bc, 3
- call CopyBytes
- ld bc, hMoneyTemp
- ld de, wMoney
- farcall CompareMoney
- jp c, .not_enough_money
ld a, MOVERELEARNERTEXT_WHICHMON
call PrintMoveRelearnerText
call JoyWaitAorB
[...]
ld bc, wStringBuffer2 - wStringBuffer1
call CopyBytes
ld b, 0
predef LearnMove
ld a, b
and a
jr z, .skip_learn
- ld hl, .cost_to_relearn
- ld de, hMoneyTemp
- ld bc, 3
- call CopyBytes
- ld bc, hMoneyTemp
- ld de, wMoney
- farcall TakeMoney
- ld de, SFX_TRANSACTION
- call PlaySFX
- call WaitSFX
.skip_learn
call CloseSubmenu
call SpeechTextbox
.cancel
ld a, MOVERELEARNERTEXT_COMEAGAIN
call PrintMoveRelearnerText
ret
.egg
ld a, MOVERELEARNERTEXT_EGG
call PrintMoveRelearnerText
ret
-.not_enough_money
- ld a, MOVERELEARNERTEXT_NOTENOUGHMONEY
- call PrintMoveRelearnerText
- ret
.no_mon
ld a, MOVERELEARNERTEXT_NOTAPOKEMON
call PrintMoveRelearnerText
ret
.no_moves
ld a, MOVERELEARNERTEXT_NOMOVESTOLEARN
call PrintMoveRelearnerText
ret
-.cost_to_relearn
- dt 1000
You can change the cost of the service by just editing the dt 1000
line instead. You can get creative and charge an item instead. The Add a Move Tutor tutorial has some information on doing this in its Other Examples section. Of course, should you change or remove the cost of the service, you will want to update the Relearner's text accordingly.
The next routine, GetRelearnableMoves:
, does exactly what its name implies, by using the level up learn sets as a reference. It goes through subroutines to cull the list of repeats, remove moves your mon already knows, checks to make sure your mon has a high enough level to learn a move before adding it to the list.
The original code included a loop that would add moves of the mon's previous previous evolutions. This had its advantages and disadvantages. Something that exemplifies this is the Bellsprout line. Victreebel, like a lot of stone evolved mons, has very few moves learned from level ups. This makes adding moves learned from level up of previous forms an attractive proposition. However, it has unintended consequences. Weepinbell, for example, learns Body Slam at level 54, but since Bellsprout learns it at level 45, Weepinbell can be 'reminded' of Body Slam before it learns the move normally. If you wish to look into how the loop worked originally, you can view it at the link below.
https://github.com/TwitchPlaysPokemon/tppcrystal251pub/blob/public/event/move_relearner.asm
The original also had non-vanilla feature accommodations like Physical-Special split and additional types. Segueing into types, if your project makes alteration to types, like adding Fairy or making Ghost Special and Dark Physical, or other common edits, you will need to modify type names under .Types
to align with what you have in data/types/names.asm
.
2. Integration into game files
First, we will need to instruct the game to include this new file. Open main.asm
and make the following edit.
SECTION "bankB", ROMX
INCLUDE "engine/battle/trainer_huds.asm"
INCLUDE "data/trainers/class_names.asm"
INCLUDE "engine/battle/ai/redundant.asm"
INCLUDE "engine/events/move_deleter.asm"
+INCLUDE "engine/events/move_relearner.asm"
INCLUDE "engine/link/mystery_gift_2.asm"
Next, we will need to be able to call the function in game. Locate the file data/events/specials_pointers.asm
and add the following line to the end of it.
add_special MoveRelearner
Lastly, the MoveRelearner::
script calls out for data in data/pokemon/evos_attacks.asm
. In order for it to be able to see it, you will need to make the following change to that file.
INCLUDE "data/pokemon/evos_attacks_pointers.asm"
+EvosAttacks::
BulbasaurEvosAttacks:
db EVOLVE_LEVEL, 16, IVYSAUR
db 0 ; no more evolutions
db 1, TACKLE
db 4, GROWL
3. Placing the Move Relearner in-game
Now with everything in place, we will need to actually place the NPC in a map. For the sake of example, I have chosen maps/MoveDeletersHouse.asm
. Make the following changes to that file to place the NPC. The sprite chosen is arbitrary and can be changed if you want.
[...]
MoveDeleter:
faceplayer
opentext
special MoveDeletion
waitbutton
closetext
end
+MoveRelearnerScript:
+ faceplayer
+ opentext
+ special MoveRelearner
+ waitbutton
+ closetext
+ end
+
MoveDeletersHouseBookshelf:
jumpstd DifficultBookshelfScript
MoveDeletersHouse_MapEvents:
db 0, 0 ; filler
def_warp_events
warp_event 2, 7, BLACKTHORN_CITY, 6
warp_event 3, 7, BLACKTHORN_CITY, 6
def_coord_events
def_bg_events
bg_event 0, 1, BGEVENT_READ, MoveDeletersHouseBookshelf
bg_event 1, 1, BGEVENT_READ, MoveDeletersHouseBookshelf
def_object_events
object_event 2, 3, SPRITE_SUPER_NERD, SPRITEMOVEDATA_STANDING_DOWN, 0, 0, -1, -1, 0, OBJECTTYPE_SCRIPT, 0, MoveDeleter, -1
+ object_event 5, 4, SPRITE_FISHER, SPRITEMOVEDATA_STANDING_DOWN, 2, 0, -1, -1, 0, OBJECTTYPE_SCRIPT, 0, MoveRelearnerScript, -1
For continuity's sake, anywhere the NPC is placed, you will likely want to update signage. In this case, the text for the sign outside the Move Deleter's house is stored in maps/BlackthornCity.asm
. Make the following change to that file.
MoveDeletersHouseSignText:
- text "MOVE DELETER'S"
- line "HOUSE"
+ text "MOVE DELETER AND"
+ line "MOVE REMINDER'S"
+ cont "HOUSE"
done
DragonDensSignText:
text "DRAGON'S DEN"
With all that done, you should find the Move Relearner in the same house in Blackthorn City as the Move Deleter.