2 Change Enemy Trainer Parties Depending on Difficulty
TheSylphIsIn edited this page 2023-01-27 01:41:34 -05:00

Implementation by Sylph. Inspired by Epic Battle Fantasy 5.

This tutorial is for adding a feature similar to Black 2/White 2's Easy and Challenge modes. The player can choose a difficulty setting, which changes which party enemy trainers will use.

Part 1: Adding difficulty options

a. Make constants for each difficulty

For ease of use, go to include/constants/global.h and define some constants there. I chose to put them below the options constants for reasons which will become clear in a moment.

#define OPTIONS_BATTLE_STYLE_SHIFT 0
#define OPTIONS_BATTLE_STYLE_SET 1

+ #define DIFFICULTY_EASY 0
+ #define DIFFICULTY_NORMAL 1
+ #define DIFFICULTY_HARD 2

NOTE

I will be adding this option to the save block and the options menu. If you don't want to do that (to avoid editing the save block, to avoid editing the options menu, because you don't want the player to be able to change difficulties after selecting, or whatever other reason), this can still work. Simply use an overworld script to store the player's choice in a var, and skip to Part 2.

b. Put the Difficulty choice in the Save Block

We'll be replacing the "Battle Style" option with Difficulty, since that's essentially what the original battle style option was for anyway. Again, if you don't want to replace any options, store difficulty in a var and skip to Part 2.

Go to include/global.h and search for "options" to find the beginning of SaveBlock2. Conveniently, these options are split among the bits of a 16-bit number of which only 12 are used, so we don't need to change anything else to add more to optionsBattleStyle.

             u16 optionsTextSpeed:3; // OPTIONS_TEXT_SPEED_[SLOW/MID/FAST]
             u16 optionsWindowFrameType:5; // Specifies one of the 20 decorative borders for text boxes
             u16 optionsSound:1; // OPTIONS_SOUND_[MONO/STEREO]
-            u16 optionsBattleStyle:1; // OPTIONS_BATTLE_STYLE_[SHIFT/SET]
+            u16 optionsBattleStyle:2; // DIFFICULTY_[EASY/NORMAL/MEDIUM]
             u16 optionsBattleSceneOff:1; // whether battle animations are disabled
             u16 regionMapZoom:1; // whether the map is zoomed in
-            //u16 padding1:4;
+            //u16 padding1:3;

u16 optionsBattleStyle:2 gives two bits, so four possible difficulties. You could do more if you want, but that might be a bit excessive.

c. Name the difficulty options

Let's define some new strings for displaying the names of our choices in the options menu. include/strings.h:

+ extern const u8 gText_BattleStyleEasy[];
+ extern const u8 gText_BattleStyleNormal[];
+ extern const u8 gText_BattleStyleHard[];

src/strings.c:

+ const u8 gText_BattleStyleEasy[] = _("{COLOR GREEN}{SHADOW LIGHT_GREEN}EASY     "); 
+ const u8 gText_BattleStyleNormal[] = _("{COLOR GREEN}{SHADOW LIGHT_GREEN}NORMAL");
+ const u8 gText_BattleStyleHard[] = _("{COLOR GREEN}{SHADOW LIGHT_GREEN}HARD     ");

Notice the extra spaces after the shorter names. This is a quick and dirty fix to make sure the name of the previous option won't be visible "underneath" the next one if the names aren't all the same length, since the options menu doesn't erase them.

d. Decide what to do about shift mode

Since we're replacing the option that controls Shift/Set mode, we'll have to decide what to do with it instead. This is controlled by this line in data/battle_scripts_1.s:

jumpifbyte CMP_EQUAL, sBATTLE_STYLE, OPTIONS_BATTLE_STYLE_SET, BattleScript_FaintedMonSendOutNew

If this evaluates to true, the "Opponent is about to send out...switch Pokemon?" prompt will be skipped.

What to do about this is up to developer discretion. I changed it to:

jumpifbyte CMP_GREATER_THAN, sBATTLE_STYLE, DIFFICULTY_EASY, BattleScript_FaintedMonSendOutNew

