Seen 2 Hours Ago
Posted March 3rd, 2021
6 posts
1.3 Years
Hello, and welcome. Here is my first tutorial, which will explain what I've been working on the last week.
For those who played Pokemon X/Y, you may remember the Sky Battles :
It was a special battle where you could only use flying-type pokemon and pokemon that had levitate as their ability. Because I am working on an X/Y demake, I had to create it, so there you are.
First, we will open src/battle_util.c and add these 3 functions :
Spoiler:
bool8 CanMonParticipateInSkyBattle(struct Pokemon* pokemon) {
    int i;
    bool8 isFlyingType = FALSE;
    bool8 isLevitating = GetMonData(pokemon, MON_DATA_ABILITY_NUM, NULL) == ABILITY_LEVITATE;
    bool8 hasFlyMove = FALSE;
    if (GetMonData(pokemon, MON_DATA_SANITY_HAS_SPECIES) && !GetMonData(pokemon, MON_DATA_IS_EGG))
    {
        u16 species = GetMonData(pokemon, MON_DATA_SPECIES);
        isFlyingType = gBaseStats[species].type1 == TYPE_FLYING || gBaseStats[species].type2 == TYPE_FLYING;
        if (isFlyingType)
        {
            gSpecialVar_Result = TRUE;
            return;
        }
    }
    return (isFlyingType || isLevitating) && !IsMonBannedFromSkyBattles(GetMonData(pokemon, MON_DATA_SPECIES2));
}

bool8 IsMonBannedFromSkyBattles(u16 species) {
    switch (species) {
    case SPECIES_EGG:
        return TRUE;
    case SPECIES_SPEAROW:
        return TRUE;
    case SPECIES_FARFETCHD:
        return TRUE;
    case SPECIES_DODUO:
        return TRUE;
    case SPECIES_DODRIO:
        return TRUE;
    case SPECIES_HOOTHOOT:
        return TRUE;
    case SPECIES_NATU:
        return TRUE;
    case SPECIES_MURKROW:
        return TRUE;
    case SPECIES_DELIBIRD:
        return TRUE;
    case SPECIES_TAILLOW:
        return TRUE;
    case SPECIES_STARLY:
        return TRUE;
    case SPECIES_CHATOT:
        return TRUE;
    case SPECIES_SHAYMIN:
        return TRUE;
    case SPECIES_PIDOVE:
        return TRUE;
    case SPECIES_ARCHEN:
        return TRUE;
    case SPECIES_DUCKLETT:
        return TRUE;
    case SPECIES_RUFFLET:
        return TRUE;
    case SPECIES_VULLABY:
        return TRUE;
    case SPECIES_FLETCHLING:
        return TRUE;
    case SPECIES_HAWLUCHA:
        return TRUE;
    case SPECIES_ROWLET:
        return TRUE;
    case SPECIES_PIKIPEK:
        return TRUE;
    default:
        return FALSE;
    }
}

bool8 IsMoveBannedFromSkyBattles(u16 move) {
    switch (move) {
    case MOVE_BODY_SLAM:
        return TRUE;
    case MOVE_BULLDOZE:
        return TRUE;
    case MOVE_DIG:
        return TRUE;
    case MOVE_DIVE:
        return TRUE;
    case MOVE_EARTH_POWER:
        return TRUE;
    case MOVE_EARTHQUAKE:
        return TRUE;
    case MOVE_ELECTRIC_TERRAIN:
        return TRUE;
    case MOVE_FIRE_PLEDGE:
        return TRUE;
    case MOVE_FISSURE:
        return TRUE;
    case MOVE_FLYING_PRESS:
        return TRUE;
    case MOVE_FRENZY_PLANT:
        return TRUE;
    case MOVE_GEOMANCY:
        return TRUE;
    case MOVE_GRASS_KNOT:
        return TRUE;
    case MOVE_GRASS_PLEDGE:
        return TRUE;
    case MOVE_GRASSY_TERRAIN:
        return TRUE;
    case MOVE_GRAVITY:
        return TRUE;
    case MOVE_HEAT_CRASH:
        return TRUE;
    case MOVE_HEAVY_SLAM:
        return TRUE;
    case MOVE_INGRAIN:
        return TRUE;
    case MOVE_LANDS_WRATH:
        return TRUE;
    case MOVE_MAGNITUDE:
        return TRUE;
    case MOVE_MAT_BLOCK:
        return TRUE;
    case MOVE_MISTY_TERRAIN:
        return TRUE;
    case MOVE_MUD_SPORT:
        return TRUE;
    case MOVE_MUDDY_WATER:
        return TRUE;
    case MOVE_ROTOTILLER:
        return TRUE;
    case MOVE_SEISMIC_TOSS:
        return TRUE;
    case MOVE_SLAM:
        return TRUE;
    case MOVE_SMACK_DOWN:
        return TRUE;
    case MOVE_SPIKES:
        return TRUE;
    case MOVE_STOMP:
        return TRUE;
    case MOVE_SUBSTITUTE:
        return TRUE;
    case MOVE_SURF:
        return TRUE;
    case MOVE_THOUSAND_ARROWS:
        return TRUE;
    case MOVE_THOUSAND_WAVES:
        return TRUE;
    case MOVE_TOXIC_SPIKES:
        return TRUE;
    case MOVE_WATER_PLEDGE:
        return TRUE;
    case MOVE_WATER_SPORT:
        return TRUE;
    default:
        return FALSE;
    }
}

