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.

bgb00002 bgb00003