Created How to add a new ability (markdown)

Thomas Winwood 2019-11-19 12:49:14 +00:00
parent 8235f34f3d
commit 3b3de80648
1 changed files with 388 additions and 0 deletions

388
How-to-add-a-new-ability.md Normal file

@ -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.