add option gating shields / tunics in shop behind finding one first (#6700)

This commit is contained in:
Philip Dubé
2026-06-09 19:45:04 +00:00
committed by GitHub
parent eaad45879a
commit 8649085862
12 changed files with 142 additions and 2 deletions
@@ -2716,6 +2716,15 @@ typedef enum {
// - *EnGirlACanBuyResult
VB_CAN_BUY_BOMBCHUS,
// #### `result`
// ```c
// false
// ```
// #### `args`
// - *EnGirlACanBuyResult
// - `RAND_INF`
VB_CAN_BUY_SHOP_SHIELD_OR_TUNIC,
// #### `result`
// ```c
// true
@@ -402,7 +402,8 @@ bool AddCheckToLogic(LocationAccess& locPair, GetAccessibleLocationsStruct& gals
(quest == RCQUEST_VANILLA && ctx->GetDungeons()->GetDungeonFromScene(parentRegion->scene)->IsVanilla()) ||
(quest == RCQUEST_MQ && ctx->GetDungeons()->GetDungeonFromScene(parentRegion->scene)->IsMQ()));
if (!location->IsAddedToPool() && locPair.ConditionsMet(parentRegion, logic->CalculatingAvailableChecks)) {
if (!location->IsAddedToPool() && locPair.ConditionsMet(parentRegion, logic->CalculatingAvailableChecks) &&
!logic->ShopItemNotForSale(loc)) {
location->AddToPool();
if (locItem == RG_NONE || logic->CalculatingAvailableChecks) {
@@ -789,12 +790,17 @@ static void CalculateBarren() {
NotBarren[RA_NONE] = true;
NotBarren[RA_LINKS_POCKET] = true;
// When shop shields/tunics are gated behind finding a shield, those items become relevant, so
// regions holding a shield or tunic should not be hinted foolish.
const bool shieldTunicGate = ctx->GetOption(RSK_SHOP_SHIELDS_AND_TUNICS_ONLY_REFILL).Is(RO_GENERIC_ON);
for (RandomizerCheck loc : ctx->allLocations) {
Rando::ItemLocation* itemLoc = ctx->GetItemLocation(loc);
std::set<RandomizerArea> locAreas = itemLoc->GetAreas();
for (auto locArea : locAreas) {
// If a location has a major item or is a way of the hero location, it is not barren
if (NotBarren[locArea] == false && (itemLoc->GetPlacedItem().IsMajorItem() || itemLoc->IsWothCandidate())) {
if (NotBarren[locArea] == false && (itemLoc->GetPlacedItem().IsMajorItem() || itemLoc->IsWothCandidate() ||
(shieldTunicGate && itemLoc->GetPlacedItem().IsShieldOrTunic()))) {
NotBarren[locArea] = true;
}
}
@@ -59,6 +59,7 @@ extern "C" {
#include "src/overlays/actors/ovl_Fishing/z_fishing.h"
#include "src/overlays/actors/ovl_Obj_Bean/z_obj_bean.h"
#include "src/overlays/actors/ovl_En_Heishi2/z_en_heishi2.h"
#include "src/overlays/actors/ovl_En_GirlA/z_en_girla.h"
#include "draw.h"
static ObjectExtension::Register<DnsItemEntry> RegisterDnsItemEntryOverride;
@@ -445,6 +446,23 @@ void RandomizerOnItemReceiveHandler(GetItemEntry receivedItemEntry) {
randomizerQueuedItemEntry = GET_ITEM_NONE;
}
if (receivedItemEntry.modIndex == MOD_NONE) {
switch (receivedItemEntry.itemId) {
case ITEM_SHIELD_DEKU:
Flags_SetRandomizerInf(RAND_INF_HAS_FOUND_DEKU_SHIELD);
break;
case ITEM_SHIELD_HYLIAN:
Flags_SetRandomizerInf(RAND_INF_HAS_FOUND_HYLIAN_SHIELD);
break;
case ITEM_TUNIC_GORON:
Flags_SetRandomizerInf(RAND_INF_HAS_FOUND_GORON_TUNIC);
break;
case ITEM_TUNIC_ZORA:
Flags_SetRandomizerInf(RAND_INF_HAS_FOUND_ZORA_TUNIC);
break;
}
}
if (receivedItemEntry.modIndex == MOD_RANDOMIZER && receivedItemEntry.getItemId == RG_MAGIC_BEAN_PACK) {
if (OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_SKIP_PLANTING_BEANS)) {
gSaveContext.sceneFlags[SCENE_DEATH_MOUNTAIN_CRATER].swch |= (1 << 3);
@@ -959,6 +977,18 @@ void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_l
case VB_CRAWL:
*should = *should && Flags_GetRandomizerInf(RAND_INF_CAN_CRAWL);
break;
case VB_CAN_BUY_SHOP_SHIELD_OR_TUNIC: {
// Gate non-randomized shop shields/tunics behind finding a non-shop copy.
if (RAND_GET_OPTION(RSK_SHOP_SHIELDS_AND_TUNICS_ONLY_REFILL).Is(RO_GENERIC_ON)) {
EnGirlACanBuyResult* canBuy = va_arg(args, EnGirlACanBuyResult*);
RandomizerInf requiredInf = (RandomizerInf)va_arg(args, int);
if (!Flags_GetRandomizerInf(requiredInf)) {
*canBuy = CANBUY_RESULT_CANT_GET_NOW;
*should = true;
}
}
break;
}
case VB_ALLOW_ENTRANCE_CS_FOR_EITHER_AGE: {
s32 entranceIndex = va_arg(args, s32);
+23
View File
@@ -78,6 +78,12 @@ const std::string& Item::GetColor() const {
}
bool Item::IsAdvancement() const {
// With the shop shield/tunic gate on, a found Deku/Hylian Shield unlocks its shop copy, so it must
// be treated as progression. Tunics already are.
if (!advancement && (randomizerGet == RG_DEKU_SHIELD || randomizerGet == RG_HYLIAN_SHIELD) &&
Context::GetInstance()->GetOption(RSK_SHOP_SHIELDS_AND_TUNICS_ONLY_REFILL).Is(RO_GENERIC_ON)) {
return true;
}
return advancement;
}
@@ -478,6 +484,23 @@ bool Item::IsMajorItem() const {
return IsAdvancement();
}
bool Item::IsShieldOrTunic() const {
switch (randomizerGet) {
case RG_DEKU_SHIELD:
case RG_HYLIAN_SHIELD:
case RG_MIRROR_SHIELD:
case RG_GORON_TUNIC:
case RG_ZORA_TUNIC:
case RG_BUY_DEKU_SHIELD:
case RG_BUY_HYLIAN_SHIELD:
case RG_BUY_GORON_TUNIC:
case RG_BUY_ZORA_TUNIC:
return true;
default:
return false;
}
}
RandomizerHintTextKey Item::GetHintKey() const {
return hintKey;
}
+1
View File
@@ -60,6 +60,7 @@ class Item {
bool IsPlaythrough() const;
bool IsBottleItem() const;
bool IsMajorItem() const;
bool IsShieldOrTunic() const;
RandomizerHintTextKey GetHintKey() const;
const HintText& GetHint() const;
GetItemCategory GetCategory();
+39
View File
@@ -1269,6 +1269,28 @@ bool Logic::BombchusEnabled() {
: HasItem(RG_BOMB_BAG);
}
// With the shop shield/tunic gate enabled, a shop slot selling a shield/tunic is considered not-for-sale
// in logic until the matching item has been found in the world (which sets its RandomizerInf). Shop slots
// are randomized, so this keys off the item actually placed in the slot rather than a fixed location.
bool Logic::ShopItemNotForSale(RandomizerCheck loc) {
if (ctx->GetOption(RSK_SHOP_SHIELDS_AND_TUNICS_ONLY_REFILL).IsNot(RO_GENERIC_ON) ||
StaticData::GetLocation(loc)->GetRCType() != RCTYPE_SHOP) {
return false;
}
switch (ctx->GetItemLocation(loc)->GetPlacedRandomizerGet()) {
case RG_BUY_DEKU_SHIELD:
return !CheckRandoInf(RAND_INF_HAS_FOUND_DEKU_SHIELD);
case RG_BUY_HYLIAN_SHIELD:
return !CheckRandoInf(RAND_INF_HAS_FOUND_HYLIAN_SHIELD);
case RG_BUY_GORON_TUNIC:
return !CheckRandoInf(RAND_INF_HAS_FOUND_GORON_TUNIC);
case RG_BUY_ZORA_TUNIC:
return !CheckRandoInf(RAND_INF_HAS_FOUND_ZORA_TUNIC);
default:
return false;
}
}
// TODO: Implement Ammo Drop Setting in place of bombchu drops
bool Logic::BombchuRefill() {
return Get(LOGIC_BUY_BOMBCHUS) || Get(LOGIC_COULD_PLAY_BOWLING) || Get(LOGIC_CARPET_MERCHANT) ||
@@ -2081,6 +2103,23 @@ void Logic::ApplyItemEffect(Item& item, bool state) {
} break;
case ITEMTYPE_EQUIP: {
RandomizerGet itemRG = item.GetRandomizerGet();
// Finding a non-shop shield/tunic unlocks its matching shop copy when that gate is enabled.
switch (itemRG) {
case RG_DEKU_SHIELD:
SetRandoInf(RAND_INF_HAS_FOUND_DEKU_SHIELD, state);
break;
case RG_HYLIAN_SHIELD:
SetRandoInf(RAND_INF_HAS_FOUND_HYLIAN_SHIELD, state);
break;
case RG_GORON_TUNIC:
SetRandoInf(RAND_INF_HAS_FOUND_GORON_TUNIC, state);
break;
case RG_ZORA_TUNIC:
SetRandoInf(RAND_INF_HAS_FOUND_ZORA_TUNIC, state);
break;
default:
break;
}
if (itemRG == RG_DEKU_SHIELD || itemRG == RG_HYLIAN_SHIELD) {
return;
}
+1
View File
@@ -77,6 +77,7 @@ class Logic {
bool CanAttack();
bool BombchusEnabled();
bool BombchuRefill();
bool ShopItemNotForSale(RandomizerCheck loc);
bool HookshotOrBoomerang();
bool ScarecrowsSong();
bool BlueFire();
@@ -398,6 +398,10 @@ void Settings::CreateOptionDescriptions() {
"After choosing a price, set it to the affordable amount based on the wallet required.\n\n"
"Affordable prices per tier: starter = 1, adult = 100, giant = 201, tycoon = 501\n\n"
"Use this to enable wallet tier locking, but make shop items not as expensive as they could be.";
mOptionDescriptions[RSK_SHOP_SHIELDS_AND_TUNICS_ONLY_REFILL] =
"Non-randomized shields and tunics sold in shops cannot be purchased until you have first found a shield "
"elsewhere. "
"Regions containing a shield or tunic will not be hinted foolish.";
mOptionDescriptions[RSK_FISHSANITY] =
"Off - Fish will not be shuffled. No changes will be made to fishing behavior.\n\n"
"Shuffle only Hyrule Loach - Allows you to earn an item by catching the Hyrule Loach at the fishing pond and "
@@ -2925,6 +2925,13 @@ RANDO_ENUM_ITEM(RAND_INF_GANONS_CASTLE_MQ_WATER_TRIAL_SECOND_DOOR_RED_ICE_3)
RANDO_ENUM_ITEM(RAND_INF_GANONS_CASTLE_MQ_WATER_TRIAL_SECOND_DOOR_RED_ICE_4)
RANDO_ENUM_ITEM(RAND_INF_GANONS_CASTLE_MQ_WATER_TRIAL_SECOND_DOOR_RED_ICE_5)
// Set when a non-shop shield/tunic is found, gating the matching shop copy behind it
// (RSK_SHOP_SHIELDS_AND_TUNICS_ONLY_REFILL).
RANDO_ENUM_ITEM(RAND_INF_HAS_FOUND_DEKU_SHIELD)
RANDO_ENUM_ITEM(RAND_INF_HAS_FOUND_HYLIAN_SHIELD)
RANDO_ENUM_ITEM(RAND_INF_HAS_FOUND_GORON_TUNIC)
RANDO_ENUM_ITEM(RAND_INF_HAS_FOUND_ZORA_TUNIC)
RANDO_ENUM_ITEM(RAND_INF_MAX)
RANDO_ENUM_END(RandomizerInf)
@@ -76,6 +76,7 @@ RANDO_ENUM_ITEM(RSK_SHOPSANITY_PRICES_ADULT_WALLET_WEIGHT)
RANDO_ENUM_ITEM(RSK_SHOPSANITY_PRICES_GIANT_WALLET_WEIGHT)
RANDO_ENUM_ITEM(RSK_SHOPSANITY_PRICES_TYCOON_WALLET_WEIGHT)
RANDO_ENUM_ITEM(RSK_SHOPSANITY_PRICES_AFFORDABLE)
RANDO_ENUM_ITEM(RSK_SHOP_SHIELDS_AND_TUNICS_ONLY_REFILL)
RANDO_ENUM_ITEM(RSK_SHUFFLE_SCRUBS)
RANDO_ENUM_ITEM(RSK_SCRUBS_PRICES)
RANDO_ENUM_ITEM(RSK_SCRUBS_PRICES_FIXED_PRICE)
@@ -650,6 +650,7 @@ void Settings::CreateOptions() {
OPT_U8(RSK_SHOPSANITY_PRICES_GIANT_WALLET_WEIGHT, "Shops Giant Wallet Weight", {NumOpts(0, 100)}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("ShopsanityGiantWalletWeight"), mOptionDescriptions[RSK_SHOPSANITY_PRICES_GIANT_WALLET_WEIGHT], WIDGET_CVAR_SLIDER_INT, 10, true, nullptr, IMFLAG_NONE);
OPT_U8(RSK_SHOPSANITY_PRICES_TYCOON_WALLET_WEIGHT, "Shops Tycoon Wallet Weight", {NumOpts(0, 100)}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("ShopsanityTycoonWalletWeight"), mOptionDescriptions[RSK_SHOPSANITY_PRICES_TYCOON_WALLET_WEIGHT], WIDGET_CVAR_SLIDER_INT, 10, true, nullptr, IMFLAG_NONE);
OPT_BOOL(RSK_SHOPSANITY_PRICES_AFFORDABLE, "Shops Affordable Prices", CVAR_RANDOMIZER_SETTING("ShopsanityPricesAffordable"), mOptionDescriptions[RSK_SHOPSANITY_PRICES_AFFORDABLE]);
OPT_BOOL(RSK_SHOP_SHIELDS_AND_TUNICS_ONLY_REFILL, "Gate Shop Shields & Tunics", CVAR_RANDOMIZER_SETTING("ShopShieldsTunicsGate"), mOptionDescriptions[RSK_SHOP_SHIELDS_AND_TUNICS_ONLY_REFILL]);
OPT_U8(RSK_SHUFFLE_TOKENS, "Token Shuffle", {"Off", "Dungeons", "Overworld", "All Tokens"}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("ShuffleTokens"), mOptionDescriptions[RSK_SHUFFLE_TOKENS], WIDGET_CVAR_COMBOBOX, RO_TOKENSANITY_OFF);
OPT_U8(RSK_SHUFFLE_SCRUBS, "Scrubs Shuffle", {"Off", "One-Time Only", "All"}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("ShuffleScrubs"), mOptionDescriptions[RSK_SHUFFLE_SCRUBS], WIDGET_CVAR_COMBOBOX, RO_SCRUBS_OFF);
OPT_CALLBACK(RSK_SHUFFLE_SCRUBS, {
@@ -1910,6 +1911,7 @@ void Settings::CreateOptions() {
&mOptions[RSK_SHOPSANITY_PRICES_GIANT_WALLET_WEIGHT],
&mOptions[RSK_SHOPSANITY_PRICES_TYCOON_WALLET_WEIGHT],
&mOptions[RSK_SHOPSANITY_PRICES_AFFORDABLE],
&mOptions[RSK_SHOP_SHIELDS_AND_TUNICS_ONLY_REFILL],
&mOptions[RSK_SHUFFLE_SCRUBS],
&mOptions[RSK_SCRUBS_PRICES],
&mOptions[RSK_SCRUBS_PRICES_FIXED_PRICE],
@@ -2142,6 +2144,7 @@ void Settings::CreateOptions() {
&mOptions[RSK_SHOPSANITY_PRICES_GIANT_WALLET_WEIGHT],
&mOptions[RSK_SHOPSANITY_PRICES_TYCOON_WALLET_WEIGHT],
&mOptions[RSK_SHOPSANITY_PRICES_AFFORDABLE],
&mOptions[RSK_SHOP_SHIELDS_AND_TUNICS_ONLY_REFILL],
&mOptions[RSK_FISHSANITY],
&mOptions[RSK_FISHSANITY_POND_COUNT],
&mOptions[RSK_FISHSANITY_AGE_SPLIT],
@@ -664,6 +664,10 @@ s32 EnGirlA_CanBuy_Longsword(PlayState* play, EnGirlA* this) {
}
s32 EnGirlA_CanBuy_HylianShield(PlayState* play, EnGirlA* this) {
s32 canBuy;
if (GameInteractor_Should(VB_CAN_BUY_SHOP_SHIELD_OR_TUNIC, false, &canBuy, RAND_INF_HAS_FOUND_HYLIAN_SHIELD)) {
return canBuy;
}
if (CHECK_OWNED_EQUIP_ALT(EQUIP_TYPE_SHIELD, EQUIP_INV_SHIELD_HYLIAN)) {
return CANBUY_RESULT_CANT_GET_NOW;
}
@@ -677,6 +681,10 @@ s32 EnGirlA_CanBuy_HylianShield(PlayState* play, EnGirlA* this) {
}
s32 EnGirlA_CanBuy_DekuShield(PlayState* play, EnGirlA* this) {
s32 canBuy;
if (GameInteractor_Should(VB_CAN_BUY_SHOP_SHIELD_OR_TUNIC, false, &canBuy, RAND_INF_HAS_FOUND_DEKU_SHIELD)) {
return canBuy;
}
if (CHECK_OWNED_EQUIP_ALT(EQUIP_TYPE_SHIELD, EQUIP_INV_SHIELD_DEKU)) {
return CANBUY_RESULT_CANT_GET_NOW;
}
@@ -690,6 +698,10 @@ s32 EnGirlA_CanBuy_DekuShield(PlayState* play, EnGirlA* this) {
}
s32 EnGirlA_CanBuy_GoronTunic(PlayState* play, EnGirlA* this) {
s32 canBuy;
if (GameInteractor_Should(VB_CAN_BUY_SHOP_SHIELD_OR_TUNIC, false, &canBuy, RAND_INF_HAS_FOUND_GORON_TUNIC)) {
return canBuy;
}
if (LINK_AGE_IN_YEARS == YEARS_CHILD &&
(!IS_RANDO || Randomizer_GetSettingValue(RSK_SHOPSANITY) == RO_SHOPSANITY_OFF)) {
return CANBUY_RESULT_CANT_GET_NOW;
@@ -707,6 +719,10 @@ s32 EnGirlA_CanBuy_GoronTunic(PlayState* play, EnGirlA* this) {
}
s32 EnGirlA_CanBuy_ZoraTunic(PlayState* play, EnGirlA* this) {
s32 canBuy;
if (GameInteractor_Should(VB_CAN_BUY_SHOP_SHIELD_OR_TUNIC, false, &canBuy, RAND_INF_HAS_FOUND_ZORA_TUNIC)) {
return canBuy;
}
if (LINK_AGE_IN_YEARS == YEARS_CHILD &&
(!IS_RANDO || Randomizer_GetSettingValue(RSK_SHOPSANITY) == RO_SHOPSANITY_OFF)) {
return CANBUY_RESULT_CANT_GET_NOW;