-
Notifications
You must be signed in to change notification settings - Fork 1k
Adding a Move Relearner & Move Deleter
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!
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.
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
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"
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.
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 INCLUDE
s, 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!
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.
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!