so that the player will only be prompted to switch on Easy mode.

e. Add Difficulty to the Options menu

If you want, you can change gText_BattleStyle in src/strings.c from "BATTLE STYLE" to "DIFFICULTY" to change the label in the options menu.

Then go to src/option_menu.c.

Replace BattleStyle_ProcessInput with this, modified from frame type:

static u8 BattleStyle_ProcessInput(u8 selection)
{
    if (JOY_NEW(DPAD_LEFT))
    {
        if (selection != 0)
			selection--;
        sArrowPressed = TRUE;
    }
    if (JOY_NEW(DPAD_RIGHT))
    {
	if (selection < DIFFICULTY_HARD)
		selection++;
	sArrowPressed = TRUE;
    }

    return selection;
}

Replace BattleStyle_DrawChoices with:

static void BattleStyle_DrawChoices(u8 selection)
{
    switch (selection)
	{
		case 0:
			DrawOptionMenuChoice(gText_BattleStyleEasy, 132, YPOS_BATTLESTYLE, 1);
			break;
		case 1:
			DrawOptionMenuChoice(gText_BattleStyleNormal, 132, YPOS_BATTLESTYLE, 1);
			break;
		case 2:
			DrawOptionMenuChoice(gText_BattleStyleHard, 132, YPOS_BATTLESTYLE, 1);
			break;
	}
}

and that's that.

Part 2: Adding more Trainer Parties

There are two possible approaches to this.

  1. Edit the defintion of the Trainer struct to include more parties, and choose between the parties depending on difficulty.
  2. Make copies of the gTrainers array, and choose which array to select the party from depending on difficulty.

I prefer the first method, because it's more space-efficient and doesn't introduce the possibility of data redundancy errors. But if you're averse to editing the trainer struct or want to be able to vary the parties even more between difficulties (having a separate array makes party flags or party sizes more flexible), you could go for the second one.

Option 1: Edit the Trainer struct

Simply find the definition of struct Trainer in include/data.h and add more union TrainerMonPtrs to it. I also took the liberty of splitting partySize so that trainers can have bigger parties on hard:

struct Trainer
{
    /*0x00*/ u8 partyFlags;
    /*0x01*/ u8 trainerClass;
    /*0x02*/ u8 encounterMusic_gender; // last bit is gender
    /*0x03*/ u8 trainerPic;
    /*0x04*/ u8 trainerName[TRAINER_NAME_LENGTH + 1];
    /*0x10*/ u16 items[MAX_TRAINER_ITEMS];
    /*0x18*/ bool8 doubleBattle;
    /*0x1C*/ u32 aiFlags;
    /*0x20*/ u8 partySize:4; // normal party size
+			 u8 hardPartySize:4; 
    /*0x24*/ union TrainerMonPtr party;
+			 union TrainerMonPtr easyParty;
+			 union TrainerMonPtr hardParty;
};

Edit the party macros

We just added two new fields, so let's edit the party macros in include/data.h to account for these fields:

