Skip to content

Adding a Move Relearner & Move Deleter

Llinos Evans edited this page Apr 19, 2023 · 1 revision

This tutorial uses content from Shin Pokered by jojobear13, which uses code from Mateo's Red++ mod. I updated the code to fit modern pokered standards and did a couple of grammar tweaks, for the purpose of my own hack. It was a pretty agonising process, so to make life easier for anyone else wanting to do this, here's a tutorial!

Introduction

RBY is notable for being the only game in the series where HMs are truly permanent, and thus a Move Deleter is one of the most common demands when adding quality enhancements. No more being stuck on Flash! Or...whatever you're using for HMs. Can never go wrong with a feature like this one, anyway. Maybe you want to remove Rage to avoid accidentally clicking it.

RBY also benefits quite a lot from a Move Relearner, resolving multiple move incompatibilities (eg. Stomp + any Powder Exeggutor) and allowing Mewtwo to learn two unused Level 1 moves in Confusion and Disable. It also means that you don't need to catch Pokemon at a certain level to ensure they have what you want; for example, it's impossible to get a Wrap Lickitung in base Red and Blue because there isn't a Slowbro of a low enough level to trade for it. Want to avoid all this nonsense? Move Relearner. Easy...well, outside of needing to add one in.

Groundwork

Menu List Tweaks

