From 3b3de80648f576d5985ec696012c54e9a96b180a Mon Sep 17 00:00:00 2001 From: Thomas Winwood Date: Tue, 19 Nov 2019 12:49:14 +0000 Subject: [PATCH] Created How to add a new ability (markdown) --- How-to-add-a-new-ability.md | 388 ++++++++++++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 How-to-add-a-new-ability.md diff --git a/How-to-add-a-new-ability.md b/How-to-add-a-new-ability.md new file mode 100644 index 0000000..0212d43 --- /dev/null +++ b/How-to-add-a-new-ability.md @@ -0,0 +1,388 @@ +This tutorial is for how to add a new ability. As an example, we'll add Snow Warning. + + +## Contents + +1. [Define an ability constant](#1-define-an-ability-constant) +2. [Give it a name and description](#2-give-it-a-name-and-description) +3. [Make it do something](#3-make-it-do-something) + 1. [Add a new weather constant](#i-add-a-new-weather-constant) + 2. [Add a new battle script](#ii-add-a-new-battle-script) + 3. [Add a new battle message](#iii-add-a-new-battle-message) + 3. [Implement the ability](#iv-implement-the-ability) + + +## 1. Define an ability constant + +Edit [include/constants/abilities.h](../blob/master/include/constants/abilities.asm): + +```diff +#ifndef GUARD_CONSTANTS_ABILITIES_H +#define GUARD_CONSTANTS_ABILITIES_H + +#define ABILITY_NONE 0 +#define ABILITY_STENCH 1 +... +#define ABILITY_CACOPHONY 76 +#define ABILITY_AIR_LOCK 77 ++#define ABILITY_SNOW_WARNING 78 + +-#define ABILITIES_COUNT 78 ++#define ABILITIES_COUNT 79 + +#endif // GUARD_CONSTANTS_ABILITIES_H +``` + + +## 2. Give it a name and description + +Edit [src/data/text/abilities.h](../blob/master/src/data/text/abilities.h): + +```diff +static const u8 sNoneDescription[] = _("No special ability."); +static const u8 sStenchDescription[] = _("Helps repel wild POKéMON."); +... +static const u8 sCacophonyDescription[] = _("Avoids sound-based moves."); +static const u8 sAirLockDescription[] = _("Negates weather effects."); ++static const u8 sSnowWarningDescription[] = _("Summons hail in battle."); + +const u8 gAbilityNames[ABILITIES_COUNT][ABILITY_NAME_LENGTH + 1] = +{ + [ABILITY_NONE] = _("-------"), + [ABILITY_STENCH] = _("STENCH"), +... + [ABILITY_CACOPHONY] = _("CACOPHONY"), + [ABILITY_AIR_LOCK] = _("AIR LOCK"), ++ [ABILITY_SNOW_WARNING] = _("SNOW WARNING"), +}; + +const u8 *const gAbilityDescriptionPointers[ABILITIES_COUNT] = +{ + [ABILITY_NONE] = sNoneDescription, + [ABILITY_STENCH] = sStenchDescription, +... + [ABILITY_CACOPHONY] = sCacophonyDescription, + [ABILITY_AIR_LOCK] = sAirLockDescription, ++ [ABILITY_SNOW_WARNING] = sSnowWarningDescription, +}; +``` + +If you want, you can edit [src/data/pokemon/base_stats.h](../blob/master/src/data/pokemon/base_stats.h) to give one of the starter Pokemon the new ability, run up a build and test it ingame - the new ability isn't set up to do anything, but it will show up correctly in the stats screen. + + +## 3. Make it do something + +This will be radically different for each ability - think about how your ability works and what similar abilities exist in Gen 3, then look into how those abilities are implemented. We know that Snow Warning works the same as Drizzle, Drought and Sand Stream, and searching for those finds us the function `AbilityBattleEffects` in [src/battle_util.c](../blob/master/src/battle_util.c): + +```c + case ABILITY_DRIZZLE: + if (!(gBattleWeather & WEATHER_RAIN_PERMANENT)) + { + gBattleWeather = (WEATHER_RAIN_PERMANENT | WEATHER_RAIN_TEMPORARY); + BattleScriptPushCursorAndCallback(BattleScript_DrizzleActivates); + gBattleScripting.battler = battler; + effect++; + } + break; + case ABILITY_SAND_STREAM: + if (!(gBattleWeather & WEATHER_SANDSTORM_PERMANENT)) + { + gBattleWeather = (WEATHER_SANDSTORM_PERMANENT | WEATHER_SANDSTORM_TEMPORARY); + BattleScriptPushCursorAndCallback(BattleScript_SandstreamActivates); + gBattleScripting.battler = battler; + effect++; + } + break; + case ABILITY_DROUGHT: + if (!(gBattleWeather & WEATHER_SUN_PERMANENT)) + { + gBattleWeather = (WEATHER_SUN_PERMANENT | WEATHER_SUN_TEMPORARY); + BattleScriptPushCursorAndCallback(BattleScript_DroughtActivates); + gBattleScripting.battler = battler; + effect++; + } + break; +``` + +From this we learn the following. + +1. The game makes a distinction between permanent and temporary weather conditions, so we want to make sure the same distinction is made for hail. +2. The game executes a unique battle script for each condition, so we'll need one for Snow Warning as well. + +However, keep `DoFieldEndTurnEffects` in your back pocket for later. + + +### i. Add a new weather constant + +Edit [include/constants/battle.h](../blob/master/include/constants/battle.h): + +```diff +// Battle Weather flags +#define WEATHER_RAIN_TEMPORARY (1 << 0) +#define WEATHER_RAIN_DOWNPOUR (1 << 1) // unused +#define WEATHER_RAIN_PERMANENT (1 << 2) +#define WEATHER_RAIN_ANY (WEATHER_RAIN_TEMPORARY | WEATHER_RAIN_DOWNPOUR | WEATHER_RAIN_PERMANENT) +#define WEATHER_SANDSTORM_TEMPORARY (1 << 3) +#define WEATHER_SANDSTORM_PERMANENT (1 << 4) +#define WEATHER_SANDSTORM_ANY (WEATHER_SANDSTORM_TEMPORARY | WEATHER_SANDSTORM_PERMANENT) +#define WEATHER_SUN_TEMPORARY (1 << 5) +#define WEATHER_SUN_PERMANENT (1 << 6) +#define WEATHER_SUN_ANY (WEATHER_SUN_TEMPORARY | WEATHER_SUN_PERMANENT) +-#define WEATHER_HAIL (1 << 7) ++#define WEATHER_HAIL_TEMPORARY (1 << 7) ++#define WEATHER_HAIL_PERMANENT (1 << 8) +-#define WEATHER_HAIL_ANY (WEATHER_HAIL) ++#define WEATHER_HAIL_ANY (WEATHER_HAIL_TEMPORARY | WEATHER_HAIL_PERMANENT) +#define WEATHER_ANY (WEATHER_RAIN_ANY | WEATHER_SANDSTORM_ANY | WEATHER_SUN_ANY | WEATHER_HAIL_ANY) +``` + +We've defined a bit for permanent hail, so we need to ensure that existing references to hail are correctly updated. Edit [src/battle_script_commands.c](../blob/master/src/battle_script_commands.c): + +```diff +static void Cmd_weatherdamage(void) +{ +... +- if (gBattleWeather & WEATHER_HAIL) ++ if (gBattleWeather & WEATHER_HAIL_ANY) + { + if (!IS_BATTLER_OF_TYPE(gBattlerAttacker, TYPE_ICE) + && !(gStatuses3[gBattlerAttacker] & STATUS3_UNDERGROUND) + && !(gStatuses3[gBattlerAttacker] & STATUS3_UNDERWATER)) + { + gBattleMoveDamage = gBattleMons[gBattlerAttacker].maxHP / 16; + if (gBattleMoveDamage == 0) + gBattleMoveDamage = 1; + } +... +} + +... + +static void Cmd_sethail(void) +{ + if (gBattleWeather & WEATHER_HAIL_ANY) + { + gMoveResultFlags |= MOVE_RESULT_MISSED; + gBattleCommunication[MULTISTRING_CHOOSER] = 2; + } + else + { +- gBattleWeather = WEATHER_HAIL; ++ gBattleWeather = WEATHER_HAIL_TEMPORARY; + gBattleCommunication[MULTISTRING_CHOOSER] = 5; + gWishFutureKnock.weatherDuration = 5; + } + + gBattlescriptCurrInstr++; +} +``` + +And [src/battle_util.c](../blob/master/src/battle_util.c): + +```diff +u8 DoFieldEndTurnEffects(void) +{ +... + case ENDTURN_HAIL: + if (gBattleWeather & WEATHER_HAIL_ANY) + { +- if (--gWishFutureKnock.weatherDuration == 0) ++ if (!(gBattleWeather & WEATHER_HAIL_PERMANENT) && --gWishFutureKnock.weatherDuration == 0) + { +- gBattleWeather &= ~WEATHER_HAIL; ++ gBattleWeather &= ~WEATHER_HAIL_TEMPORARY; + gBattlescriptCurrInstr = BattleScript_SandStormHailEnds; + } + else + { + gBattlescriptCurrInstr = BattleScript_DamagingWeatherContinues; + } + + gBattleScripting.animArg1 = B_ANIM_HAIL_CONTINUES; + gBattleCommunication[MULTISTRING_CHOOSER] = 1; + BattleScriptExecute(gBattlescriptCurrInstr); + effect++; + } + gBattleStruct->turnCountersTracker++; + break; +... +} +``` + +And [src/pokemon.c](../blob/master/src/pokemon.c): + +```diff +s32 CalculateBaseDamage(struct BattlePokemon *attacker, struct BattlePokemon *defender, u32 move, u16 sideStatus, u16 powerOverride, u8 typeOverride, u8 battlerIdAtk, u8 battlerIdDef) +{ +... + // any weather except sun weakens solar beam +- if ((gBattleWeather & (WEATHER_RAIN_ANY | WEATHER_SANDSTORM_ANY | WEATHER_HAIL)) && gCurrentMove == MOVE_SOLAR_BEAM) ++ if ((gBattleWeather & (WEATHER_RAIN_ANY | WEATHER_SANDSTORM_ANY | WEATHER_HAIL_ANY)) && gCurrentMove == MOVE_SOLAR_BEAM) + damage /= 2; +... +} +``` + + +### ii. Add a new battle script + +Edit [include/battle_scripts.h](../blob/master/include/battle_scripts.h): + +```diff +#ifndef GUARD_BATTLE_SCRIPTS_H +#define GUARD_BATTLE_SCRIPTS_H + +extern const u8 BattleScript_HitFromCritCalc[]; +extern const u8 BattleScript_MoveEnd[]; +... +extern const u8 BattleScript_ActionGetNear[]; +extern const u8 BattleScript_ActionThrowPokeblock[]; ++extern const u8 BattleScript_SnowWarningActivates[]; + +#endif // GUARD_BATTLE_SCRIPTS_H +``` + +This exposes the symbol `BattleScript_SnowWarningActivates` to C, but battle scripts are actually written in assembler macros. Open [data/battle_scripts_1.s](../blob/master/data/battle_scripts_1.s): + +``` +BattleScript_DrizzleActivates:: + pause 0x20 + printstring STRINGID_PKMNMADEITRAIN + waitstate + playanimation BS_BATTLER_0, B_ANIM_RAIN_CONTINUES, NULL + call BattleScript_WeatherFormChanges + end3 + +... + +BattleScript_SandstreamActivates:: + pause 0x20 + printstring STRINGID_PKMNSXWHIPPEDUPSANDSTORM + waitstate + playanimation BS_BATTLER_0, B_ANIM_SANDSTORM_CONTINUES, NULL + call BattleScript_WeatherFormChanges + end3 + +... + +BattleScript_DroughtActivates:: + pause 0x20 + printstring STRINGID_PKMNSXINTENSIFIEDSUN + waitstate + playanimation BS_BATTLER_0, B_ANIM_SUN_CONTINUES, NULL + call BattleScript_WeatherFormChanges + end3 +``` + +They're not in any obvious order, but the pattern is obvious enough that we can follow it. Add the following to the bottom of the file: + +``` +BattleScript_SnowWarningActivates:: + pause 0x20 + printstring STRINGID_PKMNSXWHIPPEDUPHAILSTORM + waitstate + playanimation BS_BATTLER_0, B_ANIM_HAIL_CONTINUES, NULL + call BattleScript_WeatherFormChanges + end3 +``` + +We're nearly there, but first we need to deal with this `STRINGID` business. + + +### iii. Add a new battle message + +Edit [include/constants/battle_string_ids.h](../blob/master/include/constants/battle_string_ids.h): + +```diff +#ifndef GUARD_CONSTANTS_BATTLE_STRING_IDS_H +#define GUARD_CONSTANTS_BATTLE_STRING_IDS_H + +-#define BATTLESTRINGS_COUNT 369 ++#define BATTLESTRINGS_COUNT 370 + +#define BATTLESTRINGS_ID_ADDER 12 // all battlestrings have its ID + 12, because first 5 are reserved + +#define STRINGID_INTROMSG 0 +#define STRINGID_INTROSENDOUT 1 +#define STRINGID_RETURNMON 2 +#define STRINGID_SWITCHINMON 3 +#define STRINGID_USEDMOVE 4 +#define STRINGID_BATTLEEND 5 + +// todo: make some of those names less vague: attacker/target vs pkmn, etc. +#define STRINGID_TRAINER1LOSETEXT 12 +#define STRINGID_PKMNGAINEDEXP 13 +... +#define STRINGID_TRAINER1WINTEXT 379 +#define STRINGID_TRAINER2WINTEXT 380 ++#define STRINGID_PKMNSXWHIPPEDUPHAILSTORM 381 + +#endif // GUARD_CONSTANTS_BATTLE_STRING_IDS_H +``` + +This works essentially the same as in step 1 - we've defined a constant, now we need to write the actual message. Edit [src/battle_message.c](../blob/master/src/battle_message.c): + +```diff +const u8 * const gBattleStringsTable[BATTLESTRINGS_COUNT] = +{ + [STRINGID_TRAINER1LOSETEXT - 12] = sText_Trainer1LoseText, + [STRINGID_PKMNGAINEDEXP - 12] = sText_PkmnGainedEXP, +... + [STRINGID_TRAINER1WINTEXT - 12] = sText_Trainer1WinText, + [STRINGID_TRAINER2WINTEXT - 12] = sText_Trainer2WinText, ++ [STRINGID_PKMNSXWHIPPEDUPHAILSTORM - 12] = sText_PkmnsXWhippedUpHailstorm, +}; +``` + +This file is a mess, so there's no obvious place to put the actual text; I chose to put it immediately following this line, since they're very similar. + +```diff +static const u8 sText_PkmnsXWhippedUpSandstorm[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX}'s {B_SCR_ACTIVE_ABILITY}\nwhipped up a sandstorm!"); ++static const u8 sText_PkmnsXWhippedUpHailstorm[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX}'s {B_SCR_ACTIVE_ABILITY}\nwhipped up a hailstorm!"); +``` + + +### iv. Implement the ability + +Now we can return to [src/battle_util.c](../blob/master/src/battle_util.c) and implement our new ability: + +```diff + case ABILITY_DRIZZLE: + if (!(gBattleWeather & WEATHER_RAIN_PERMANENT)) + { + gBattleWeather = (WEATHER_RAIN_PERMANENT | WEATHER_RAIN_TEMPORARY); + BattleScriptPushCursorAndCallback(BattleScript_DrizzleActivates); + gBattleScripting.battler = battler; + effect++; + } + break; + case ABILITY_SAND_STREAM: + if (!(gBattleWeather & WEATHER_SANDSTORM_PERMANENT)) + { + gBattleWeather = (WEATHER_SANDSTORM_PERMANENT | WEATHER_SANDSTORM_TEMPORARY); + BattleScriptPushCursorAndCallback(BattleScript_SandstreamActivates); + gBattleScripting.battler = battler; + effect++; + } + break; + case ABILITY_DROUGHT: + if (!(gBattleWeather & WEATHER_SUN_PERMANENT)) + { + gBattleWeather = (WEATHER_SUN_PERMANENT | WEATHER_SUN_TEMPORARY); + BattleScriptPushCursorAndCallback(BattleScript_DroughtActivates); + gBattleScripting.battler = battler; + effect++; + } + break; ++ case ABILITY_SNOW_WARNING: ++ if (!(gBattleWeather & WEATHER_HAIL_PERMANENT)) ++ { ++ gBattleWeather = (WEATHER_HAIL_PERMANENT | WEATHER_HAIL_TEMPORARY); ++ BattleScriptPushCursorAndCallback(BattleScript_SnowWarningActivates); ++ gBattleScripting.battler = battler; ++ effect++; ++ } ++ break; +``` + +If you now build and test the game as before, when you send out the Pokemon with Snow Warning it will summon a hailstorm that lasts for the remainder of the battle. \ No newline at end of file