-#define NO_ITEM_DEFAULT_MOVES(party) { .NoItemDefaultMoves = party }, .partySize = ARRAY_COUNT(party), .partyFlags = 0
-#define NO_ITEM_CUSTOM_MOVES(party) { .NoItemCustomMoves = party }, .partySize = ARRAY_COUNT(party), .partyFlags = F_TRAINER_PARTY_CUSTOM_MOVESET
-#define ITEM_DEFAULT_MOVES(party) { .ItemDefaultMoves = party }, .partySize = ARRAY_COUNT(party), .partyFlags = F_TRAINER_PARTY_HELD_ITEM
-#define ITEM_CUSTOM_MOVES(party) { .ItemCustomMoves = party }, .partySize = ARRAY_COUNT(party), .partyFlags = F_TRAINER_PARTY_CUSTOM_MOVESET | F_TRAINER_PARTY_HELD_ITEM
+#define NO_ITEM_DEFAULT_MOVES(party) { .NoItemDefaultMoves = sParty_##party }, .partySize = ARRAY_COUNT(sParty_##party), .partyFlags = 0, .hardParty = { .NoItemDefaultMoves = sHardParty_##party }, .hardPartySize = ARRAY_COUNT(sHardParty_##party), .easyParty = { .NoItemDefaultMoves = sEasyParty_##party }
+#define NO_ITEM_CUSTOM_MOVES(party) { .NoItemCustomMoves = sParty_##party }, .partySize = ARRAY_COUNT(sParty_##party), .partyFlags = F_TRAINER_PARTY_CUSTOM_MOVESET, .hardParty = { .NoItemCustomMoves = sHardParty_##party }, .hardPartySize = ARRAY_COUNT(sHardParty_##party), .easyParty = { .NoItemCustomMoves = sEasyParty_##party }
+#define ITEM_DEFAULT_MOVES(party) { .ItemDefaultMoves = sParty_##party }, .partySize = ARRAY_COUNT(sParty_##party), .partyFlags = F_TRAINER_PARTY_HELD_ITEM, .hardParty = { .ItemDefaultMoves = sHardParty_##party }, .hardPartySize = ARRAY_COUNT(sHardParty_##party), .easyParty = { .ItemDefaultMoves = sEasyParty_##party }
+#define ITEM_CUSTOM_MOVES(party) { .ItemCustomMoves = sParty_##party }, .partySize = ARRAY_COUNT(sParty_##party), .partyFlags = F_TRAINER_PARTY_CUSTOM_MOVESET | F_TRAINER_PARTY_HELD_ITEM, .hardParty = { .ItemCustomMoves = sHardParty_##party }, .hardPartySize = ARRAY_COUNT(sHardParty_##party), .easyParty = { .ItemCustomMoves = sEasyParty_##party }

They look a bit unruly at first glance, but they simplify gTrainers. Simply enter the trainer's name (without the sParty_ prefix), and the macro will properly assign all the parties IF you follow the naming convention of the normal party's label being sParty_(name), the easy party being sEasyParty_(name), and the hard party being sHardParty_(name). But that's good practice anyway, and you can edit the macros (or ignore them) if you dislike that.

It's likely that not every trainer will have separate parties for every difficulty, so here are some macros that assign the normal party to all fields:

#define NO_ITEM_DEFAULT_MOVES_NO_HARD(party) { .NoItemDefaultMoves = sParty_##party }, .partySize = ARRAY_COUNT(sParty_##party), .partyFlags = 0, .hardParty = { .NoItemDefaultMoves = sParty_##party }, .hardPartySize = ARRAY_COUNT(sParty_##party), .easyParty = { .NoItemDefaultMoves = sParty_##party }
#define NO_ITEM_CUSTOM_MOVES_NO_HARD(party) { .NoItemCustomMoves = sParty_##party }, .partySize = ARRAY_COUNT(sParty_##party), .partyFlags = F_TRAINER_PARTY_CUSTOM_MOVESET, .hardParty = { .NoItemCustomMoves = sParty_##party }, .hardPartySize = ARRAY_COUNT(sParty_##party), .easyParty = { .NoItemCustomMoves = sParty_##party }
#define ITEM_DEFAULT_MOVES_NO_HARD(party) { .ItemDefaultMoves = sParty_##party }, .partySize = ARRAY_COUNT(sParty_##party), .partyFlags = F_TRAINER_PARTY_HELD_ITEM, .hardParty = { .ItemDefaultMoves = sParty_##party }, .hardPartySize = ARRAY_COUNT(sParty_##party), .easyParty = { .ItemDefaultMoves = sParty_##party }
#define ITEM_CUSTOM_MOVES_NO_HARD(party) { .ItemCustomMoves = sParty_##party }, .partySize = ARRAY_COUNT(sParty_##party), .partyFlags = F_TRAINER_PARTY_CUSTOM_MOVESET | F_TRAINER_PARTY_HELD_ITEM, .hardParty = { .ItemCustomMoves = sParty_##party }, .hardPartySize = ARRAY_COUNT(sParty_##party), .easyParty = { .ItemCustomMoves = sParty_##party }