Just before I explain this, go into include/battle_util.h, and add this:
bool8 CanMonParticipateInSkyBattle(struct Pokemon* pokemon);
bool8 IsMonBannedFromSkyBattles(u16 species);
bool8 IsMoveBannedFromSkyBattles(u16 move);
It has to be before the #endif
The first function, CanMonParticipateInSkyBattle, checks if our pokemon has the ability levitate, or if the pokemon is a flying type. In my version, I decided to add a new condition: if our pokemon knows fly, he can participate too. Here's my modified CanMonParticipateInSkyBattle function, if you want to implement this :
Spoiler:
bool8 CanMonParticipateInSkyBattle(struct Pokemon* pokemon) {
    int i;
    bool8 isFlyingType = FALSE;
    bool8 isLevitating = GetMonData(pokemon, MON_DATA_ABILITY_NUM, NULL) == ABILITY_LEVITATE;
    bool8 hasFlyMove = FALSE;
    if (GetMonData(pokemon, MON_DATA_SANITY_HAS_SPECIES) && !GetMonData(pokemon, MON_DATA_IS_EGG))
    {
        u16 species = GetMonData(pokemon, MON_DATA_SPECIES);
        isFlyingType = gBaseStats[species].type1 == TYPE_FLYING || gBaseStats[species].type2 == TYPE_FLYING;
        if (isFlyingType)
        {
            gSpecialVar_Result = TRUE;
            return;
        }
    }
    // Fly Move Mod
    for (i = 0; i < MAX_MON_MOVES; i++)
    {
        u16 existingMove = GetMonData(pokemon, MON_DATA_MOVE1 + i, NULL);
        if (existingMove == MOVE_FLY)
        {
            hasFlyMove = TRUE;
        }
    }
    // -- Fly Move Mod - End
    return (isFlyingType || isLevitating) && (!IsMonBannedFromSkyBattles(GetMonData(pokemon, MON_DATA_SPECIES2)) || hasFlyMove);
}

The two other functions, IsMonBannedFromSkyBattles and IsMoveBannedFromSkyBattles, will be used later. In pokemon X/Y, some pokemon couldn't join the battle, like the first stage of bird pokemon, hawlucha, etc. Every pokemon that was not flying visually. Also, some moves were banned, like ingrain for example: how would you use this in the sky.
Now, we are going to create two special commands. Let's open src/field_specials.c and add these two functions :
Spoiler:
void CanDoSkyBattle(void)
{
    int i;
    for (i = 0; i < CalculatePlayerPartyCount(); i++)
    {
        struct Pokemon* pokemon = &gPlayerParty[i];

        if (CanMonParticipateInSkyBattle(pokemon) && GetMonData(pokemon, MON_DATA_HP, NULL) > 0)
        {
            gSpecialVar_Result = TRUE;
            return;
        }
    }
    gSpecialVar_Result = FALSE;
}

void PrepareSkyBattle(void)
{
    int i, var;
    u8 partyCount;
    u16 species;
    partyCount = CalculatePlayerPartyCount();
    SavePlayerParty();
    FlagSet(FLAG_IS_IN_SKY_BATTLE);
    var = 0;
    for (i = 0; i < partyCount; i++)
    {
        struct Pokemon* pokemon = &gPlayerParty[i];

        if (CanMonParticipateInSkyBattle(pokemon)) // We do not remove fainted pokemon, because the player can revive them with Revives. The CanDoSkyBattle already handles this.
            var += 1 << i; // This part saves the value of the pokemon that participate or not, and this allows us to save them after the battle, instead of loading them back from before the battle, so they lose HP, PP, etc.
        else
            ZeroMonData(pokemon);
    }
    VarSet(VAR_SKY_BATTLE_POKEMON_POSITIONS, var);
    CompactPartySlots();
}

