0 Overview: Items and their Effects
DavidJCobb edited this page 2023-11-16 19:51:10 +01:00

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.

How using field items works

Shoal Salt

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 in include/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 are POCKET_NONE, POCKET_ITEMS, POCKET_POKE_BALLS, POCKET_TM_HM, POCKET_BERRIES, and POCKET_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 are ITEM_USE_MAIL, ITEM_USE_PARTY_MENU, ITEM_USE_FIELD, ITEM_USE_PBLOCK_CASE, and ITEM_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.

Old Rod

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 and ITEM_ACRO_BIKE is 272, but the secondary IDs MACH_BIKE and ACRO_BIKE are 0 and 1. Similarly, the secondary IDs OLD_ROD, GOOD_ROD, and SUPER_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.

A few more tips

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

How using items on a Pokémon works

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.

Revives

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.

X-Accuracy

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.

Their common meeting point: ExecuteTableBasedItemEffect

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 than ITEM4_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 than ITEM5_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.
    • 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 and ITEM6_SUBTRACT_EV are typically used.
      • ITEM4_EV_ATK
      • ITEM4_HEAL_HP
        • Amount by which to heal. Unsigned. Special constants ITEM6_HEAL_HP_FULL, ITEM6_HEAL_HP_HALF, and ITEM6_HEAL_HP_LVL_UP are available; the latter is used by Rare Candies.
      • 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.

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

How Poké Balls work

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.

Writing your own catch rate effects

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);
}