Skip to content

Add Physical Special Split

Matthew Cabral edited this page Jan 22, 2022 · 15 revisions

This tutorial is for adding the per-move physical/special split that was implemented starting in Diamond/Pearl.

Contents

  1. Adding a byte to the move struct
  2. Modifying the category check macro
  3. Modifying the damage calculation logic
  4. Adding the category byte to moves
  5. A Note on AI Scripts

1. Adding a byte to the move struct

The struct that defines the properties of a move are located in include/pokemon.h, so we will be modifying that file to include a byte that determines whether the move is physical or special. We will also be adding some macros to make the values more descriptive. Before editing, the struct looks like this:

struct BattleMove
{
    u8 effect;
    u8 power;
    u8 type;
    u8 accuracy;
    u8 pp;
    u8 secondaryEffectChance;
    u8 target;
    s8 priority;
    u8 flags;
};

All we need to do is add another field to determine if the move is physical, special, or status. I will be using the name "category" for this, but the name can be anything as long as it is consistent. We will also add some new macros for the possible values of this field. Again, the values can be anything as long as consistency is maintained, but I will be using 0 for physical, 1 for special, and 2 for status. After adding these, our modified struct looks like this:

#define MOVE_CATEGORY_PHYSICAL 0
#define MOVE_CATEGORY_SPECIAL 1
#define MOVE_CATEGORY_STATUS 2

struct BattleMove
{
    u8 effect;
    u8 power;
    u8 type;
    u8 accuracy;
    u8 pp;
    u8 secondaryEffectChance;
    u8 target;
    s8 priority;
    u8 flags;
    u8 category;
};

2. Modifying the category check macro

The game uses a function-style macro to check whether a move is physical or special based on its type. The macro lives in include/battle.h and looks like this:

#define IS_TYPE_PHYSICAL(moveType)(moveType < TYPE_MYSTERY)
#define IS_TYPE_SPECIAL(moveType)(moveType > TYPE_MYSTERY)

The macro simply looks at the move and compares the index of its type to TYPE_MYSTERY, which is 0x9. Types are split down the middle by TYPE_MYSTERY; all physical types are before it, and all special types are after it, so a simple comparison does the job. We will change this to look at the category byte of the move instead of the move's type. For my definition, it looks like this:

#define IS_MOVE_PHYSICAL(move)(gBattleMoves[move].category == MOVE_CATEGORY_PHYSICAL)
#define IS_MOVE_SPECIAL(move)(gBattleMoves[move].category == MOVE_CATEGORY_SPECIAL)
#define IS_MOVE_STATUS(move)(gBattleMoves[move].category == MOVE_CATEGORY_STATUS)

3. Modifying the damage calculation logic

Next, we need to update a few checks in the function for calculating the base damage of a move. First, we will look at src/pokemon.c. The function we want here is:

s32 CalculateBaseDamage(struct BattlePokemon *attacker, struct BattlePokemon *defender, u32 move, u16 sideStatus, u16 powerOverride, u8 typeOverride, u8 battlerIdAtk, u8 battlerIdDef)

First, we need to look at the block of code that begins like:

