From 29668a06d63edb3bf6db702f53418d1c1b342f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:00:23 +0000 Subject: [PATCH] Progressive BGS (#7037) --- .../randomizer/3drando/item_pool.cpp | 12 +++++++++--- .../Enhancements/randomizer/hook_handlers.cpp | 3 +++ soh/soh/Enhancements/randomizer/item.cpp | 5 +++-- soh/soh/Enhancements/randomizer/logic.cpp | 18 ++++++++++++++++-- .../randomizer/option_descriptions.cpp | 6 ++++++ soh/soh/Enhancements/randomizer/randomizer.cpp | 5 ++++- .../randomizerEnums/RandomizerSettingKey.h | 1 + .../randomizer/randomizer_item_tracker.cpp | 8 +++++--- soh/soh/Enhancements/randomizer/settings.cpp | 3 +++ 9 files changed, 50 insertions(+), 11 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp index 1d2c7f443f..0aec234877 100644 --- a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp @@ -182,7 +182,11 @@ void GenerateItemPool() { AddFixedItemToPool(RG_PROGRESSIVE_HOOKSHOT, 2 - ctx->GetOption(RSK_STARTING_HOOKSHOT).Get()); if (!ctx->GetOption(RSK_STARTING_HYLIAN_SHIELD)) AddItemToPool(RG_HYLIAN_SHIELD, 1, 1, 1, 1); AddItemToPool(RG_DOUBLE_DEFENSE, 2, 1, 0, 0); - if (ctx->GetOption(RSK_STARTING_BIGGORON_SWORD).IsNot(RO_STARTING_BGS_BIGGORON_SWORD)) { + if (ctx->GetOption(RSK_PROGRESSIVE_GORON_SWORD)) { + int startGoronSword = ctx->GetOption(RSK_STARTING_BIGGORON_SWORD).Get(); + AddItemToPool(RG_PROGRESSIVE_GORONSWORD, std::max(0, 3 - startGoronSword), std::max(0, 2 - startGoronSword), + std::max(0, 2 - startGoronSword), std::max(0, 1 - startGoronSword)); + } else if (ctx->GetOption(RSK_STARTING_BIGGORON_SWORD).IsNot(RO_STARTING_BGS_BIGGORON_SWORD)) { AddItemToPool(RG_BIGGORON_SWORD, 2, 1, 1, 0); } bool isScrubs = ctx->GetOption(RSK_SHUFFLE_SCRUBS).Is(RO_SCRUBS_ALL); @@ -527,7 +531,7 @@ void GenerateItemPool() { if (ctx->GetOption(RSK_SHUFFLE_MERCHANTS).Is(RO_SHUFFLE_MERCHANTS_ALL_BUT_BEANS) || ctx->GetOption(RSK_SHUFFLE_MERCHANTS).Is(RO_SHUFFLE_MERCHANTS_ALL)) { - if (/*!ProgressiveGoronSword TODO: Implement Progressive Goron Sword*/ + if (!ctx->GetOption(RSK_PROGRESSIVE_GORON_SWORD) && ctx->GetOption(RSK_STARTING_BIGGORON_SWORD).Is(RO_STARTING_BGS_OFF)) { AddFixedItemToPool(RG_GIANTS_KNIFE, 1); } @@ -538,7 +542,9 @@ void GenerateItemPool() { } } else { ctx->PlaceItemInLocation(RC_KAK_GRANNYS_SHOP, RG_BLUE_POTION_REFILL, false, true); - ctx->PlaceItemInLocation(RC_GC_MEDIGORON, RG_GIANTS_KNIFE, false, true); + // when progressive, Medigoron only replaces broken knives, so he never hands out a first one + ctx->PlaceItemInLocation( + RC_GC_MEDIGORON, ctx->GetOption(RSK_PROGRESSIVE_GORON_SWORD) ? RG_SOLD_OUT : RG_GIANTS_KNIFE, false, true); ctx->PlaceItemInLocation(RC_WASTELAND_BOMBCHU_SALESMAN, RG_BOMBCHU_10, false, true); } diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index 5377d0622b..0503afa759 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -1495,6 +1495,9 @@ void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_l Flags_UnsetInfTable(INFTABLE_B1); *should = true; } + } else if (id == VB_BE_ELIGIBLE_FOR_GIANTS_KNIFE_PURCHASE && RAND_GET_OPTION(RSK_PROGRESSIVE_GORON_SWORD)) { + // the progressive sword carries the first knife, leaving Medigoron to replace broken ones + *should = false; } break; } diff --git a/soh/soh/Enhancements/randomizer/item.cpp b/soh/soh/Enhancements/randomizer/item.cpp index 1e12868573..0606618d6c 100644 --- a/soh/soh/Enhancements/randomizer/item.cpp +++ b/soh/soh/Enhancements/randomizer/item.cpp @@ -370,8 +370,9 @@ std::shared_ptr Item::GetGIEntry() const { // NOLINT(*-no-recursio break; } break; - case RG_PROGRESSIVE_GORONSWORD: // todo progressive? - actual = RG_BIGGORON_SWORD; + case RG_PROGRESSIVE_GORONSWORD: + // owning the slot means the knife was already given, breaking it doesn't clear the flag + actual = logic->CheckEquipment(EQUIP_FLAG_SWORD_BGS) ? RG_BIGGORON_SWORD : RG_GIANTS_KNIFE; break; case RG_PROGRESSIVE_BOMBCHU_BAG: if (OTRGlobals::Instance->gRandoContext->GetOption(RSK_BOMBCHU_BAG).Is(RO_BOMBCHU_BAG_SINGLE)) { diff --git a/soh/soh/Enhancements/randomizer/logic.cpp b/soh/soh/Enhancements/randomizer/logic.cpp index 97ad226cfd..035ebaabdd 100644 --- a/soh/soh/Enhancements/randomizer/logic.cpp +++ b/soh/soh/Enhancements/randomizer/logic.cpp @@ -73,10 +73,13 @@ bool Logic::HasItem(RandomizerGet itemName) { case RG_IRON_BOOTS: case RG_HOVER_BOOTS: return CheckEquipment(RandoGetToEquipFlag.at(itemName)); + case RG_PROGRESSIVE_GORONSWORD: case RG_GIANTS_KNIFE: - return CheckEquipment(RandoGetToEquipFlag.at(itemName)) || Get(LOGIC_MEDIGORON); + // progressive leaves Medigoron replacing broken knives, so the first one has to be found + return CheckEquipment(EQUIP_FLAG_SWORD_BGS) || + (!ctx->GetOption(RSK_PROGRESSIVE_GORON_SWORD) && Get(LOGIC_MEDIGORON)); case RG_BIGGORON_SWORD: - return CheckEquipment(RandoGetToEquipFlag.at(itemName)) && mSaveContext->bgsFlag; + return CheckEquipment(EQUIP_FLAG_SWORD_BGS) && mSaveContext->bgsFlag; case RG_GORONS_BRACELET: return CurrentUpgrade(UPG_STRENGTH); case RG_SILVER_GAUNTLETS: @@ -335,6 +338,7 @@ bool Logic::ItemUseAllowed(RandomizerGet itemName) { switch (itemName) { case RG_KOKIRI_SWORD: case RG_MASTER_SWORD: + case RG_PROGRESSIVE_GORONSWORD: case RG_GIANTS_KNIFE: case RG_BIGGORON_SWORD: return BAllowed(); @@ -578,6 +582,7 @@ bool Logic::CanUse(RandomizerGet itemName) { return IsAdult; // || MirrorShieldAsChild; case RG_MASTER_SWORD: return IsAdult; // || MasterSwordAsChild; + case RG_PROGRESSIVE_GORONSWORD: case RG_GIANTS_KNIFE: case RG_BIGGORON_SWORD: return IsAdult; // || BiggoronSwordAsChild; @@ -2262,6 +2267,15 @@ void Logic::ApplyItemEffect(Item& item, bool state) { } SetInventory(ITEM_BOMBCHU, (!state ? ITEM_NONE : ITEM_BOMBCHU)); } break; + case RG_PROGRESSIVE_GORONSWORD: + if (state ? CheckEquipment(EQUIP_FLAG_SWORD_BGS) : mSaveContext->bgsFlag) { + mSaveContext->bgsFlag = state; + } else if (state) { + mSaveContext->inventory.equipment |= EQUIP_FLAG_SWORD_BGS; + } else { + mSaveContext->inventory.equipment &= ~EQUIP_FLAG_SWORD_BGS; + } + break; case RG_PROGRESSIVE_MAGIC_METER: { auto realGI = item.GetGIEntry(); if (realGI->itemId == RG_MAGIC_INF && realGI->modIndex == MOD_RANDOMIZER) { diff --git a/soh/soh/Enhancements/randomizer/option_descriptions.cpp b/soh/soh/Enhancements/randomizer/option_descriptions.cpp index 3079737adf..2592bb602e 100644 --- a/soh/soh/Enhancements/randomizer/option_descriptions.cpp +++ b/soh/soh/Enhancements/randomizer/option_descriptions.cpp @@ -860,6 +860,12 @@ void Settings::CreateOptionDescriptions() { "\n" "If you have Bombchu Bag disabled, you will need a Bomb Bag " "and existing Bombchus for Bombchus to drop."; + mOptionDescriptions[RSK_PROGRESSIVE_GORON_SWORD] = + "Giant's Knife and Biggoron's Sword are shuffled as one progressive item: the first copy is the " + "breakable Giant's Knife, the second is Biggoron's Sword.\n" + "\n" + "Starting with Biggoron's Sword still starts you with both.\n" + "Medigoron only repairs broken Giant's Knife."; mOptionDescriptions[RSK_BLUE_FIRE_ARROWS] = "Ice Arrows act like Blue Fire, making them able to melt red ice. " "Item placement logic will respect this option, so it might be required to use this to progress."; diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index 9fba15043c..90854a1051 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -472,7 +472,10 @@ ItemObtainability Randomizer::GetItemObtainabilityFromRandomizerGet(RandomizerGe return Inventory_HasEmptyBottle() ? CAN_OBTAIN : CANT_OBTAIN_NEED_EMPTY_BOTTLE; // Trade Items - // case RG_PROGRESSIVE_GORONSWORD: + // Giant's Knife and Biggoron's Sword share a slot, bgsFlag marks the final upgrade. + // Giant's Knife itself stays obtainable, Medigoron replaces broken ones. + case RG_PROGRESSIVE_GORONSWORD: + return !gSaveContext.bgsFlag ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; // case RG_GIANTS_KNIFE: // Misc Items diff --git a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerSettingKey.h b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerSettingKey.h index e8926f38e7..4e457e91a1 100644 --- a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerSettingKey.h +++ b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerSettingKey.h @@ -308,6 +308,7 @@ RANDO_ENUM_ITEM(RSK_ROCS_FEATHER) RANDO_ENUM_ITEM(RSK_SHUFFLE_ICICLES) RANDO_ENUM_ITEM(RSK_SHUFFLE_RED_ICE) RANDO_ENUM_ITEM(RSK_STARTING_BUNNY_HOOD) +RANDO_ENUM_ITEM(RSK_PROGRESSIVE_GORON_SWORD) RANDO_ENUM_ITEM(RSK_MAX) RANDO_ENUM_END(RandomizerSettingKey) diff --git a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp index 49aad6c646..4368b640bc 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp @@ -882,13 +882,15 @@ void DrawItemCount(ItemTrackerItem item, bool hideMax) { void DrawEquip(ItemTrackerItem item) { assert(item.kind == ITEM_KIND_ITEM); - bool hasEquip = HasEquipment(item); + bool hasEquip = HasEquipment(item) && IsValidSaveFile(); + bool giantsKnife = item.id == ITEM_SWORD_BGS && hasEquip && !gSaveContext.bgsFlag; + std::string iconName = giantsKnife ? "ITEM_SWORD_KNIFE" : hasEquip ? item.iconName : item.fadedIconName; float iconSize = static_cast(CVarGetInteger(CVAR_TRACKER_ITEM("IconSize"), 36)); ImGui::Image(std::dynamic_pointer_cast(Ship::Context::GetRawInstance()->GetWindow()->GetGui()) - ->GetTextureByName(hasEquip && IsValidSaveFile() ? item.iconName : item.fadedIconName), + ->GetTextureByName(iconName), ImVec2(iconSize, iconSize), ImVec2(0.0f, 0.0f), ImVec2(1, 1)); - Tooltip(SohUtils::GetItemName(item.id).c_str()); + Tooltip(giantsKnife ? "Giant's Knife" : SohUtils::GetItemName(item.id).c_str()); } void DrawQuest(ItemTrackerItem item) { diff --git a/soh/soh/Enhancements/randomizer/settings.cpp b/soh/soh/Enhancements/randomizer/settings.cpp index 3c96c31049..5fa3cbbf9e 100644 --- a/soh/soh/Enhancements/randomizer/settings.cpp +++ b/soh/soh/Enhancements/randomizer/settings.cpp @@ -423,6 +423,7 @@ void Settings::CreateOptions() { }); OPT_U8(RSK_BOMBCHU_BAG, "Bombchu Bag", {"None", "Single Bag", "Progressive Bags"}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("BombchuBag"), mOptionDescriptions[RSK_BOMBCHU_BAG], WIDGET_CVAR_COMBOBOX, RO_BOMBCHU_BAG_NONE); OPT_U8(RSK_ENABLE_BOMBCHU_DROPS, "Bombchu Drops", {"No", "Yes"}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("EnableBombchuDrops"), mOptionDescriptions[RSK_ENABLE_BOMBCHU_DROPS], WIDGET_CVAR_COMBOBOX, RO_AMMO_DROPS_ON); + OPT_BOOL(RSK_PROGRESSIVE_GORON_SWORD, "Progressive Goron Sword", CVAR_RANDOMIZER_SETTING("ProgressiveGoronSword"), mOptionDescriptions[RSK_PROGRESSIVE_GORON_SWORD]); // TODO: AmmoDrops and/or HeartDropRefill, combine with/separate Ammo Drops from Bombchu Drops? // Triforce Hunt: the total piece count is the on/off control. Zero disables the hunt entirely; any // positive value adds that many Triforce Pieces to the pool and unlocks the pieces-location option. @@ -2116,6 +2117,7 @@ void Settings::CreateOptions() { &mOptions[RSK_ROCS_FEATHER], &mOptions[RSK_BOMBCHU_BAG], &mOptions[RSK_ENABLE_BOMBCHU_DROPS], + &mOptions[RSK_PROGRESSIVE_GORON_SWORD], &mOptions[RSK_INFINITE_UPGRADES], &mOptions[RSK_SKELETON_KEY], }, @@ -2305,6 +2307,7 @@ void Settings::CreateOptions() { &mOptions[RSK_SHUFFLE_100_GS_REWARD], &mOptions[RSK_SHUFFLE_BEAN_SOULS], &mOptions[RSK_ROCS_FEATHER], + &mOptions[RSK_PROGRESSIVE_GORON_SWORD], &mOptions[RSK_SHUFFLE_BOSS_SOULS], &mOptions[RSK_SHUFFLE_DEKU_STICK_BAG], &mOptions[RSK_SHUFFLE_DEKU_NUT_BAG],