These NPCs use very different menu code to anything else in the game, so you'll need to do some groundskeeping. In home/list_menu.asm...

	call GetItemPrice
	pop hl
	ld a, [wListMenuID]
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;needed to make Mateo's move deleter/relearner work
+	cp a, MOVESLISTMENU
+	jr z, .skipStoringItemName
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+```
	cp ITEMLISTMENU
	jr nz, .skipGettingQuantity
; if it's an item menu
...
	ld a, BANK(ItemNames)
	ld [wPredefBank], a
-	ld a, ITEM_NAME
-	ld [wNameListType], a
	call GetName
	jr .storeChosenEntry
...
.storeChosenEntry ; store the menu entry that the player chose and return
	ld de, wcd6d
	call CopyToStringBuffer ; copy name to wcf4b - finding the translation for CopyToC49 or whatever it was wasn't enjoyable
+.skipStoringItemName	;skip here if skipping storing item name
	ld a, CHOSE_MENU_ITEM
	ld [wMenuExitMethod], a
...
	ld [wChosenMenuItem], a
	xor a
-	ldh [hJoy7], a ; joypad state update flag
+	ld [hJoy7], a ; joypad state update flag
	ld hl, wd730

Updating evos_moves.asm

engine/pokemon/evos_moves.asm has a pretty notable block of code slapped in to ensure the move relearner works. Implement it into the file like so;

Evolution_FlagAction:
	predef_jump FlagActionPredef

+; From here, Move Relearner-related code -PvK
+;joenote - custom function by Mateo for move relearner
+PrepareRelearnableMoveList:: ; I don't know how the fuck you're a single colon in shin pokered but it sure as shit doesn't work here - PvK
+; Loads relearnable move list to wRelearnableMoves.
+; Input: party mon index = [wWhichPokemon]
+	; Get mon id.
+	ld a, [wWhichPokemon]
+	ld c, a
+	ld b, 0
+	ld hl, wPartySpecies
+	add hl, bc
+	ld a, [hl] ; a = mon id
+	ld [wd0b5], a	;joenote - put mon id into wram for potential later usage of GetMonHeader
+	; Get pointer to evos moves data.
+	dec a
+	ld c, a
+	ld b, 0
+	ld hl, EvosMovesPointerTable
+	add hl, bc
+	add hl, bc
+	ld a, [hli]
+	ld h, [hl]
+	ld l, a  ; hl = pointer to evos moves data for our mon
+	push hl
+	; Get pointer to mon's currently-known moves.
+	ld a, [wWhichPokemon]
+	ld hl, wPartyMon1Level
+	ld bc, wPartyMon2 - wPartyMon1
+	call AddNTimes
+	ld a, [hl]
+	ld b, a
+	push bc
+	ld a, [wWhichPokemon]
+	ld hl, wPartyMon1Moves
+	ld bc, wPartyMon2 - wPartyMon1
+	call AddNTimes
+	pop bc
+	ld d, h
+	ld e, l
+	pop hl
+	; Skip over evolution data.
+.skipEvoEntriesLoop
+	ld a, [hli]
+	and a
+	jr nz, .skipEvoEntriesLoop
+	; Write list of relearnable moves, while keeping count along the way.
+	; de = pointer to mon's currently-known moves
+	; hl = pointer to moves data for our mon
+	;  b = mon's level
+	ld c, 0 ; c = count of relearnable moves
+.loop
+	ld a, [hli]
+	and a
+	jr z, .done
+	cp b
+	jr c, .addMove
+	jr nz, .done
+.addMove
+	push bc
+	ld a, [hli] ; move id
+	ld b, a
+	; Check if move is already known by our mon.
+	push de
+	ld a, [de]
+	cp b
+	jr z, .knowsMove
+	inc de
+	ld a, [de]
+	cp b
+	jr z, .knowsMove
+	inc de
+	ld a, [de]
+	cp b
+	jr z, .knowsMove
+	inc de
+	ld a, [de]
+	cp b
+	jr z, .knowsMove
+.relearnableMove
+	pop de
+	push hl
+	; Add move to the list, and update the running count.
+	ld a, b
+	ld b, 0
+	ld hl, wMoveBuffer + 1
+	add hl, bc
+	ld [hl], a
+	pop hl
+	pop bc
+	inc c
+	jr .loop
+.knowsMove
+	pop de
+	pop bc
+	jr .loop
+.done	
+
+
+;joenote - start checking for level-0 moves
+	xor a
+	ld b, a	;b will act as a counter, as there can only be up to 4 level-0 moves
+	call GetMonHeader ;mon id already stored earlier in wd0b5
+	ld hl, wMonHMoves
+.loop2
+	ld a, b	;get the current loop counter into a
+	cp $4
+	jr nc, .done2	;if gone through 4 moves already, reached the end of the list. move to done2.
+	ld a, [hl]	;load move
+	and a
+	jr z, .done2	;if move has id 0, list has reached the end early. move to done2.
+	
+	;check if the move is already in the learnable move list
+	push bc
+	push hl
+	;c = buffer length
+.buffer_loop
+	ld hl, wMoveBuffer
+	ld b, 0
+	add hl, bc	;move to buffer at current c value
+	ld b, a	;b = move id
+	ld a, [hl] ; move id at buffer point
+	cp b
+	ld a, b	;a = move id
+	jr z, .move_in_buffer
+	inc c
+	dec c
+	jr z, .end_buffer_loop	;jump out if start of buffer is reached
+	dec c	;else decrement c and loop again
+	jr .buffer_loop
+.move_in_buffer
+	pop hl
+	pop bc
+	inc hl	;increment to the next level-0 move
+	inc b	;increment the loop counter
+	jr .loop2
+.end_buffer_loop
+	pop hl
+	pop bc
+	
+	;Check if move is already known by our mon.
+	push bc
+	ld a, [hl] ; move id
+	ld b, a
+	push de
+	ld a, [de]
+	cp b
+	jr z, .knowsMove2
+	inc de
+	ld a, [de]
+	cp b
+	jr z, .knowsMove2
+	inc de
+	ld a, [de]
+	cp b
+	jr z, .knowsMove2
+	inc de
+	ld a, [de]
+	cp b
+	jr z, .knowsMove2
+
+	;if the move is not already known, add it to the learnable move list
+	pop de
+	push hl
+	; Add move to the list, and update the running count.
+	ld a, b
+	ld b, 0
+	ld hl, wMoveBuffer + 1
+	add hl, bc
+	ld [hl], a
+	pop hl
+	pop bc
+	inc c
+	inc hl	;increment to the next level-0 move
+	inc b	;increment the loop counter
+	jr .loop2
+	
+.knowsMove2
+	pop de
+	pop bc
+	inc hl	;increment to the next level-0 move
+	inc b	;increment the loop counter
+	jr .loop2
+	
+.done2
+	ld b, 0
+	ld hl, wMoveBuffer + 1
+	add hl, bc
+	ld a, $ff
+	ld [hl], a
+	ld hl, wMoveBuffer
+	ld [hl], c
+	ret

INCLUDE "data/pokemon/evos_moves.asm"

WRAM management

In ram/wram.asm, you'll need to add some new RAM entries for stuff these NPCs use. The script for the Move Relearner, at the very least, uses a ton of WRAM - if you have larger learnsets, you'll need to use a really big ds stack. I noticed there was a hefty one near the slot machines, which seems to be more than enough.

; ROM back to return to when the player is done with the slot machine
wSlotMachineSavedROMBank:: db
-	ds 166

+; Move Buffer stuff for Mateo's code
+wMoveBuffer::
+wRelearnableMoves::
+	ds 164
+; Try not to use this stack. 
+; A good amount of space is needed to store data for the move relearner.
+; If it's like, 2, it'll lag like crazy and show garbage from elsewhere.	

Adding relevant files

Scripts

The way this implementation was done involved using individual files.

Make a file called move_deleter.asm in scripts, and copy this into it. You'll see a lot of map names, but don't worry - it works all the same.

MoveDeleterText1:
	text_asm
	ld hl, MoveDeleterGreetingText
	call PrintText
.jumpback
	call YesNoChoice
	ld a, [wCurrentMenuItem]
	and a
	jp nz, .exit
	ld hl, MoveDeleterSaidYesText
	call PrintText
	; Select pokemon from party.
	call SaveScreenTilesToBuffer2
	xor a
	ld [wListScrollOffset], a
	ld [wPartyMenuTypeOrMessageID], a
	ld [wUpdateSpritesEnabled], a
	ld [wMenuItemToSwap], a
	call DisplayPartyMenu
	push af
	call GBPalWhiteOutWithDelay3
	call RestoreScreenTilesAndReloadTilePatterns
	call LoadGBPal
	pop af
	jp c, .exit
	ld a, [wWhichPokemon]
	ld b, a
	push bc
	call PrepareDeletableMoveList
	pop bc
	ld a, [wMoveBuffer]
	cp 2
	jr nc, .chooseMove
	ld hl, MoveDeleterOneMoveText
	call PrintText
	jr .jumpback
.chooseMove
	push bc
	xor a
	ld [wListScrollOffset], a
	ld [wCurrentMenuItem], a
	ld hl, MoveDeleterWhichMoveText
	call PrintText
	ld a, MOVESLISTMENU
	ld [wListMenuID], a
	ld de, wMoveBuffer
	ld hl, wListPointer
	ld [hl], e
	inc hl
	ld [hl], d
	xor a
	ld [wPrintItemPrices], a ; don't print prices
	call DisplayListMenuID
	pop bc
	jr c, .exit  ; exit if player chose cancel
	; Save the selected move id.
	ld a, [wcf91]
	ld d, a
	push de
	push bc
	ld [wMoveNum], a
	ld [wd11e],a
	call GetMoveName
	call CopyToStringBuffer ; copy name to wcf4b
	ld hl, MoveDeleterConfirmText
	call PrintText
	call YesNoChoice
	pop bc
	pop de
	ld a, [wCurrentMenuItem]
	and a
	jr nz, .chooseMove
	push de
	ld a, b ; a = mon index
	ld hl, wPartyMon1Moves
	ld bc, wPartyMon2 - wPartyMon1
	call AddNTimes
	; hl = pointer to mon's moves
	; Search for the move, and set it to 0.
	pop de ; d = move id
	call DeleteMove
	ld hl, MoveDeleterForgotText
	call PrintText
.exit
	ld hl, MoveDeleterByeText
	call PrintText
	jp TextScriptEnd

DeleteMove:
; d = move id
	ld b, 0
.searchLoop
	ld a, [hli]
	cp d
	jr z, .foundMoveLoop
	inc b
	jr .searchLoop
.foundMoveLoop
	ld a, b
	cp 3
	jr z, .zeroLastMove
	ld a, [hl]
	dec hl
	ld [hli], a
	push hl
	ld de, wPartyMon1PP - wPartyMon1Moves
	add hl, de
	ld a, [hld]
	ld [hl], a ; copy move's PP
	pop hl
	inc hl
	inc b
	jr .foundMoveLoop
.zeroLastMove
	dec hl
	xor a
	ld [hl], a
	ld de, wPartyMon1PP - wPartyMon1Moves
	add hl, de
	ld [hl], a ; clear last move's PP
	ret

PrepareDeletableMoveList:
; Places a list of the selected pokemon's moves at wMoveBuffer.
; First byte is count, and last byte is $ff.
; Input: party mon index = [wWhichPokemon]
	ld a, [wWhichPokemon]
	ld hl, wPartyMon1Moves
	ld bc, wPartyMon2 - wPartyMon1
	call AddNTimes
	; hl = pointer to mon's 4 moves
	ld b, 0 ; count of moves
	ld c, 4 + 1 ; 4 moves
	ld de, wMoveBuffer + 1
.loop
	dec c
	jr z, .done
	ld a, [hli]
	and a
	jr z, .loop
	ld [de], a
	inc de
	inc b
	jr .loop
.done
	ld a, $ff  ; terminate the list
	ld [de], a
	ld a, b  ; store number of moves
	ld [wMoveBuffer], a
	ret

MoveDeleterGreetingText:
	text_far _MoveDeleterGreetingText
	text_end

MoveDeleterSaidYesText:
	text_far _MoveDeleterSaidYesText
	text_end

MoveDeleterWhichMoveText:
	text_far _MoveDeleterWhichMoveText
	text_end

MoveDeleterConfirmText:
	text_far _MoveDeleterConfirmText
	text_end

MoveDeleterForgotText:
	text_far _MoveDeleterForgotText
	text_end

MoveDeleterByeText:
	text_far _MoveDeleterByeText
	text_end

MoveDeleterOneMoveText:
	text_far _MoveDeleterOneMoveText
	text_end

Likewise, do the following for the move relearner, under move_relearner.asm in scripts.

MoveRelearnerText1:
	text_asm
; Display the list of moves to the player.
	ld hl, MoveRelearnerGreetingText
	call PrintText
	call YesNoChoice
	ld a, [wCurrentMenuItem]
	and a
	jp nz, .exit
	xor a
	;charge 1000 money
	ld [hMoney], a	
	ld [hMoney + 2], a	
	ld a, $0A
	ld [hMoney + 1], a  
	call HasEnoughMoney
	jr nc, .enoughMoney
	; not enough money
	ld hl, MoveRelearnerNotEnoughMoneyText
	call PrintText
	jp TextScriptEnd
.enoughMoney
	ld hl, MoveRelearnerSaidYesText
	call PrintText
	; Select pokemon from party.
	call SaveScreenTilesToBuffer2
	xor a
	ld [wListScrollOffset], a
	ld [wPartyMenuTypeOrMessageID], a
	ld [wUpdateSpritesEnabled], a
	ld [wMenuItemToSwap], a
	call DisplayPartyMenu
	push af
	call GBPalWhiteOutWithDelay3
	call RestoreScreenTilesAndReloadTilePatterns
	call LoadGBPal
	pop af
	jp c, .exit
	ld a, [wWhichPokemon]
	ld b, a
	push bc
	ld hl, PrepareRelearnableMoveList
	ld b, Bank(PrepareRelearnableMoveList)
	call Bankswitch
	ld a, [wMoveBuffer]
	and a
	jr nz, .chooseMove
	pop bc
	ld hl, MoveRelearnerNoMovesText
	call PrintText
	jp TextScriptEnd
.chooseMove
	ld hl, MoveRelearnerWhichMoveText
	call PrintText
	xor a
	ld [wCurrentMenuItem], a
	ld [wLastMenuItem], a
	ld a, MOVESLISTMENU
	ld [wListMenuID], a
	ld de, wMoveBuffer
	ld hl, wListPointer
	ld [hl], e
	inc hl
	ld [hl], d
	xor a
	ld [wPrintItemPrices], a ; don't print prices
	call DisplayListMenuID
	pop bc
	jr c, .exit  ; exit if player chose cancel
	push bc
	; Save the selected move id.
	ld a, [wcf91]
	ld [wMoveNum], a
	ld [wd11e],a
	call GetMoveName
	call CopyToStringBuffer ; copy name to wcf4b
	pop bc
	ld a, b
	ld [wWhichPokemon], a
	ld a, [wLetterPrintingDelayFlags]
	push af
	xor a
	ld [wLetterPrintingDelayFlags], a
	predef LearnMove
	pop af
	ld [wLetterPrintingDelayFlags], a
	ld a, b
	and a
	jr z, .exit
	; Charge 1000 money
	xor a
	ld [wPriceTemp], a
	ld [wPriceTemp + 2], a	
	ld a, $0A
	ld [wPriceTemp + 1], a	
	ld hl, wPriceTemp + 2
	ld de, wPlayerMoney + 2
	ld c, $3
	predef SubBCDPredef
	ld hl, MoveRelearnerByeText
	call PrintText
	jp TextScriptEnd
.exit
	ld hl, MoveRelearnerByeText
	call PrintText
	jp TextScriptEnd


MoveRelearnerGreetingText:
	text_far _MoveRelearnerGreetingText
	text_end

MoveRelearnerSaidYesText:
	text_far _MoveRelearnerSaidYesText
	text_end

MoveRelearnerNotEnoughMoneyText:
	text_far _MoveRelearnerNotEnoughMoneyText
	text_end

MoveRelearnerWhichMoveText:
	text_far _MoveRelearnerWhichMoveText
	text_end

MoveRelearnerByeText:
	text_far _MoveRelearnerByeText
	text_end

MoveRelearnerNoMovesText:
	text_far _MoveRelearnerNoMovesText
	text_end

In maps.asm, wherever you want the NPCs to be used, add them like so, below the map you're looking to use them in.

INCLUDE "scripts/mapname.asm"
INCLUDE "data/maps/objects/mapname.asm"
MapName_Blocks: INCBIN "maps/mapname.blk"
+; Mateo's move relearner/deleter files
+INCLUDE "scripts/move_deleter.asm"
+INCLUDE "scripts/move_relearner.asm"

Depending on how much you're using the last vanilla pokered map bank, it may end up overflowing. In the event of doing that, simply add a new SECTION below - just write one in and do your INCLUDEs, pokered will do the rest for you. There is more than enough space, I assure you!

That should look like this:

+SECTION "Maps 22", ROMX 

Remember: Only do this if you NEED to! Be efficient on your storage!

Text

The Move Deleter & Move Relearner also have their own text files. Make these in the root text/ folder, akin to how did with scripts.

Create a file called move_deleter.asm and include the following;

_MoveDeleterGreetingText::
	text "Mom says I'm so"
	line "forgetful that it"
	cont "is contagious."

	para "Want me to make a"
	line "#MON forget a"
	cont "move?"
	done

_MoveDeleterSaidYesText::
	text "Which #MON"
	line "should forget a"
	cont "move?"
	prompt

_MoveDeleterWhichMoveText::
	text "Which move should"
	line "it forget, then?"
	done

_MoveDeleterConfirmText::
	text "Make it forget"
	line "@"
	text_ram wStringBuffer
	text "?"
	prompt

_MoveDeleterForgotText::
	text "@"
	text_ram wStringBuffer
	text " was"
	line "forgotten!"
	prompt

_MoveDeleterByeText::
	text "Come visit me"
	line "again!"
	done

_MoveDeleterOneMoveText::
	text "That #mon"
	line "has one move."
	cont "Pick another?"
	done

And once again, but for move_relearner.asm;

_MoveRelearnerGreetingText::
	text "I tutor children,"
	line "but I also tutor"
	cont "#MON."
	
	para "I teach them to"
	line "remember moves"
	cont "they forgot."

	para "¥1000 per lesson."
	line "How about it?"
	done

_MoveRelearnerSaidYesText::
	text "Which #MON"
	line "should I tutor?"
	prompt

_MoveRelearnerNotEnoughMoneyText::
	text "Hmmm..."

	para "You don't have"
	line "enough money!"
	done

_MoveRelearnerWhichMoveText::
	text "Which move should"
	line "it learn?"
	done

_MoveRelearnerByeText::
	text "If any of your"
	line "#MON need to"
	cont "remember a move,"
	cont "come visit me!"
	done

_MoveRelearnerNoMovesText::
	text "This #MON"
	line "hasn't forgotten"
	cont "any moves."
	done

In text.asm...

INCLUDE "text/CinnabarIsland.asm"
INCLUDE "text/SaffronCity.asm"
INCLUDE "data/text/text_6.asm"

+SECTION "Text 11", ROMX
+INCLUDE "text/move_deleter.asm"
+INCLUDE "text/move_relearner.asm"

Depending on what you're doing, you may have made a new text bank already, such as if you're including new Pokedex text.

Placing the NPCs

With that, you now have everything you need to get cracking. Let's place these minor criminals!

In any maps/objects file...

def_object_events
	... (pretend there are object_events here)
+	object_event  1, 17, SPRITE_GAMEBOY_KID, STAY, DOWN, X
+	object_event  3, 17, SPRITE_GAMEBOY_KID, STAY, DOWN, X

Edit the sprite as desired. X should be higher than the rest - like 8 if the last was 7 and so on. Make sure bg_events are still ahead though, otherwise things can get a little weird.

Then, in the equivalent maps/scripts file...

MapHere_TextPointers:
	dw BlahBlah1
+	dw MoveDeleterText1
+	dw MoveRelearnerText1

Yep, because everything was defined in the files we slotted in, you don't need to do any tinkering with text farcalls later.

And that's it! You should have a fully functional move reminder and move deleter!

Clone this wiki locally