Skip to content

Adding an NPC that will trade your own pokemon back for evolution purposes

DeanWesrey edited this page Oct 19, 2023 · 6 revisions

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.

Contents

1. Add an NPC to trade with

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.

2. Add a new in game trade

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.

3. Allow pokemon to evolve from in game trades

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.

4. Edit the in game trading system

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!

Clone this wiki locally