Table of Contents
- Step 1: Write your trainer's quotes and come up with their team
- Step 2: Add the trainer to the overworld using Porymap
- Step 3: Writing your trainer's script
- Step 4: Defining your trainer's opponent constant
- Step 5: Realizing that the opponent constant you just replaced was a Match Call trainer
- Step 6: Culling wherever in the game that old trainer of yours was referenced
- Step 7: Adding in the new text for your trainer
- Step 8: Tying your trainer script to the trainer in the map
- Step 9: Defining your trainer's sprite, encounter music, trainer class, AI, and name
- Step 10: Defining the trainer's party Pokemon
- Step 11: Compile and test that sucker in-game
If you're reading this then you've successfully been suckered into trying to make an actual game using one of pret's gen 3 decomps, poor sap that you are. Maybe your pokecrystal using gen 2 hacker friends told you how convenient and easy to use the pret projects were. And they're right! If you're using pokecrystal that is. But this isn't gen 2, this is gen 3, and gen 3 is a whole different beast.
One of the most immediately recognizable features of a Pokemon game are the battles you have against other trainers, would you like to know how to create and insert one of these into your romhack? Well you've come to the right place.
Step 1: Write your trainer's quotes and come up with their team
This part is simple, right? You just need to write them an opening, beaten, and closing quote like so:
Bug Catcher Tim
lv. 3 Spinarak, lv. 4 Joltik
"What's the matter? Scared of some little spiders?"
"Tangled in a web of defeat!"
"Arachnophobia? Now why would they need to go and invent a word like that?"
This is Tim, he likes spiders.
Step 2: Add the trainer to the overworld using Porymap
Crack open the map you plan on adding the trainer to in Porymap, and add a new object. Adjust the sprite to your liking.
See there on the sidebar where it says Script (NULL)? That's where we'll be linking in the script later once we create it.
Set the trainer type to TRAINER_TYPE_NORMAL and give your trainer a View Radius, typically 1-4 depending on how far you want them to be able to spot you.
To open up the map's scripts file from the decomp, click the button in Porymap that says "Open Map Scripts".
Step 3: Writing your trainer's script
For a stock, basic, totally regular trainer battle, you can pretty much copy and paste this script from the original pokeemerald. You will want to change the code labels and constants here (which I'll go over) to match your new trainer's data and location.
Route2_EventScript_BCTim::
trainerbattle_single TRAINER_BC_TIM, Route2_Text_BCTimIntro, Route2_Text_BCTimDefeated
msgbox Route2_Text_BCTimPostBattle, MSGBOX_AUTOCLOSE
end
The top line with the two colons is the script label. You'll want to change the location and trainer names in it -- in this case Route 2 and BCTim -- to match your own trainer.
The second line is declaring a trainer battle using the trainerbattle_single
(for a single battle) script macro. After the space is the OPPONENT CONSTANT -- in this case, TRAINER_BC_TIM
.
After the trainer constant are two TEXT POINTERS. These correspond to the opening quote (when the trainer runs up to battle you) and the defeat quote (when they slide in on the battle screen after their last Pokemon is defeated).
On the third line is the TEXT POINTER to the post-battle quote (the one when you talk to them on the overworld after beating them).
The fourth line is end
. It ends the script.
Step 4: Defining your trainer's opponent constant
In the script, we used the opponent constant TRAINER_BC_TIM
. But what is an opponent constant?
Well, an opponent constant is a number defined for every trainer battle in the game in include/constants/opponents.h.
In this file, you will see a list of #define
s for all the trainers. In this example, we'll be replacing the first entry on the list after index 0.
You may be wondering, "Why am I replacing a trainer instead of just adding one to the end of the list?" Pokemon Emerald as it is already used up 855 out of a maximum 864 trainer battle slots. The reason these slots are limited as they come is because these opponent constants also handle the save flags for recognizing what trainers you've defeated already. That means adding too many trainers to the game will require adding more flags, and changing how the save file is structured, and rendering any save files you currently have useless. Replacing a trainer is simpler and easier.
So with that out of the way, let's replace opponent constant number 1, TRAINER_SAWYER_1
:
#ifndef GUARD_CONSTANTS_OPPONENTS_H
#define GUARD_CONSTANTS_OPPONENTS_H
#define TRAINER_NONE 0
-#define TRAINER_SAWYER_1 1
+#define TRAINER_BC_TIM 1
#define TRAINER_GRUNT_AQUA_HIDEOUT_1 2
#define TRAINER_GRUNT_AQUA_HIDEOUT_2 3
#define TRAINER_GRUNT_AQUA_HIDEOUT_3 4
Just replace the constant like so. Easy, right? WRONG!
Step 5: Realizing that the opponent constant you just replaced was a Match Call trainer
Noticed that TRAINER_SAWYER_1
has a _1
after it? That's because Sawyer is a Match Call trainer, a trainer that can call you and ask for a rematch at a later point in the game. If you want to create your own Match Call trainers, that's something I don't know how to do yet either. I'll save writing that tutorial for someone more competent (probably me in another month or so).
You don't have to do this part if the trainer you replaced isn't a Match Call trainer.
In src/match_call.c starting on line 115, you'll find sMatchCallTrainers
, which is a list of trainerIds and associated match call data. Use Ctrl+F to find the opponent constant you replaced (TRAINER_SAWYER_1
) in the list, then delete the codeblock starting and ending with the brackets { } there. I have absolutely no idea what this may break, but your game will not build unless you cull that.
Step 6: Culling wherever in the game that old trainer of yours was referenced
Because we're replacing a trainer slot and renaming its opponent constant, we also need to find anywhere that opponent constant was used and gut the scripts.
In stock pokeemerald, you'll need to go to Mt. Chimney on Porymap and delete the overworld sprite object associated with Hiker Sawyer.
You'll also need to comment out the lines where their scripts were. You can do this by adding //
or @
before every line, like so:
// MtChimney_EventScript_Sawyer:: @ 822F208
// trainerbattle_single TRAINER_SAWYER_1, MtChimney_Text_SawyerIntro, MtChimney_Text_SawyerDefeat, MtChimney_EventScript_SawyerDefeated
// specialvar VAR_RESULT, ShouldTryRematchBattle
// compare VAR_RESULT, TRUE
// goto_if_eq MtChimney_EventScript_SawyerRematch
// msgbox MtChimney_Text_SawyerPostBattle, MSGBOX_DEFAULT
// release
// end
// MtChimney_EventScript_SawyerDefeated:: @ 822F234
// special PlayerFaceTrainerAfterBattle
// waitmovement 0
// msgbox MtChimney_Text_SawyerRegister, MSGBOX_DEFAULT
// register_matchcall TRAINER_SAWYER_1
// release
// end
// MtChimney_EventScript_SawyerRematch:: @ 822F253
// trainerbattle_rematch TRAINER_SAWYER_1, MtChimney_Text_SawyerRematchIntro, MtChimney_Text_SawyerRematchDefeat
// msgbox MtChimney_Text_SawyerPostRematch, MSGBOX_AUTOCLOSE
// end
Step 7: Adding in the new text for your trainer
In base pokeemerald, all the text for trainers is stored in data/text/trainers.inc. You don't have to put your new trainer's text here; you can instead put your new text pointers right underneath your trainer script in your map's scripts file. Once you've found the place to put your trainer's text, add it in like so:
Route2_EventScript_BCTim::
trainerbattle_single TRAINER_BC_TIM, Route2_Text_BCTimIntro, Route2_Text_BCTimDefeated
msgbox Route2_Text_BCTimPostBattle, MSGBOX_AUTOCLOSE
end
+Route2_Text_BCTimIntro:
+ .string "What's the matter? Scared of some\n"
+ .string "little spiders?$"
+
+Route2_Text_BCTimDefeated:
+ .string "Tangled in a web of defeat!$"
+
+Route2_Text_BCTimPostBattle:
+ .string "Arachnophobia? Now why would they need\n"
+ .string "to go and invent a word like that?$"
What's that? How do you format those lines of text? My son, my lad, let me teach you a trick to save you time and keep you from having to count out pixels per line line a total troglodyte: abusing Poryscript playground's automatic text formatting function!
Just go on this page and delete everything from the example script but the msgbox(format part, then paste in your new unformatted text like so and voila, instantly formatted text that won't overdraw the textboxes!:
I mean, technically you could write your whole script with poryscript too. But I don't know how to do that, so at least you can format your text instantly with it.
Step 8: Tying your trainer script to the trainer in the map
Now, go back to Porymap and change the part on your NPC that says Script (NULL) by copying and pasting the script label that marks the beginning of your trainer's script here. In this case Route2_EventScript_BCTim
.
Step 9: Defining your trainer's sprite, encounter music, trainer class, AI, and name
For the next step, we'll open up src/data/trainers.h and find the data associated with the opponent constant we replaced. The final result will look like this:
[TRAINER_BC_TIM] =
{
.partyFlags = 0,
.trainerClass = TRAINER_CLASS_BUG_CATCHER,
.encounterMusic_gender = TRAINER_ENCOUNTER_MUSIC_HIKER,
.trainerPic = TRAINER_PIC_BUG_CATCHER,
.trainerName = _("TIM"),
.items = {},
.doubleBattle = FALSE,
.aiFlags = AI_SCRIPT_CHECK_BAD_MOVE | AI_SCRIPT_TRY_TO_FAINT | AI_SCRIPT_CHECK_VIABILITY,
.partySize = ARRAY_COUNT(sParty_BCTim),
.party = {.NoItemDefaultMoves = sParty_BCTim},
},
Here is where everything related to the trainer is linked to the opponent constant. You'll find another huge data list of every trainer in the game. We set quite a few parameters here:
.partyFlags
: Tells the game whether this trainer will have Pokemon with custom moves (F_TRAINER_PARTY_CUSTOM_MOVESET
) and/or held items (F_TRAINER_PARTY_HELD_ITEM
). 0 means they have neither..trainerClass
: Determines the title the trainer's name is prefixed with (e.g. "BUG CATCHER" or "HIKER") and how much money they'll give when they're defeated. There's a list of class constants to be found in include/constants/trainers.h..encounterMusic_gender
: The music that plays when the trainer walks up to battle. A list of constants can be found in include/constants/trainers.h..trainerPic
: Which sprite the trainer uses. Again, a list is found in include/constants/trainers.h..items
: What items (if any) the trainer uses in battle, with a limit of four. For example here's Flannery's items in her first gym battle:{ITEM_HYPER_POTION, ITEM_HYPER_POTION, ITEM_NONE, ITEM_NONE}
. A list of item constants can be found in include/constants/items.h..doubleBattle
: FALSE or TRUE, depending on whether battling this trainer should start a double battle. Note that double battles also require a slightly different script compared to a regular trainer battle and include an extra line of text; this field alone is not enough..aiFlags
: These determine which AI scripts the trainer is allowed to follow. A list can be found in include/constants/battle_ai.h..partySize
and.party
: These two are POINTERS that need to point to the relevant data for your trainer as labeled in trainer_parties.h
Which brings us to our last major part:
Step 10: Defining the trainer's party Pokemon
For this part we'll be in src/data/trainer_parties.h:
static const struct TrainerMonNoItemDefaultMoves sParty_BCTim[] = {
{
.iv = 0,
.lvl = 3,
.species = SPECIES_SPINARAK,
},
{
.iv = 0,
.lvl = 4,
.species = SPECIES_JOLTIK,
}
};
Here we get to give the trainer we made their data! For Bug Catcher Tim, the two defined Pokemon only have two parameters, .iv
and .lvl
along with their species constants.
.lvl
is obviously the Pokemon's level, but if you look around at the parties in stock Emerald you'll quickly notice the .iv parameter isn't simply IVs from 0-31. This value actually runs from 0-255. The game does math to scale this value into the proper 0-31 range. 255 becomes the highest possible IV (31), 0 becomes the lowest possible IV (0). IVs are the same for all stats.
Anyway, Tim is a simple fella with simple mons. But what about Gym Leaders and opponents with craftier strategies like custom movesets and held items on their Pokemon? Well, have a look at Flannery's team from stock Emerald:
static const struct TrainerMonItemCustomMoves sParty_Flannery1[] = {
{
.iv = 200,
.lvl = 24,
.species = SPECIES_NUMEL,
.heldItem = ITEM_NONE,
.moves = {MOVE_OVERHEAT, MOVE_TAKE_DOWN, MOVE_MAGNITUDE, MOVE_SUNNY_DAY}
},
{
.iv = 200,
.lvl = 24,
.species = SPECIES_SLUGMA,
.heldItem = ITEM_NONE,
.moves = {MOVE_OVERHEAT, MOVE_SMOG, MOVE_LIGHT_SCREEN, MOVE_SUNNY_DAY}
},
{
.iv = 250,
.lvl = 26,
.species = SPECIES_CAMERUPT,
.heldItem = ITEM_NONE,
.moves = {MOVE_OVERHEAT, MOVE_TACKLE, MOVE_SUNNY_DAY, MOVE_ATTRACT}
},
{
.iv = 250,
.lvl = 29,
.species = SPECIES_TORKOAL,
.heldItem = ITEM_WHITE_HERB,
.moves = {MOVE_OVERHEAT, MOVE_SUNNY_DAY, MOVE_BODY_SLAM, MOVE_ATTRACT}
}
};
As you can see, at the top where Tim had TrainerMonNoItemDefaultMoves, Flannery has TrainerMonItemCustomMoves to declare that she is a baller and uses Pokemon with items and custom moves. Note that TrainerMonNoItemDefaultMoves vs TrainerMonItemCustomMoves vs other variants need to match .partyFlags
and .party
from the previous step.
The .moves and .item fields are also equally straightforward. Just be sure to also define the empty item and move slots, as Flannery's team does here.
Step 11: Compile and test that sucker in-game
With all our preparations complete, it's time to see Tim in the flesh. MAKE your game and see if he works!
I bet he does, after all, you followed a tutorial :^)