-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Overview: Items and their Effects
src/data/items.h
defines the list of items, and associates each with a field use function, battle usage type, and battle use function. Let's look at some items to get a better idea of how items actually work: Shoal Salt, the Old Rod, Revives, and X-Accuracy.
- Shoal Salt, to learn about items that have no use
- the Old Rod, to learn about items used on the field
- Revives and X-Accuracy, to learn about items used on a Pokémon
- the different kinds of Poké Balls
Shoal Salt is an item with no immediate function on its own. It can be given to an NPC alongside Shoal Sand, in order to make a Shell Bell.
Here's the entry for Shoal Salt in src/data/items.h
, formatted here for better legibility:
[ITEM_SHOAL_SALT] =
{
.name = _("SHOAL SALT"),
.itemId = ITEM_SHOAL_SALT,
.price = 20,
.description = sShoalSaltDesc,
.pocket = POCKET_ITEMS,
.type = ITEM_USE_BAG_MENU,
.fieldUseFunc = ItemUseOutOfBattle_CannotUse,
},
Most of these fields are easy to grasp:
name
- The item's name, written directly in this header.
itemId
- The item's unique ID, which is also its position in this list; so, we see
ITEM_SHOAL_SALT
written twice. That constant, by the way, is defined ininclude/constants/items.h
(note the "S" at the end of the filename). price
- The item's purchase price. Its sell price — how much the PokeMart pays you for it — will be half that. You can't actually buy Shoal Salt, but this value is still needed in order to let you sell it for $10.
pocket
- The bag pocket that item goes into. The bag pocket constants are defined in
include/constants/item.h
(note the lack of an "S" in the filename) and arePOCKET_NONE
,POCKET_ITEMS
,POCKET_POKE_BALLS
,POCKET_TM_HM
,POCKET_BERRIES
, andPOCKET_KEY_ITEMS
. type
- This is usually one of the item use constants defined in
include/constants/items.h
-with-an-S, and it controls the exit callback: that is: does the game leave the Bag menu when you choose to use the item, and if so, where does it go? The values areITEM_USE_MAIL
,ITEM_USE_PARTY_MENU
,ITEM_USE_FIELD
,ITEM_USE_PBLOCK_CASE
, andITEM_USE_BAG_MENU
. fieldUseFunc
- This is the most interesting field: this is a function pointer, telling the game what function to run when you try to use this item.
The field use function is defined in src/item_use.c
, and is pretty easy to grasp: it just displays an error message.
void ItemUseOutOfBattle_CannotUse(u8 taskId)
{
DisplayDadsAdviceCannotUseItemMessage(taskId, gTasks[taskId].tUsingRegisteredKeyItem);
}
Nice and simple. It's easy to define an item that has no explicit use of its own — an item that's more important for its role in scripted events, where the scripts do the work.
Here's the entry for the Old Rod in src/data/items.h
, again formatted for legibility:
[ITEM_OLD_ROD] =
{
.name = _("OLD ROD"),
.itemId = ITEM_OLD_ROD,
.price = 0,
.description = sOldRodDesc,
.importance = 1,
.registrability = TRUE,
.pocket = POCKET_KEY_ITEMS,
.type = ITEM_USE_FIELD,
.fieldUseFunc = ItemUseOutOfBattle_Rod,
.secondaryId = OLD_ROD,
},
We specify a few more fields here:
importance
-
When this value is set to 1, it has the following effects:
- The item can't be given to a Pokémon as a hold item. (This is enforced in multiple code locations.)
- The item's quantity is not displayed in the bag. (This is also true for any item in the Key Items pocket.)
- The item cannot be given to Battle Frontier Apprentices.
- The item cannot be given to the randomized Lilycove Lady NPC.
- The item cannot be deposited in the PC.
- The item cannot be thrown away via the Bag menu.
registrability
- Indicates whether the player can register the item for use via the Select button.
secondaryId
-
This has a variable meaning. Some field items activate the exact same underlying functions but need minor differences in behavior. For example, every bicycle type shares code, and every fishing rod shares code. The secondary ID is passed to that code so that it knows what "variant" it's been run on.
Some examples:
ITEM_MACH_BIKE
is 259 andITEM_ACRO_BIKE
is 272, but the secondary IDsMACH_BIKE
andACRO_BIKE
are 0 and 1. Similarly, the secondary IDsOLD_ROD
,GOOD_ROD
, andSUPER_ROD
are 0, 1, and 2.
Anyway, what we're most interested in is the field use function, ItemUseOutOfBattle_Rod
. That's defined in src/item_use.c
, and looks like this:
void ItemUseOutOfBattle_Rod(u8 taskId)
{
if (CanFish() == TRUE)
{
sItemUseOnFieldCB = ItemUseOnFieldCB_Rod;
SetUpItemUseOnFieldCallback(taskId);
}
else
DisplayDadsAdviceCannotUseItemMessage(taskId, gTasks[taskId].tUsingRegisteredKeyItem);
}
static void ItemUseOnFieldCB_Rod(u8 taskId)
{
StartFishing(ItemId_GetSecondaryId(gSpecialVar_ItemId));
DestroyTask(taskId);
}
The StartFishing
function is defined in src/field_player_avatar.c
, and kicks off the entire process of fishing.
- Inside of an item use function,
gSpecialVar_ItemId
is the ID of the item being used. - A few helper functions, like
RemoveUsedItem()
, are available for common tasks. -
SetUpItemUseCallback
is itself a helper function to aid in properly closing the current bag (i.e. the normal Bag or the Battle Pyramid Bag) and moving on from the current screen (i.e. to the overworld or the party menu). Many item types use it, but some field-only items, like the bicycle, prefer an alternate helper function:SetUpItemUseOnFieldCallback
. This alternate function brings the game directly back to the overworld, which is helpful if the item does something on the overworld. - The
gTasks[taskId].tUsingRegisteredKeyItem
variable indicates whether the item is being used via the Select button (i.e. it was hotkeyed). If an item opens a submenu, then its field use function will generally need this, in order to know how to set up the code to return from that screen;ItemUseOutOfBattle_PokeblockCase
is an example.
Let's look at Revives (ITEM_REVIVE
) and X-Accuracy (ITEM_X_ACCURACY
) as examples. Heading back to src/data/items.h
:
[ITEM_REVIVE] =
{
.name = _("REVIVE"),
.itemId = ITEM_REVIVE,
.price = 1500,
.description = sReviveDesc,
.pocket = POCKET_ITEMS,
.type = ITEM_USE_PARTY_MENU,
.fieldUseFunc = ItemUseOutOfBattle_Medicine,
.battleUsage = ITEM_B_USE_MEDICINE,
.battleUseFunc = ItemUseInBattle_Medicine,
},
[ITEM_X_ACCURACY] =
{
.name = _("X ACCURACY"),
.itemId = ITEM_X_ACCURACY,
.price = 950,
.description = sXAccuracyDesc,
.pocket = POCKET_ITEMS,
.type = ITEM_USE_BAG_MENU,
.fieldUseFunc = ItemUseOutOfBattle_CannotUse,
.battleUsage = ITEM_B_USE_OTHER,
.battleUseFunc = ItemUseInBattle_StatIncrease,
},
These items have field-use functions (though X-Accuracy just uses the field use function that says you can't use the item), but they also have two new fields:
battleUsage
-
When this field is specified, its value should be one of the constants defined in
include/constants/items.h
-with-an-S. It tells the game roughly what kind of item this is, but there aren't a lot of "kinds" listed because as the comments in that file explain, the game really only cares whether the value is zero. (Zero is the default if you don't specify anything, and indicates that the item can't be used in battle.)The specific impact that this has is, it controls whether you get the option to use the item while viewing it in the Bag menu during a battle.
battleUseFunc
- This is a function pointer similar to
fieldUseFunc
, telling the game where to find the code it should run when you try to use the item in battle.
The field use function for Revives and other medicine items is defined in src/item_use.c
and is quite simple:
void ItemUseOutOfBattle_Medicine(u8 taskId)
{
gItemUseCB = ItemUseCB_Medicine;
SetUpItemUseCallback(taskId);
}
The battle use function for Revives and other medicine items, ItemUseInBattle_Medicine
, is also defined there, and is very similar:
void ItemUseInBattle_Medicine(u8 taskId)
{
gItemUseCB = ItemUseCB_Medicine;
ItemUseInBattle_ShowPartyMenu(taskId);
}
Both of these functions write the same item use callback, ItemUseCB_Medicine
, to the gItemUseCB
function pointer. The former sets the callback up to run immediately, while the latter pops the party menu to let the player choose a target to use the Revive on.
Now, as it turns out, ItemUseCB_Medicine
is defined in src/party_menu.c
. This function runs a few checks of its own (e.g. to disallow you from using HP Up on Shedinja, or using an HP-restoring item on a Pokémon with full health), but in most cases it falls through to ExecuteTableBasedItemEffect_
to attempt to use the item.
ExecuteTableBasedItemEffect_
returns TRUE
if the item would have no effect on a given Pokémon. It's just a wrapper function specific to the party menu, to make it easier to call a function defined elsewhere (names ending in an underscore seem to be PRET's convention for this). The real function is ExecuteTableBasedItemEffect
, defined in src/pokemon.c
.
Jumping back to X-Accuracy, we see that ItemUseInBattle_StatIncrease
, from src/item_use.c
, calls directly to the same place: ExecuteTableBasedItemEffect
.
void ItemUseInBattle_StatIncrease(u8 taskId)
{
u16 partyId = gBattlerPartyIndexes[gBattlerInMenuId];
if (ExecuteTableBasedItemEffect(&gPlayerParty[partyId], gSpecialVar_ItemId, partyId, 0) != FALSE)
{
if (!InBattlePyramid())
DisplayItemMessage(taskId, FONT_NORMAL, gText_WontHaveEffect, CloseItemMessage);
else
DisplayItemMessageInBattlePyramid(taskId, gText_WontHaveEffect, Task_CloseBattlePyramidBagMessage);
}
else
{
gTasks[taskId].func = Task_UseStatIncreaseItem;
gTasks[taskId].data[8] = 0;
}
}
That function, ExecuteTableBasedItemEffect
, is the core function for applying item effects to a Pokémon. It'll return FALSE
if any effects were applied successfully, or TRUE
if no effects were applied. It's the caller's responsibility to remove the item from the inventory if it has any effect (or to set up some other code to remove the item), and looking at ItemUseCB_Medicine
and Task_UseStatIncreaseItem
, we can see that they do so.
Looking at ExecuteTableBasedItemEffect
, we can see that it consults a table named gItemEffectTable
, which is defined in src/data/pokemon/item_effects.h
and defines all of the effects an item can have on a Pokémon. The table lists items in the same order as the item constants, but it skips several items at the start of the list and so starts from ITEM_POTION
. Each entry in the table is an array of bytes, and the syntax for this array is... unusual. Let's look at the entry for Revives:
const u8 gItemEffect_Revive[7] = {
[4] = ITEM4_REVIVE | ITEM4_HEAL_HP,
[6] = ITEM6_HEAL_HP_HALF,
};
So let's go over it.
- All constants related to item effects are defined in
include/constants/item_effects.h
. - Bytes 0, 1, 2, 3, 4, and 5 are flags masks indicating what effect types the item can have. For example, the
ITEM4_REVIVE
flag can appear in byte 4 and indicates that the item should revive a fainted Pokémon. - Bytes 6 and onward (using the constant
ITEM_EFFECT_ARG_START
) are arguments specific to each item type.- Arguments are typically signed bytes, so negative values are often allowed, though they have special meanings in some cases.
- Example: you can't actually "heal" a negative amount of HP; the values -1, -2, and -3 are used as special indicators for values that you can't predict in advance (e.g. "restore all of a Pokémon's HP" or "restore until the Pokémon is at half HP").
- Arguments should be stored in the same order that the item types are listed and processed.
-
Example: because
ITEM4_HEAL_HP
uses a lower bit thanITEM4_HEAL_PP
, the argument for the amount of HP to heal should appear before the argument for the amount of PP to heal. -
Example:
ITEM4_HEAL_HP
uses a lower bit thanITEM5_EV_SPEED
, but it also appears in an earlier byte (byte 4, compared to byte 5). This means that the argument for how much PP to heal should appear before the argument for how much to modify the EV by.
-
Example: because
- Not every item effect type can accept arguments. The following effect types have arguments:
-
ITEM4_EV_HP
- All EV-modifying items use a signed amount by which to modify the EV; constants
ITEM6_ADD_EV
andITEM6_SUBTRACT_EV
are typically used.
- All EV-modifying items use a signed amount by which to modify the EV; constants
ITEM4_EV_ATK
-
ITEM4_HEAL_HP
- Amount by which to heal. Unsigned. Special constants
ITEM6_HEAL_HP_FULL
,ITEM6_HEAL_HP_HALF
, andITEM6_HEAL_HP_LVL_UP
are available; the latter is used by Rare Candies.
- Amount by which to heal. Unsigned. Special constants
-
ITEM4_HEAL_PP
- Amount of PP to restore. Unsigned.
ITEM5_EV_DEF
ITEM5_EV_SPEED
ITEM5_EV_SPDEF
ITEM5_EV_SPATK
-
ITEM5_FRIENDSHIP_LOW
- The amount of friendship to change. Signed (can be negative). The item only has an effect if the Pokémon's friendship is below 100.
-
ITEM5_FRIENDSHIP_MID
- The amount of friendship to change. Signed (can be negative). The item only has an effect if the Pokémon's friendship is in the range [100, 199].
-
ITEM5_FRIENDSHIP_HIGH
- The amount of friendship to change. Signed (can be negative). The item only has an effect if the Pokémon's friendship is 200 or above.
-
- Arguments are typically signed bytes, so negative values are often allowed, though they have special meanings in some cases.
So we can see here that the revive is marked as reviving a Pokémon and healing some of its HP. Notably, the ITEM4_REVIVE
flag isn't directly responsible for reviving a Pokémon; rather, it modifies the behavior of the ITEM4_HEAL_HP
flag. If you only specify ITEM4_HEAL_HP
, then you've created a normal HP-restoring item that can't be used on a fainted Pokémon. If you specify ITEM4_REVIVE | ITEM4_HEAL_HP
, then you've created an HP-restoring that can't be used on a non-fainted Pokémon (and the battle also tracks the number of revives used by each trainer).
These are a bit more complicated. Here's the definition for an Ultra Ball:
[ITEM_ULTRA_BALL] =
{
.name = _("ULTRA BALL"),
.itemId = ITEM_ULTRA_BALL,
.price = 1200,
.description = sUltraBallDesc,
.pocket = POCKET_POKE_BALLS,
.type = ITEM_ULTRA_BALL - FIRST_BALL,
.battleUsage = ITEM_B_USE_OTHER,
.battleUseFunc = ItemUseInBattle_PokeBall,
.secondaryId = ITEM_ULTRA_BALL - FIRST_BALL,
},
We can see that the secondaryId
identifies the ball type, and curiously, the type
field is used the same way.
The battle use function, ItemUseInBattle_PokeBall
, is defined in src/item_use.c
as usual:
void ItemUseInBattle_PokeBall(u8 taskId)
{
if (IsPlayerPartyAndPokemonStorageFull() == FALSE) // have room for mon?
{
RemoveBagItem(gSpecialVar_ItemId, 1);
if (!InBattlePyramid())
Task_FadeAndCloseBagMenu(taskId);
else
CloseBattlePyramidBag(taskId);
}
else if (!InBattlePyramid())
{
DisplayItemMessage(taskId, FONT_NORMAL, gText_BoxFull, CloseItemMessage);
}
else
DisplayItemMessageInBattlePyramid(taskId, gText_BoxFull, Task_CloseBattlePyramidBagMessage);
}
This function fails if your party and Pokémon Storage System (the PC) are both full, or if you're in the Battle Pyramid. Otherwise, the function consumes the Poké Ball,... and then doesn't do anything?!
It turns out that large portions of the Poké Ball behavior are built directly into the battle system. If we look at HandleAction_UseItem
in src/battle_util.c
, we see that right near the start, it treats Poké Balls as a special case, by running specific battle scripts for each ball.
void HandleAction_UseItem(void)
{
gBattlerAttacker = gBattlerTarget = gBattlerByTurnOrder[gCurrentTurnActionNumber];
gBattle_BG0_X = 0;
gBattle_BG0_Y = 0;
ClearFuryCutterDestinyBondGrudge(gBattlerAttacker);
gLastUsedItem = gBattleBufferB[gBattlerAttacker][1] | (gBattleBufferB[gBattlerAttacker][2] << 8);
if (gLastUsedItem <= LAST_BALL) // is ball
{
gBattlescriptCurrInstr = gBattlescriptsForBallThrow[gLastUsedItem];
}
The list of battle scripts, and the scripts themselves, are defined in data/battle_scripts_2.s
.
In general, adding new Poké Balls is not quite as easy as adding other item types because as the comments in include/constants/items.h
-with-an-S explain, the game expects ball IDs to begin from 1 and be contiguous: there should only be Poké Balls in the range [1, LAST_BALL
]. There is a FIRST_BALL
constant, yes, but it's used for convenience; several pieces of code, including gBattlescriptsForBallThrow
and MON_DATA_POKEBALL
(to track what ball a Pokémon was caught with), are written such that if you define Poké Ball IDs anywhere else in the overall ID list, things will break. The recommended approach for adding new Poké Balls is to place them after ITEM_PREMIER_BALL
, renumber the next two dozen items, and remove one of the unused item definitions starting at ITEM_034
.
There's no point in adding a Poké Ball that doesn't have special behavior or catch odds, so how do we implement those? For that, we need to turn to the battle script command definitions, in src/battle_script_commands.c
. The handleballthrow
command is responsible for computing whether a wild Pokémon has been successfully caught, and for updating some of its data on capture. That command is powered by the Cmd_handleballthrow
C function, and that function handles catch odds:
if (gLastUsedItem == ITEM_SAFARI_BALL)
catchRate = gBattleStruct->safariCatchFactor * 1275 / 100;
else
catchRate = gSpeciesInfo[gBattleMons[gBattlerTarget].species].catchRate;
if (gLastUsedItem > ITEM_SAFARI_BALL)
{
switch (gLastUsedItem)
{
case ITEM_NET_BALL:
if (IS_BATTLER_OF_TYPE(gBattlerTarget, TYPE_WATER) || IS_BATTLER_OF_TYPE(gBattlerTarget, TYPE_BUG))
ballMultiplier = 30;
else
ballMultiplier = 10;
break;
case ITEM_DIVE_BALL:
if (GetCurrentMapType() == MAP_TYPE_UNDERWATER)
ballMultiplier = 35;
else
ballMultiplier = 10;
break;
case ITEM_NEST_BALL:
if (gBattleMons[gBattlerTarget].level < 40)
{
ballMultiplier = 40 - gBattleMons[gBattlerTarget].level;
if (ballMultiplier <= 9)
ballMultiplier = 10;
}
else
{
ballMultiplier = 10;
}
break;
case ITEM_REPEAT_BALL:
if (GetSetPokedexFlag(SpeciesToNationalPokedexNum(gBattleMons[gBattlerTarget].species), FLAG_GET_CAUGHT))
ballMultiplier = 30;
else
ballMultiplier = 10;
break;
case ITEM_TIMER_BALL:
ballMultiplier = gBattleResults.battleTurnCounter + 10;
if (ballMultiplier > 40)
ballMultiplier = 40;
break;
case ITEM_LUXURY_BALL:
case ITEM_PREMIER_BALL:
ballMultiplier = 10;
break;
}
}
else
ballMultiplier = sBallCatchBonuses[gLastUsedItem - ITEM_ULTRA_BALL];
We begin with a special case for the Safari Ball, replacing the normal catch rate with one that depends on the battle state (i.e. rocks or bait you've thrown). The code below that computes the ball's catch multiplier. Most of it is inside of an if-statement that checks if the item ID is greater than ITEM_SAFARI_BALL
. If we look at the item IDs and the code above, we see that the Poké Ball types are split into two groups. Here's the item IDs, but annotated with that division:
//
// Balls that use a fixed catch chance are listed first.
//
#define ITEM_MASTER_BALL 1
#define ITEM_ULTRA_BALL 2
#define ITEM_GREAT_BALL 3
#define ITEM_POKE_BALL 4
#define ITEM_SAFARI_BALL 5
//
// Balls with special catch bonuses are listed last.
//
#define ITEM_NET_BALL 6
#define ITEM_DIVE_BALL 7
#define ITEM_NEST_BALL 8
#define ITEM_REPEAT_BALL 9
#define ITEM_TIMER_BALL 10
#define ITEM_LUXURY_BALL 11
#define ITEM_PREMIER_BALL 12
So the switch
statement only handles catch multipliers for the special Poké Balls anyway, but instead of using a default
case, the whole thing's been wrapped in an if-statement. The "typical" Poké Balls, Master Ball included, rely on sBallCatchBonuses
, which is defined further up in src/battle_script_commands.c
:
static const u8 sBallCatchBonuses[] =
{
[ITEM_ULTRA_BALL - ITEM_ULTRA_BALL] = 20,
[ITEM_GREAT_BALL - ITEM_ULTRA_BALL] = 15,
[ITEM_POKE_BALL - ITEM_ULTRA_BALL] = 10,
[ITEM_SAFARI_BALL - ITEM_ULTRA_BALL] = 15
};
It's easiest to add your custom Poké Ball to the end of the list, after Premier Balls, so if you want to add special catch odds, you'd just need to add a new case to the switch
statement. For example, if we're creating ITEM_DREAM_BALL
, then we might add something like this:
case ITEM_DREAM_BALL:
if (gBattleMons[gBattlerTarget].status1 & STATUS1_SLEEP)
ballMultiplier = 40;
else
ballMultiplier = 10;
break;
But what if you want something else? For example, the Heal Ball fully restores the HP of the caught Pokémon and cures any status ailments it may have, and the Friend Ball increases the caught Pokémon's friendship. Well, there's a solution for that, too. Let's look further down in Cmd_handleballthrow
. We'll find two different places that run the following code when a Pokémon is successfully caught (one spot for instant captures, as with the Master Ball; another spot for captures after the ball shakes three times):
gBattlescriptCurrInstr = BattleScript_SuccessBallThrow;
SetMonData(&gEnemyParty[gBattlerPartyIndexes[gBattlerTarget]], MON_DATA_POKEBALL, &gLastUsedItem);
That second function call, SetMonData
, updates the wild Pokémon's data to list the ball it was caught with. If we're adding ITEM_HEAL_BALL
, then it should be possible to further update the Pokémon's stats by editing both of the code locations in Cmd_handleballthrow
that perform this task:
gBattlescriptCurrInstr = BattleScript_SuccessBallThrow;
SetMonData(&gEnemyParty[gBattlerPartyIndexes[gBattlerTarget]], MON_DATA_POKEBALL, &gLastUsedItem);
if (gLastUsedItem == ITEM_HEAL_BALL)
{
u32 status = 0;
u16 hp = gBattleMons[gBattlerTarget].maxHP;
SetMonData(&gEnemyParty[gBattlerPartyIndexes[gBattlerTarget]], MON_DATA_STATUS, &status);
SetMonData(&gEnemyParty[gBattlerPartyIndexes[gBattlerTarget]], MON_DATA_HP, &hp);
}