Also add this at the top of the file.
#include "load_save.h"
#include "constants/abilities.h"
Let's register them in data/specials.inc and add these two lines at the end of the file, following the others:
def_special CanDoSkyBattle
def_special PrepareSkyBattle
The first special will be used in a script. It will help us to check if the player has at least one alive pokemon that can participate. If it's the case, we can compare VAR_RESULT and use the special PrepareSkyBattle. This one will remember the position of the pokemons that will participate in the party and will remove the others. Once these two specials will be activated, you can just use the trainerbattle command. I don't know yet how I can change the background, but I'll edit the tutorial when I'll find a solution.
As you can see, the second special sets a flag and a var. They don't exist in the game, so you'll have to create them. Open include/constants/flags.h and include/constants/vars.h. In these files, find an unused flag and var, and rename the unused flag FLAG_IS_IN_SKY_BATTLE and the unused var VAR_SKY_BATTLE_POKEMON_POSITIONS. For me, I choose 0x71 for the flag and 0x404E for the var.
Now, we need to handle the end of the battle. Let's open src/battle_setup.c and go in the static void CB2_EndTrainerBattle(void) function, and add this just before this line : "else if (IsPlayerDefeated(gBattleOutcome) == TRUE)"
Spoiler:
else if (FlagGet(FLAG_IS_IN_SKY_BATTLE))
    {
        FlagClear(FLAG_IS_IN_SKY_BATTLE);
        UpdatePlayerSavedPartyAfterSkyBattle(); // Only updates the pokemon that participated in the battle
        LoadPlayerParty(); // Load the saved pokemons
        if (IsPlayerDefeated(gBattleOutcome) == TRUE) { // Check if every pokemon is fainted;
            everyPokemonFainted = TRUE;
            partyCount = CalculatePlayerPartyCount();
            for (i = 0; i < partyCount; i++)
            {
                if (GetMonData(&gPlayerParty[i], MON_DATA_HP, NULL) > 0)
                {
                    everyPokemonFainted = FALSE;
                    break;
                }
            }
            if (everyPokemonFainted && !FlagGet(FLAG_LOSING_BATTLE_CONTINUES_SCRIPT))
                SetMainCallback2(CB2_WhiteOut);
            else
                SetMainCallback2(CB2_ReturnToFieldContinueScriptPlayMapMusic);
        }
        SetMainCallback2(CB2_ReturnToFieldContinueScriptPlayMapMusic);
    }

You also need to add this before this line "if gTrainerBattleOpponent_A == TRAINER_SECRET_BASE"
bool8 everyPokemonFainted;
u8 partyCount;
u8 i;
Finally, at the top of the file, add
#include "load_save.h"
Let's explain this. If we are in a Sky Battle, the first thing we do is reset the flag, so the next battle won't be considered as a Sky Battle. Then, we use a function we don't have added yet. We want to load back the pokemon that we saved before we remove our pokemons in the battle, however, we need to update the ones which participated (else, they won't lose HP, PP, etc). Finally, we load back every mon. Then, we have to do another check, if the player loses. If the player lost the battle, we check if there is at least one pokemon in the party with more than 1HP. If that condition is false, the player blackouts.
Warning: Since we only edit CB2_EndTrainerBattle, our Sky Battle system does not apply to wild battles. If you want to implement this and understand the code I gave, you can easily adapt this.
We're almost there! Let's open src/load_save.c and add this function:
Spoiler:
void UpdatePlayerSavedPartyAfterSkyBattle(void)
{
    int i;
    int c = 0;
    int skyPokemonVar = VarGet(VAR_SKY_BATTLE_POKEMON_POSITIONS);

    for (i = 0; i < PARTY_SIZE; i++)
        if ((skyPokemonVar >> i & 1) == 1) {
            gSaveBlock1Ptr->playerParty[i] = gPlayerParty[c];
            c++;
        }
}

We need to register this function in include/load_save.h, so we need to add this line
void UpdatePlayerSavedPartyAfterSkyBattle(void);
We need to change one last file ! Let's go in src/battle_scripts_command.c
Find the "Cmd_attackcanceler" function, and change the line "if (NoTargetPresent(gCurrentMove))" by
if (NoTargetPresent(gCurrentMove) || (FlagGet(FLAG_IS_IN_SKY_BATTLE) && IsMoveBannedFromSkyBattles(gCurrentMove)))
That will make any banned move fail while used in a sky battle.
Aaaand it's done! Now, if you want to create a Sky Battle using a script, you just have to use this:
special CanDoSkyBattle
compare VAR_RESULT, FALSE
goto_if_eq [Script where you can't do a Sky battle]
special PrepareSkyBattle
trainerbattle TRAINER_BATTLE_CONTINUE_SCRIPT [Your arguments for the battle]
Thanks for reading! If there's any bug or thing I forgot in the tutorial, please let me know!