diff --git a/asm/macros/event.inc b/asm/macros/event.inc index 3495598548..7b62108439 100644 --- a/asm/macros/event.inc +++ b/asm/macros/event.inc @@ -1247,7 +1247,7 @@ @ Opens the Pokemart system, offering the specified products for sale. @ Products should be a list of .2byte item values preceded by an .align 2 - .macro pokemart products:req + .macro pokemart products=Pokemart_DefaultItemList .byte SCR_OP_POKEMART .4byte \products .endm diff --git a/data/event_scripts.s b/data/event_scripts.s index d3988f44d9..d362775b14 100644 --- a/data/event_scripts.s +++ b/data/event_scripts.s @@ -1347,10 +1347,10 @@ Common_EventScript_PlayerHandedOverTheItem:: .include "data/scripts/elite_four.inc" .include "data/scripts/movement.inc" .include "data/scripts/check_furniture.inc" + .include "data/scripts/mart_clerk.inc" .include "data/text/record_mix.inc" .include "data/text/pc.inc" .include "data/text/pkmn_center_nurse.inc" - .include "data/text/mart_clerk.inc" .include "data/text/obtain_item.inc" .include "data/text/move_relearner.inc" diff --git a/data/text/mart_clerk.inc b/data/scripts/mart_clerk.inc similarity index 53% rename from data/text/mart_clerk.inc rename to data/scripts/mart_clerk.inc index 891fb937e0..2b9f872e85 100644 --- a/data/text/mart_clerk.inc +++ b/data/scripts/mart_clerk.inc @@ -8,3 +8,13 @@ gText_PleaseComeAgain:: gText_PlayerWhatCanIDoForYou:: .string "{PLAYER}{STRING 5}, welcome!\p" .string "What can I do for you?$" + +Pokemart_DefaultItemList:: + .2byte ITEM_ULTRA_BALL + .2byte ITEM_HYPER_POTION + .2byte ITEM_MAX_POTION + .2byte ITEM_FULL_RESTORE + .2byte ITEM_FULL_HEAL + .2byte ITEM_REVIVE + .2byte ITEM_MAX_REPEL + pokemartlistend diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 41ea827732..92815e2d6a 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -50,6 +50,7 @@ - [How to use Map Previews](tutorials/how_to_map_preview_screen.md) - [Quickstart Documentation](tutorials/quickstart.md) - [How to use Generic Spots](tutorials/how_to_spots.md) + - [How to use Dynamic Shop](tutorials/how_to_dynamic_shop.md) - [Changelog](./CHANGELOG.md) - [1.15.x]() - [Version 1.15.0](changelogs/1.15.x/1.15.0.md) diff --git a/docs/tutorials/how_to_dynamic_shop.md b/docs/tutorials/how_to_dynamic_shop.md new file mode 100644 index 0000000000..166140348c --- /dev/null +++ b/docs/tutorials/how_to_dynamic_shop.md @@ -0,0 +1,166 @@ +# How to Use Dynamic Shop + +## Overview + +![Showcase](/docs/tutorials/img/dynamic_shop/showcase.gif) + +This is a feature for the Shop Menu to allow developers to have a shop with automagically added items based off a certain Criteria. In the core series games, items are gated behind badge progression - this feature allows for that an further expands it to gate items based on any criteria the developer wants. + +## Usage + +To use this feature, simply use `pokemart` as you normally do. The real Sauce will be the Criterias that the items would get filtered on, as all items are added by Expansion's default. +``` +MartScript:: + lock + faceplayer + message gText_HowMayIServeYou + waitmessage + pokemart + msgbox gText_PleaseComeAgain, MSGBOX_DEFAULT + release + end +``` +You can also use it without any arguments to instead use `Pokemart_DefaultItemList` in `data/scripts/mart_clerk.inc`, useful for general pokemart shops. + +## What is a Criteria? + +Criteria is the meat that helps the code automagically add items based on your feeded items list. By default, all items are added regardless to not break existing shops. This prevents unintended items from appearing. + +Internally, a Criteria is actually a *function* that returns either `TRUE` or `FALSE` depending if a criteria is fulfilled. + +## Adding Criteria to an Item + +Adding a Criteria to an Item is simple enough, you'd just add the fields at the bottom of an item's data: +```diff + [ITEM_POKE_BALL] = + { + .name = ITEM_NAME("Poké Ball"), + .price = 200, + .description = COMPOUND_STRING( + "A tool used for\n" + "catching wild\n" + "Pokémon."), + .pocket = POCKET_POKE_BALLS, + .type = ITEM_USE_BAG_MENU, + .battleUsage = EFFECT_ITEM_THROW_BALL, + .secondaryId = BALL_POKE, + .iconPic = gItemIcon_PokeBall, + .iconPalette = gItemIconPalette_PokeBall, ++ .shopCriteriaFunc = , + }, +``` + +Below are extra helper functions for the average usecases. They're not meant to be directly used as the Criteria function itself, only for the actual Criteria function you're making. Be sure to remove the `UNUSED` label from the function you're using after. + +### `ShopCriteriaByBadgeCount` +This function is what you'll probably be using the most. You must provide one argument containing a number of total badges the player must own. +Example: +``` +bool32 ShopCriteriaByTwoBadges(enum Item item) +{ + return ShopCriteriaByBadgeCount(2); +} +``` + +### `ShopCriteriaByFlag` +This function is basically what `BadgeCount` partially do, but you'd explicitly use one flag instead. Useful if you need a certain badge flag instead of just the _total amount_ of badges, or a quest-only item. You must provide one argument for the flag id you want, the flag ids found in [`include/constants/flags.h`](/include/constants/flags.h), like `FLAG_BADGE07_GET`. This function will returns `TRUE`/`FALSE` depending on if the flag is set or not. +Example: +``` +bool32 ShopCriteriaByChampionFlag(enum Item item) +{ + return ShopCriteriaByFlag(FLAG_IS_CHAMPION); +} +``` + +### `ShopCriteriaByVar` +This function is basically the `Flag` function but with variables instead. You must provide both the variable id found in [`include/constants/vars.h`](/include/constants/vars.h) and a value to compare with said variable id as the arguments. +Example: +``` +bool32 ShopCriteriaBySpecificValueInSpecificVar(enum Item item) +{ + return ShopCriteriaByVar(VAR_ASH_GATHER_COUNT, 20); +} +``` + +## Create a Custom Criteria + +First, add a new function for your criteria in [`src/shop_criteria.c`](/src/shop_criteria.c), preferably after the `// Add new Criterias below!` comment, like so: +```diff + // Add new Criterias below! ++bool32 ShopCriteriaByMyNeed(enum Item item) ++{ ++ // add your stuff here, be sure to handle both `TRUE`/`FALSE` return values! ++} +``` + +Then, you'll need to add a declaration for your custom function so that it can be used by your items. +```diff + // Add new Criterias below! ++bool32 ShopCriteriaByMyNeed(enum Item item); + + #endif // GUARD_SHOP_CRITERIA_H +``` + +And that's it! You should now be good to go and can immediately use that function for your items. +```diff + [ITEM_POKE_BALL] = + { + .name = ITEM_NAME("Poké Ball"), + .price = 200, + .description = COMPOUND_STRING( + "A tool used for\n" + "catching wild\n" + "Pokémon."), + .pocket = POCKET_POKE_BALLS, + .type = ITEM_USE_BAG_MENU, + .battleUsage = EFFECT_ITEM_THROW_BALL, + .secondaryId = BALL_POKE, + .iconPic = gItemIcon_PokeBall, + .iconPalette = gItemIconPalette_PokeBall, ++ .shopCriteriaFunc = ShopCriteriaByMyNeed, + }, +``` + +### Specific Example + +We want to allow the player to purchase the Light Ball, but only after they have already captured Pikachu and defeated Wattson's final rematch. + +#### [`src/shop_criteria.c`](/src/shop_criteria.c) +```diff + // Add new Criterias below! ++bool32 ShopCriteriaHasPikachu(enum Item item) ++{ ++ if (FlagGet(TRAINER_FLAGS_START + WATTSON_5) == FALSE) ++ return FALSE; ++ ++ return (GetSetPokedexFlag(SpeciesToNationalPokedexNum(SPECIES_PIKACHU), FLAG_GET_CAUGHT)); ++} +``` + +#### [`include/shop_criteria.h`](/include/shop_criteria.h) +```diff + // Add new Criterias below! ++bool32 ShopCriteriaHasPikachu(enum Item item); +``` + +#### [`src/data/items`](/src/data/items.c) +```diff + [ITEM_LIGHT_BALL] = + { + .name = ITEM_NAME("Light Ball"), + .price = (I_PRICE >= GEN_7) ? 1000 : 100, + .holdEffect = HOLD_EFFECT_LIGHT_BALL, + .description = COMPOUND_STRING( + "A hold item that\n" + "raises the Atk and\n" + "Sp. Atk of Pikachu."), + .pocket = POCKET_ITEMS, + .sortType = ITEM_TYPE_HELD_ITEM, + .type = ITEM_USE_BAG_MENU, + .fieldUseFunc = ItemUseOutOfBattle_CannotUse, + .flingPower = 30, + .iconPic = gItemIcon_LightBall, + .iconPalette = gItemIconPalette_LightBall, ++ .shopCriteriaFunc = ShopCriteriaHasPikachu, + }, +``` diff --git a/docs/tutorials/img/dynamic_shop/showcase.gif b/docs/tutorials/img/dynamic_shop/showcase.gif new file mode 100755 index 0000000000..fe4fd6d4e1 Binary files /dev/null and b/docs/tutorials/img/dynamic_shop/showcase.gif differ diff --git a/include/item.h b/include/item.h index 3eb477842d..b19a7dd747 100644 --- a/include/item.h +++ b/include/item.h @@ -64,6 +64,7 @@ enum PACKED ItemSortType }; typedef void (*ItemUseFunc)(u8); +typedef bool32 (*ShopCriteriaFunc)(enum Item); struct ItemInfo { @@ -85,6 +86,7 @@ struct ItemInfo u8 flingPower; const u32 *iconPic; const u16 *iconPalette; + ShopCriteriaFunc shopCriteriaFunc; }; struct ALIGNED(2) BagPocket @@ -277,5 +279,7 @@ u32 GetItemStatus1Mask(enum Item itemId); bool32 ItemHasVolatileFlag(enum Item itemId, enum Volatile volatile); u32 GetItemSellPrice(enum Item itemId); bool32 IsHoldEffectChoice(enum HoldEffect holdEffect); +ShopCriteriaFunc GetItemShopCriteriaFunc(u32 itemId); +bool32 IsItemShopCriteriaFulfilled(u32 itemId); #endif // GUARD_ITEM_H diff --git a/include/shop_criteria.h b/include/shop_criteria.h new file mode 100644 index 0000000000..6f6ec11db0 --- /dev/null +++ b/include/shop_criteria.h @@ -0,0 +1,9 @@ +#ifndef GUARD_SHOP_CRITERIA_H +#define GUARD_SHOP_CRITERIA_H + +void TryBuildDynamicShopItemList(const u16 **ogItemList, u16 *resultingTotal); +void TryFreeDynamicShopItemList(const u16 **ogItemList); + +// Add new Criterias below! + +#endif // GUARD_SHOP_CRITERIA_H diff --git a/src/item.c b/src/item.c index 4a4b6ba552..cec77d7dfb 100644 --- a/src/item.c +++ b/src/item.c @@ -15,6 +15,7 @@ #include "battle_pyramid.h" #include "battle_pyramid_bag.h" #include "graphics.h" +#include "shop_criteria.h" #include "constants/battle.h" #include "constants/items.h" #include "constants/moves.h" @@ -951,3 +952,18 @@ bool32 IsHoldEffectChoice(enum HoldEffect holdEffect) || holdEffect == HOLD_EFFECT_CHOICE_SCARF || holdEffect == HOLD_EFFECT_CHOICE_SPECS; } + +ShopCriteriaFunc GetItemShopCriteriaFunc(u32 itemId) +{ + return gItemsInfo[SanitizeItemId(itemId)].shopCriteriaFunc; +} + +bool32 IsItemShopCriteriaFulfilled(u32 itemId) +{ + ShopCriteriaFunc func = GetItemShopCriteriaFunc(itemId); + + if (!func) + return TRUE; + + return func(SanitizeItemId(itemId)); +} diff --git a/src/shop.c b/src/shop.c index c6ff864025..070cc6a280 100644 --- a/src/shop.c +++ b/src/shop.c @@ -34,6 +34,7 @@ #include "strings.h" #include "text_window.h" #include "tv.h" +#include "shop_criteria.h" #include "constants/decorations.h" #include "constants/event_objects.h" #include "constants/items.h" @@ -340,6 +341,8 @@ static const u8 sShopBuyMenuTextColors[][3] = [COLORID_GRAY_CURSOR] = {0, 3, 2}, }; +static const u16 sShopItemsListDummy[] = { ITEM_NONE }; + static u8 CreateShopMenu(u8 martType) { int numMenuItems; @@ -385,6 +388,12 @@ static void SetShopItemsForSale(const u16 *items) sMartInfo.itemList = items; sMartInfo.itemCount = 0; + assertf(items != NULL, "Shop items list should never be set as NULL") + { + sMartInfo.itemList = sShopItemsListDummy; + return; + } + // Read items until ITEM_NONE / DECOR_NONE is reached while (sMartInfo.itemList[i]) { @@ -521,6 +530,8 @@ static void CB2_InitBuyMenu(void) sShopData->scrollIndicatorsTaskId = TASK_NONE; sShopData->itemSpriteIds[0] = SPRITE_NONE; sShopData->itemSpriteIds[1] = SPRITE_NONE; + if (sMartInfo.martType == MART_TYPE_NORMAL) + TryBuildDynamicShopItemList(&sMartInfo.itemList, &sMartInfo.itemCount); BuyMenuBuildListMenuTemplate(); BuyMenuInitBgs(); FillBgTilemapBufferRect_Palette0(0, 0, 0, 0, 0x20, 0x20); @@ -550,6 +561,9 @@ static void CB2_InitBuyMenu(void) static void BuyMenuFreeMemory(void) { + if (sMartInfo.martType == MART_TYPE_NORMAL) + TryFreeDynamicShopItemList(&sMartInfo.itemList); + Free(sShopData); Free(sListMenuItems); Free(sItemNames); diff --git a/src/shop_criteria.c b/src/shop_criteria.c new file mode 100644 index 0000000000..8071673363 --- /dev/null +++ b/src/shop_criteria.c @@ -0,0 +1,83 @@ +#include "global.h" +#include "item.h" +#include "script.h" +#include "event_data.h" +#include "malloc.h" +#include "shop_criteria.h" + +static EWRAM_DATA const u16 *sDynamicShopItemListRef = NULL; + +// Remove the UNUSED if you'll use the functions! +static UNUSED bool32 ShopCriteriaByBadgeCount(u32 count); +static UNUSED bool32 ShopCriteriaByFlag(u32 flagId); +static UNUSED bool32 ShopCriteriaByVar(u32 varId, u32 varValue); + +void TryBuildDynamicShopItemList(const u16 **ogItemList, u16 *resultingTotal) +{ + sDynamicShopItemListRef = *ogItemList; + + u16 *list = AllocZeroed((*resultingTotal + 1) * sizeof(u16)); + u32 overallIdx = 0, idx = 0; + + while (idx < *resultingTotal) + { + enum Item item = sDynamicShopItemListRef[idx]; + + if (IsItemShopCriteriaFulfilled(item)) + { + list[overallIdx] = item; + overallIdx++; + } + + idx++; + } + + list[overallIdx] = ITEM_NONE; + + *ogItemList = list; + *resultingTotal = overallIdx; +} + +void TryFreeDynamicShopItemList(const u16 **ogItemList) +{ + Free((u16 *)*ogItemList); + *ogItemList = sDynamicShopItemListRef; +} + +// Add new Criterias below! + +static UNUSED bool32 ShopCriteriaByBadgeCount(u32 count) +{ + u32 badgeCount = 0; + + for (u32 badgeFlag = FLAG_BADGE01_GET; badgeFlag < FLAG_BADGE01_GET + NUM_BADGES; badgeFlag++) + { + if (FlagGet(badgeFlag)) + badgeCount++; + } + + if (badgeCount >= count) + return TRUE; + + return FALSE; +} + +// These two below are somewhat identical to ShopCriteriaByBadgeCount +// but uses only one specific event var/flag check. Useful if you need +// a specific badge flag instead of just the badge total. + +static UNUSED bool32 ShopCriteriaByFlag(u32 flagId) +{ + if (FlagGet(flagId)) + return TRUE; + + return FALSE; +} + +static UNUSED bool32 ShopCriteriaByVar(u32 varId, u32 varValue) +{ + if (VarGet(varId) >= varValue) + return TRUE; + + return FALSE; +}