-
Notifications
You must be signed in to change notification settings - Fork 1k
Adding an NPC that will trade your own pokemon back for evolution purposes
This tutorial is designed primarily to show the implementation of a tradeback NPC, inspired by the one in crystal clear. However, other items are within the scope of this tutorial such as adding new in game trades and enabling evolution through in game trades.
- Add an NPC to trade with
- Add a new in game trade
- Allow pokemon to evolve from in game trades
- Edit the in game trading system
The first step to get your very own tradeback guy is adding an NPC to do the trading with, this is extremely simple. Just pick a location. For our example we will use CeladonMartF1. We'll start by opening it's header file and placing an NPC in it. The NPCs are added in the following format:
object SPRITE constant, Coord1, Coord2, Movement Type, Movement Direction, Map Script Pointer Number
That in mind edit data/maps/objects/CeladonMart1F.asm:
...
def_bg_events
bg_event 11, 4, TEXT_CELADONMART1F_DIRECTORY_SIGN
bg_event 14, 1, TEXT_CELADONMART1F_CURRENT_FLOOR_SIGN
def_object_events
object_event 8, 3, SPRITE_LINK_RECEPTIONIST, STAY, DOWN, TEXT_CELADONMART1F_RECEPTIONIST
+ object_event 3, 2, SPRITE_MOM, STAY, DOWN, TEXT_CELADONMART1F_TRADER
def_warps_to CELADON_MART_1F
I've used your Mom as the sprite just to be cheeky, you can obviously use a different sprite, like the male CoolTrainer sprite:
...
def_bg_events
bg_event 11, 4, TEXT_CELADONMART1F_DIRECTORY_SIGN
bg_event 14, 1, TEXT_CELADONMART1F_CURRENT_FLOOR_SIGN
def_object_events
object_event 8, 3, SPRITE_LINK_RECEPTIONIST, STAY, DOWN, TEXT_CELADONMART1F_RECEPTIONIST
+ object_event 3, 2, SPRITE_COOLTRAINER_M, STAY, DOWN, TEXT_CELADONMART1F_TRADER
def_warps_to CELADON_MART_1F
Either way, we've placed it near the elevator against the wall. It stands in place and looks down, and it's attached to the 4th script that we'll be adding next.
Next we need to add the script the NPC will be using to the map's script file. Edit scripts/CeladonMart1F.asm:
CeladonMart1F_Script:
jp EnableAutoTextBoxDrawing
CeladonMart1F_TextPointers:
dw_const CeladonMart1FReceptionistText, TEXT_CELADONMART1F_RECEPTIONIST
+ dw_const CeladonMart1FTraderText, TEXT_CELADONMART1F_TRADER
dw_const CeladonMart1FDirectorySignText, TEXT_CELADONMART1F_DIRECTORY_SIGN
dw_const CeladonMart1FCurrentFloorSignText, TEXT_CELADONMART1F_CURRENT_FLOOR_SIGN
CeladonMart1FReceptionistText:
text_far _CeladonMart1FReceptionistText
text_end
CeladonMart1FDirectorySignText:
text_far _CeladonMart1FDirectorySignText
text_end
CeladonMart1FCurrentFloorSignText:
text_far _CeladonMart1FCurrentFloorSignText
text_end
+CeladonMart1FTraderText:
+ text_asm
+ ld a, TRADE_WITH_SELF
+ ld [wWhichTrade], a
+ predef DoInGameTradeDialogue
+ jp TextScriptEnd
If you're familiar with in game trades in red, you may recognize the script we attached to our NPC. We'll be diving more into that in the next step.
The way we'll be implementing our tradeback NPC is by hijacking the already existing in game trades code. We're getting ahead of ourselves though. First we need to add a new trade that we can tie that to.
For this step we'll start by creating a new constant for said trade. If you wanted to add custom in game trades this is how you would do it by the way. Edit constants/script_constants.asm:
...
; in game trades
; TradeMons indexes (see data/events/trades.asm)
const_def
const TRADE_FOR_TERRY
const TRADE_FOR_MARCEL
const TRADE_FOR_CHIKUCHIKU
const TRADE_FOR_SAILOR
const TRADE_FOR_DUX
const TRADE_FOR_MARC
const TRADE_FOR_LOLA
const TRADE_FOR_DORIS
const TRADE_FOR_CRINKLES
const TRADE_FOR_SPOT
+ const TRADE_WITH_SELF
; in game trade dialog sets
; InGameTradeTextPointers indexes (see engine/events/in_game_trades.asm)
const_def
const TRADE_DIALOGSET_CASUAL
const TRADE_DIALOGSET_POLITE
const TRADE_DIALOGSET_HAPPY
+ const TRADE_DIALOGSET_SELF
; badges
; wObtainedBadges and wBeatGymFlags bits
const_def
While we were here we also added a constant for the dialogset we'll be using. Our Tradeback NPC shouldn't say the same things that the other in game trade NPCS say after all.
Next we add the details for the new trade itself, if you were just adding a new trade and not making a tradeback guy you would obviously input the relevant information here, and in that case, provided you use an already existing dialogset, you would be done. We're going for the tradeback guy though so we'll continue on. Edit data/events/trades.asm:
TradeMons:
; entries correspond to TRADE_FOR_* constants
; give mon, get mon, dialog id, nickname
db NIDORINO, NIDORINA, TRADE_DIALOGSET_CASUAL, "TERRY@@@@@@"
db ABRA, MR_MIME, TRADE_DIALOGSET_CASUAL, "MARCEL@@@@@"
db BUTTERFREE, BEEDRILL, TRADE_DIALOGSET_HAPPY, "CHIKUCHIKU@"
db PONYTA, SEEL, TRADE_DIALOGSET_CASUAL, "SAILOR@@@@@"
db SPEAROW, FARFETCHD, TRADE_DIALOGSET_HAPPY, "DUX@@@@@@@@"
db SLOWBRO, LICKITUNG, TRADE_DIALOGSET_CASUAL, "MARC@@@@@@@"
db POLIWHIRL, JYNX, TRADE_DIALOGSET_POLITE, "LOLA@@@@@@@"
db RAICHU, ELECTRODE, TRADE_DIALOGSET_POLITE, "DORIS@@@@@@"
db VENONAT, TANGELA, TRADE_DIALOGSET_HAPPY, "CRINKLES@@@"
db NIDORAN_M, NIDORAN_F, TRADE_DIALOGSET_HAPPY, "SPOT@@@@@@@"
+ db NO_MON, NO_MON, TRADE_DIALOGSET_SELF, "Unseen@@@@@"
Our choice of NO_MON here actually serves a purpose, no matter how many trades you add it is unlikely that you will ever trade missingno. Though if you intend to, change things accordingly. You'll also notice that we're using our new dialogset constant here, we'll add the text for that towards the end. The nickname is entirely unimportant as it won't ever be used.
Before we change that code for in game trades to accommodate our Tradeback NPC, we're going to make sure they can evolve through this method. this is exceedingly easy as it just involves deleting some code in one file. It also would enable you to have players trade for a mon that would actually evolve upon receipt. Edit engine/events/evolve_trade.asm:
...
; This was fixed in Yellow.
- ld a, [wInGameTradeReceiveMonName]
-
- ; GRAVELER
- cp "G"
- jr z, .ok
-
- ; "SPECTRE" (HAUNTER)
- cp "S"
- ret nz
- ld a, [wInGameTradeReceiveMonName + 1]
- cp "P"
- ret nz
-
-.ok
- ld a, [wPartyCount]
- dec a
- ld [wWhichPokemon], a
ld a, $1
ld [wForceEvolution], a
ld a, LINK_STATE_TRADING
ld [wLinkState], a
callfar TryEvolvingMon
xor a ; LINK_STATE_NONE
ld [wLinkState], a
jp PlayDefaultMusic
Note that if for some reason you weren't interested in a Tradeback NPC, you could instead stop deleting just above ld a, [wPartyCount]
, your traded mons would evolve just fine and you would be done here. That bit of code is moved to the next file for our continued purposes though.
Now I promise that sounds and looks a lot more complicated that anything we're actually doing here. The vast majority of what we're doing is just checking to see if the trade was called by the Tradeback NPC and skipping portions of code if so. Things will only be skipped if you're trading with the tradeback NPC, other in game trades will be entirely unaffected.
The next several edits all take place in the same file. They are all done in descending fashion so you can do them by scrolling down through the file rather than jumping around within it. I'll explain each as we go. Edit engine/events/in_game_trades.asm:
Here we skip the "PLAYER traded X for Y" text that comes after the trade, that information is a bit redundant and it also loads names using the table from step 2, meaning if we showed it it would claim we traded MISSINGNO for MISSINGNO. It could be fixed, but like I said before, it's a tad redundant for the tradback NPC anyway
...
; if the trade hasn't been done yet
xor a
ld [wInGameTradeTextPointerTableIndex], a
call .printText
ld a, $1
ld [wInGameTradeTextPointerTableIndex], a
call YesNoChoice
ld a, [wCurrentMenuItem]
and a
jr nz, .printText
call InGameTrade_DoTrade
jr c, .printText
+ ld a, [wInGameTradeGiveMonSpecies]
+ cp NO_MON
+ jr z, .printText
ld hl, TradedForText
call PrintText
.printText
Here we skip the check to see that you offered the mon from the table in step 2, we also need to change a jr to a jp because our additions may push some code out of jr range
...
InGameTrade_DoTrade:
xor a ; NORMAL_PARTY_MENU
ld [wPartyMenuTypeOrMessageID], a
dec a
ld [wUpdateSpritesEnabled], a
call DisplayPartyMenu
push af
call InGameTrade_RestoreScreen
pop af
ld a, $1
jp c, .tradeFailed ; jump if the player didn't select a pokemon
ld a, [wInGameTradeGiveMonSpecies]
+ cp NO_MON
+ jr z, .skip_mon_check
ld b, a
ld a, [wcf91]
cp b
ld a, $2
- jr nz, .tradeFailed ; jump if the selected mon's species is not the required one
+ jp nz, .tradeFailed ; jump if the selected mon's species is not the required one
+.skip_mon_check
Just under that we're skipping the setting of the flag that tells the game we've done this trade and shouldn't be able to do it again, if we didn't do this our tradeback NPC would only be good once. This way we can trade infinitely.
...
call AddNTimes
ld a, [hl]
ld [wCurEnemyLVL], a
+ ld a, [wInGameTradeGiveMonSpecies]
+ cp NO_MON
+ jr z, .skip_flag_set
ld hl, wCompletedInGameTradeFlags
ld a, [wWhichTrade]
ld c, a
ld b, FLAG_SET
predef FlagActionPredef
+.skip_flag_set
ld hl, ConnectCableText
call PrintText
ld a, [wWhichPokemon]
There is no space between the above code and this code, we broke for explanation. With this edit we're setting up a different beginning to the code that determines the pokemon being traded and received. This is mostly for the sake of the trade movie as will become apparent in a bit.
push af
ld a, [wCurEnemyLVL]
push af
call LoadHpBarAndStatusTilePatterns
+ ld a, [wInGameTradeGiveMonSpecies]
+ cp NO_MON
+ jr nz, .normal_in_game_trade_data
+ call TradeSelf_PrepareTradeData
+ jr .self_trade_data
+.normal_in_game_trade_data
call InGameTrade_PrepareTradeData
+.self_trade_data
predef InternalClockTradeAnim
This first line doesn't NEED to be removed, but it's kinda just wasting space, the addition below it actually skips the removal of a mon from your party and the addition of another one. You might call this the actual trade part, but for our purposes we don't actually NEED to remove a mon from the party for the evolve attempt to be made, the smoke and mirrors of it are enough. The addition below that is the block of code from step 3 that we could have left alone if we weren't making a Tradeback NPC. Also the point we skip to is on the other side of it. This enables normal trades to use the code while the tradeback NPC avoids it.
...
- ld [wMonDataLocation], a ; not used
+ push af
+ ld a, [wInGameTradeGiveMonSpecies]
+ cp NO_MON
+ jr z, .skip_swap_mons
+ pop af
ld [wRemoveMonFromBox], a
call RemovePokemon
ld a, $80 ; prevent the player from naming the mon
ld [wMonDataLocation], a
call AddPartyMon
call InGameTrade_CopyDataToReceivedMon
+ ld a, [wPartyCount]
+ dec a
+ ld [wWhichPokemon], a
+.skip_swap_mons
The next edit is the different beginning to the code that determines the pokemon being traded and received, that was mentioned earlier, basically it loads the pokemon you choose as both the sending and receiving pokemon. This marks the end of anything you could call heavy lifting in this process.
...
InGameTrade_RestoreScreen:
call GBPalWhiteOutWithDelay3
call RestoreScreenTilesAndReloadTilePatterns
call ReloadTilesetTilePatterns
call LoadScreenTilesFromBuffer2
call Delay3
call LoadGBPal
ld c, 10
call DelayFrames
farjp LoadWildData
+TradeSelf_PrepareTradeData:
+ ld a, [wWhichPokemon]
+ ld hl, wPartySpecies
+ ld b, 0
+ ld c, a
+ add hl, bc
+ ld a, [hl]
+ ld [wTradedPlayerMonSpecies], a
+ ld [wInGameTradeReceiveMonSpecies], a
+ ld hl, wTradedPlayerMonSpecies
+ jr InGameTrade_PrepareTradeData.loaded_self_trade_instead
InGameTrade_PrepareTradeData:
ld hl, wTradedPlayerMonSpecies
ld a, [wInGameTradeGiveMonSpecies]
+.loaded_self_trade_instead
Here is where we tie in the text for our new dialog set, it ultimately farcalls the actual text, I'm pretty sure this isn't necessary and that you could just put the text here, but for consistency's sake we'll do it the way the files are structured. Note that the wrong mon text and the after trade text are copied from a different set, this is because those scenarios aren't possible with the changes we've made and therefore would never be used.
...
InGameTradeTextPointers:
; entries correspond to TRADE_DIALOGSET_* constants
dw TradeTextPointers1
dw TradeTextPointers2
dw TradeTextPointers3
+ dw TradeTextPointers4
...
...
TradeTextPointers3:
dw WannaTrade3Text
dw NoTrade3Text
dw WrongMon3Text
dw Thanks3Text
dw AfterTrade3Text
+TradeTextPointers4:
+ dw WannaTrade4Text
+ dw NoTrade4Text
+ dw WrongMon1Text
+ dw Thanks4Text
+ dw AfterTrade1Text
...
...
AfterTrade3Text:
text_far _AfterTrade3Text
text_end
+
+WannaTrade4Text:
+ text_far _WannaTrade4Text
+ text_end
+
+NoTrade4Text:
+ text_far _NoTrade4Text
+ text_end
+
+Thanks4Text:
+ text_far _Thanks4Text
+ text_end
And the final step, add in what the Trader actually says. The following is very basic text to get the point across, You can edit this to say whatever you'd like. As a nugget of general knowledge, lines of text can be 18 characters long (including spaces and punctuation) before breaking the edge of the textbox and needing to be continued in another line. Edit data/text/text_7.asm:
...
_UsedCutText::
text_ram wcd6d
text " hacked"
line "away with Cut!"
prompt
+
+_WannaTrade4Text::
+ text "I'm the TRADER! I"
+ line "can trade your"
+ para "own #MON back"
+ para "to you."
+ para "Wanna trade?"
+ done
+
+_NoTrade4Text::
+ text "Ok, maybe next"
+ line "time then."
+ done
+
+_Thanks4Text::
+ text "All done, I hope"
+ line "that helped."
+ done
We're all done. With a tradeback NPC you don't have to change evolution methods for you mons if you don't want to. Enjoy!