for (i = 0; i < ARRAY_COUNT(sHoldEffectToType); i++)
    {
        if (attackerHoldEffect == sHoldEffectToType[i][0]
            && type == sHoldEffectToType[i][1])

This is the code used to apply bonuses from type-boosting items like Miracle Seed, Charcoal, Magnet, etc. The original function checks whether the type is physical or special and then only boosts the corresponding stat. Since we've made the physical/special split on a per-move basis, we'll just boost both stats to ensure the move gets boosted. This is as simple as removing a check, and when done, the block of code should look like this:

for (i = 0; i < ARRAY_COUNT(sHoldEffectToType); i++)
    {
        if (attackerHoldEffect == sHoldEffectToType[i][0]
            && type == sHoldEffectToType[i][1])
        {
            attack = (attack * (attackerHoldEffectParam + 100)) / 100;
            spAttack = (spAttack * (attackerHoldEffectParam + 100)) / 100;
            break;
        }
    }

Weather

We also need to make a similar change to the boosts that weather provides. In Gen 3, all types that can be boosted by weather are special types, so the code only checks for the boost if a special move is being used. All of the weather checks are conveniently bunched together, so we can just cut this section and paste it outside of the special check block:

if (WEATHER_HAS_EFFECT2)
        {
            if (gBattleWeather & WEATHER_RAIN_TEMPORARY)
            {
                switch (type)
                {
                case TYPE_FIRE:
                    damage /= 2;
                    break;
                case TYPE_WATER:
                    damage = (15 * damage) / 10;
                    break;
                }
            }

            // any weather except sun weakens solar beam
            if ((gBattleWeather & (WEATHER_RAIN_ANY | WEATHER_SANDSTORM_ANY | WEATHER_HAIL_ANY)) && gCurrentMove == MOVE_SOLAR_BEAM)
                damage /= 2;

            // sunny
            if (gBattleWeather & WEATHER_SUN_ANY)
            {
                switch (type)
                {
                case TYPE_FIRE:
                    damage = (15 * damage) / 10;
                    break;
                case TYPE_WATER:
                    damage /= 2;
                    break;
                }
            }

The special check block ends just before the line that says return damage + 2;, so use that as a reference if you're lost in the if blocks. Another detail; if you want to be efficient, you can leave the Solar Beam code in the special block since it will only ever be special, but moving it out of the block does not cause it to stop working--instead, it will just be checked for every move, instead of just for special moves.

Flash Fire

Just like weather, since Flash Fire only affects Fire-type moves, which are all special in Gen 3, the check for Flash Fire only occurs in the special block. It's actually located right after the weather checks, so you can cut and paste this check outside of the block along with them:

if ((gBattleResources->flags->flags[battlerIdAtk] & RESOURCE_FLAG_FLASH_FIRE) && type == TYPE_FIRE)
            damage = (15 * damage) / 10;

Thick Fat

Similarly to Flash Fire, Thick Fat only affects Fire and Ice moves, which are all special. For that reason, the damage calculation divides the special attack of the attacking Pokemon in half to reduce the damage taken. We will need to change this to reduce the base power of the move instead, so that it can apply to any category or move of the affected types: (src/pokemon.c)

if (defender->ability == ABILITY_THICK_FAT && (type == TYPE_FIRE || type == TYPE_ICE))
    gBattleMovePower /= 2;

The next thing we need to do is change the arguments passed to IS_MOVE_PHYSICAL and IS_MOVE_SPECIAL. Originally, they were passed a type argument, but now we want to pass a move argument. There are two instances of these functions in src/pokemon.c, three more in src/battle_script_commands.c, and two more in src/battle_tv.c.

In pokemon.c, the first one originally reads like this:

IS_TYPE_PHYSICAL(type)

We will change this to:

IS_MOVE_PHYSICAL(gCurrentMove)

And the second one we will need to change from:

IS_TYPE_SPECIAL(type)

to

IS_MOVE_SPECIAL(gCurrentMove)

The ones in battle_script_commands.c can also be changed like this, but the argument names are different. The first one (related to the Hustle ability) looks like this:

IS_TYPE_PHYSICAL(moveType)

We will change this to:

IS_MOVE_PHYSICAL(move)

NOTE: For this change only I used move rather than gCurrentMove to match other uses of move in that function, but either should work. The second instance we need to change looks like this originally:

IS_TYPE_PHYSICAL(moveType)

We will change this to:

IS_MOVE_PHYSICAL(gCurrentMove)

The third one also needs to be changed; originally it reads:

!IS_TYPE_PHYSICAL(moveType)

We will change this to:

IS_MOVE_SPECIAL(gCurrentMove)

Note: We have changed !IS_TYPE_PHYSICAL to IS_TYPE_SPECIAL because "not physical" no longer automatically means "special" due to the introduction of the "status" option.

And lastly, in battle_tv.c the two IS_TYPE_PHYSICAL and IS_TYPE_SPECIAL can be respectfully changed to:

IS_MOVE_PHYSICAL(gCurrentMove)

and

IS_MOVE_SPECIAL(gCurrentMove)

4. Adding the category byte to moves

The last step is definitely the most tedious, but it is very simple. We need to go through every move and define whether it is physical, special, or status. The file that defines all move effects is located at src/data/battle_moves.h. There are plenty of resources to find out which one a move is if you do not already know. After adding these, the split is implemented. For reference, here is one example of each possible value of the category byte for my names and values:

{ // MOVE_ICE_PUNCH
	.effect = EFFECT_FREEZE_HIT,
	.power = 75,
	.type = TYPE_ICE,
	.accuracy = 100,
	.pp = 15,
	.secondaryEffectChance = 10,
	.target = MOVE_TARGET_SELECTED,
	.priority = 0,
	.flags = FLAG_MAKES_CONTACT | FLAG_PROTECT_AFFECTED | FLAG_MIRROR_MOVE_AFFECTED,
	.category = MOVE_CATEGORY_PHYSICAL,
},
{ // MOVE_GUST
	.effect = EFFECT_GUST,
	.power = 40,
	.type = TYPE_FLYING,
	.accuracy = 100,
	.pp = 35,
	.secondaryEffectChance = 0,
	.target = MOVE_TARGET_SELECTED,
	.priority = 0,
	.flags = FLAG_PROTECT_AFFECTED | FLAG_MIRROR_MOVE_AFFECTED | FLAG_KINGSROCK_AFFECTED,
	.category = MOVE_CATEGORY_SPECIAL,
},
{ // MOVE_SAND_ATTACK
	.effect = EFFECT_ACCURACY_DOWN,
	.power = 0,
	.type = TYPE_GROUND,
	.accuracy = 100,
	.pp = 15,
	.secondaryEffectChance = 0,
	.target = MOVE_TARGET_SELECTED,
	.priority = 0,
	.flags = FLAG_PROTECT_AFFECTED | FLAG_MAGICCOAT_AFFECTED | FLAG_MIRROR_MOVE_AFFECTED,
	.category = MOVE_CATEGORY_STATUS,
},

5. A Note on AI Scripts

The last thing that should be noted is how AI Scripts relating to physical/special moves work in these games. The file data/battle_ai_scripts.s contains the code for the battle AI and how it chooses which moves to use. The way it does this is not by calling the macro that the damage calculation code uses; instead, it has multiple internal tables used to keep track of which types are physical and which are special. Note that this does not affect any actual damage calculation, and its effect on the AI is limited in the first place. The three things affected are:

  1. The likelihood of the AI using an attack-lowering vs. a special attack-lowering move
  2. The likelihood of the AI using Reflect vs. Light Screen
  3. The likelihood of the AI using Counter vs. Mirror Coat

An important note is that these decisions are all made based on the types of the opposing Pokemon itself rather than the moves it has, so the original check is pretty bad to begin with--in fact, it's arguable whether you would even notice the difference. Correcting the AI to check for the category of moves instead of the types of Pokemon is beyond the scope of this tutorial and is more suited to a general AI overhaul tutorial, but I wanted to mention it here to be totally clear about what's happening with these calculations.

Clone this wiki locally