Note: If you're installing this in a fresh pokeemerald: using these macros, then going to src/data/trainers.h and Find/Replacing MOVES(sParty_ for MOVES_NO_HARD( should assure that nothing will break if you fight vanilla trainers without giving them different parties.

Option 2: Duplicate gTrainers

This is comparatively simple. Simply go to include/data.h and duplicate the declaration there:

extern const struct Trainer gTrainers[];
+extern const struct Trainer gEasyTrainers[];
+extern const struct Trainer gHardTrainers[];

Then copy the entirety of gTrainers, paste it twice, and relabel the copies to gEasyTrainers and gHardTrainers. This wastes some space with redundant data.

I prefer the other method because it's more space-efficient and doesn't introduce the possibility of data redundancy errors, but to each their own.

Assigning difficulty-dependent parties

Option 1

Create two new parties in src/data/trainer_parties.h, labeled s(difficulty)Party_(name), and make sure the trainer in question isn't using the NO_HARD macro. Note: Parties must use the same party flags between difficulties, and the normal/easy parties need to have the same number of Pokemon. You can remove these limitations by further editing struct Trainer, but I chose to keep it simple for this tutorial.

Option 2

Create a new party in src/data/trainer_parties.h for the difficulty you want, and assign it to the relevant g(difficulty)Trainers array. Make sure it has a unique label.

Part 3: Using the Difficulty option to select the correct Trainer party

For this we'll have to edit the CreateNPCTrainerParty function in src/battle_main.c.

Note: If you chose to use a var instead of the save file for difficulty, replace all instances of gSaveBlock2Ptr->optionsBattleStyle with VarGet(<Your var>) in the following code.

a. Read the proper party size

If you added new party size values to struct Trainer, we'll have to make sure the game reads them. Find the first usage of monsCount:

+if (gSaveBlock2Ptr->optionsBattleStyle == DIFFICULTY_HARD)
+    monsCount = gTrainers[trainerNum].hardPartySize;
+else
+    monsCount = gTrainers[trainerNum].partySize;
 if (gBattleTypeFlags & BATTLE_TYPE_TWO_OPPONENTS)
 {
-     if (gTrainers[trainerNum].partySize > PARTY_SIZE / 2)
+     if (monsCount > PARTY_SIZE / 2)
           monsCount = PARTY_SIZE / 2;
-else
-     monsCount = gTrainers[trainerNum].partySize;
-}
-else
-{
-     monsCount = gTrainers[trainerNum].partySize;
-}

if you duplicated gTrainers, you don't need to touch that at all.

b. Reading the party itself

Go to the switch where the game reads the party flags. We want to assign different parties to *partyData depending on the difficulty. However, there are separate instances of it for each party flag case. So, we need to take this code:

-const struct TrainerMonNoItemDefaultMoves *partyData = gTrainers[trainerNum].party.NoItemDefaultMoves;
+const struct TrainerMonNoItemDefaultMoves *partyData;
+if (gSaveBlock2Ptr->optionsBattleStyle == DIFFICULTY_HARD)
+	partyData = gTrainers[trainerNum].hardParty.NoItemDefaultMoves;
+else if (gSaveBlock2Ptr->optionsBattleStyle == DIFFICULTY_EASY)
+	partyData = gTrainers[trainerNum].easyParty.NoItemDefaultMoves;
+else
+	partyData = gTrainers[trainerNum].party.NoItemDefaultMoves;

and put it in all four cases, using the appropriate struct for each one (NoItemDefaultMoves/NoItemCustomMoves/ItemDefaultMoves/ItemCustomMoves) for each one.

If you're using the duplicate gTrainers method, that would be:

-const struct TrainerMonNoItemDefaultMoves *partyData = gTrainers[trainerNum].party.NoItemDefaultMoves;
+const struct TrainerMonNoItemDefaultMoves *partyData;
+if (gSaveBlock2Ptr->optionsBattleStyle == DIFFICULTY_HARD)
+	partyData = gHardTrainers[trainerNum].party.NoItemDefaultMoves;
+else if (gSaveBlock2Ptr->optionsBattleStyle == DIFFICULTY_EASY)
+	partyData = gEasyTrainers[trainerNum].party.NoItemDefaultMoves;
+else
+	partyData = gTrainers[trainerNum].party.NoItemDefaultMoves;

And...that's that. If you so choose, you can tweak the specifics of the system to your liking, but this is the basic gist of how to get it working.

Example parties

static const struct TrainerMonItemCustomMoves sParty_Brawly1[] = {
    {
    .iv = 100,
    .lvl = 16,
    .species = SPECIES_MACHOP,
    .heldItem = ITEM_NONE,
    .moves = {MOVE_KARATE_CHOP, MOVE_LOW_KICK, MOVE_SEISMIC_TOSS, MOVE_BULK_UP}
    },
    {
    .iv = 100,
    .lvl = 16,
    .species = SPECIES_MEDITITE,
    .heldItem = ITEM_NONE,
    .moves = {MOVE_FOCUS_PUNCH, MOVE_LIGHT_SCREEN, MOVE_REFLECT, MOVE_BULK_UP}
    },
    {
    .iv = 200,
    .lvl = 19,
    .species = SPECIES_MAKUHITA,
    .heldItem = ITEM_SITRUS_BERRY,
    .moves = {MOVE_ARM_THRUST, MOVE_VITAL_THROW, MOVE_REVERSAL, MOVE_BULK_UP}
    }
};

static const struct TrainerMonItemCustomMoves sEasyParty_Brawly1[] = {
    {
    .iv = 50,
    .lvl = 16,
    .species = SPECIES_MACHOP,
    .heldItem = ITEM_NONE,
    .moves = {MOVE_KARATE_CHOP, MOVE_LOW_KICK, MOVE_LEER, MOVE_NONE}
    },
    {
    .iv = 50,
    .lvl = 15,
    .species = SPECIES_MEDITITE,
    .heldItem = ITEM_NONE,
    .moves = {MOVE_FOCUS_PUNCH, MOVE_LIGHT_SCREEN, MOVE_REFLECT, MOVE_BULK_UP}
    },
    {
    .iv = 100,
    .lvl = 17,
    .species = SPECIES_MAKUHITA,
    .heldItem = ITEM_SITRUS_BERRY,
    .moves = {MOVE_ARM_THRUST, MOVE_VITAL_THROW, MOVE_FAKE_OUT, MOVE_BULK_UP}
    }
};

static const struct TrainerMonItemCustomMoves sHardParty_Brawly1[] = {
    {
    .iv = 150,
    .lvl = 16,
    .species = SPECIES_MACHOP,
    .heldItem = ITEM_BLACK_BELT,
    .moves = {MOVE_KARATE_CHOP, MOVE_ROCK_TOMB, MOVE_SEISMIC_TOSS, MOVE_BULK_UP}
    },
    {
    .iv = 150,
    .lvl = 16,
    .species = SPECIES_MEDITITE,
    .heldItem = ITEM_NONE,
    .moves = {MOVE_HI_JUMP_KICK, MOVE_CONFUSION, MOVE_STRENGTH, MOVE_BULK_UP}
    },
    {
    .iv = 150,
    .lvl = 17,
    .species = SPECIES_SHROOMISH,
    .heldItem = ITEM_NONE,
    .moves = {MOVE_STUN_SPORE, MOVE_LEECH_SEED, MOVE_HEADBUTT, MOVE_MEGA_DRAIN}
    },
    {
    .iv = 200,
    .lvl = 19,
    .species = SPECIES_MAKUHITA,
    .heldItem = ITEM_SITRUS_BERRY,
    .moves = {MOVE_SEISMIC_TOSS, MOVE_VITAL_THROW, MOVE_REVERSAL, MOVE_BULK_UP}
    }
};