From 824ac414387d4610ad4fa5ff302e3ac5074d5ad2 Mon Sep 17 00:00:00 2001 From: SuperDude88 <82904174+SuperDude88@users.noreply.github.com> Date: Mon, 16 Feb 2026 17:39:42 -0500 Subject: [PATCH 1/2] Required Boss Drops - Add an option to make bosses drop required items (similar to how race mode used to work) - Set dungeons with plandomized major items to have a required boss in race mode --- gui/desktop/mainwindow.cpp | 3 +- gui/desktop/mainwindow.hpp | 1 + gui/desktop/mainwindow.ui | 178 +++++++++++++++------------- gui/desktop/option_descriptions.hpp | 4 + gui/wiiu/OptionActions.cpp | 10 ++ gui/wiiu/OptionActions.hpp | 1 + gui/wiiu/Page.cpp | 1 + logic/Fill.cpp | 78 +++++++++++- logic/Hints.cpp | 2 +- logic/LogicTests.cpp | 4 +- logic/Search.cpp | 2 +- logic/World.cpp | 51 +++++++- options.cpp | 5 + options.hpp | 2 + seedgen/config.cpp | 4 + 15 files changed, 254 insertions(+), 92 deletions(-) diff --git a/gui/desktop/mainwindow.cpp b/gui/desktop/mainwindow.cpp index 0d764549..836bfd74 100644 --- a/gui/desktop/mainwindow.cpp +++ b/gui/desktop/mainwindow.cpp @@ -541,6 +541,7 @@ void MainWindow::apply_config_settings() APPLY_COMBOBOX_SETTING(config, ui, dungeon_small_keys); APPLY_COMBOBOX_SETTING(config, ui, dungeon_big_keys); APPLY_COMBOBOX_SETTING(config, ui, dungeon_maps_compasses); + APPLY_CHECKBOX_SETTING(config, ui, required_boss_items); APPLY_SPINBOX_SETTING(config, ui, damage_multiplier, float(2.0f), float(MAXIMUM_DAMAGE_MULTIPLIER)); @@ -861,6 +862,7 @@ void MainWindow::on_remove_swords_stateChanged(int arg1) DEFINE_STATE_CHANGE_FUNCTION(randomize_charts) DEFINE_STATE_CHANGE_FUNCTION(chest_type_matches_contents) +DEFINE_STATE_CHANGE_FUNCTION(required_boss_items) void MainWindow::on_damage_multiplier_valueChanged(int multiplier) { @@ -1378,4 +1380,3 @@ void MainWindow::on_paste_permalink_clicked() auto permalink = QGuiApplication::clipboard()->text(); on_permalink_textEdited(permalink); } - diff --git a/gui/desktop/mainwindow.hpp b/gui/desktop/mainwindow.hpp index 4c58e93d..d7141c92 100644 --- a/gui/desktop/mainwindow.hpp +++ b/gui/desktop/mainwindow.hpp @@ -187,6 +187,7 @@ private slots: void on_chest_type_matches_contents_stateChanged(int arg1); void on_damage_multiplier_valueChanged(int multiplier); void on_num_required_dungeons_currentIndexChanged(int index); + void on_required_boss_items_stateChanged(int arg1); // Convenience Tweaks void on_invert_sea_compass_x_axis_stateChanged(int arg1); diff --git a/gui/desktop/mainwindow.ui b/gui/desktop/mainwindow.ui index de0ac157..2339b29b 100644 --- a/gui/desktop/mainwindow.ui +++ b/gui/desktop/mainwindow.ui @@ -1746,6 +1746,13 @@ + + + Performance + + + + Do Not Generate Spoiler Log @@ -1753,39 +1760,72 @@ + + + Start With Random Item Sliding Item + + + + + + + Classic Mode + + + + Fix RNG - - + + + + + 0 + 0 + + - Start With Random Item Sliding Item + Require Boss Items + + + + + + + Start With Random Item - - + + QLayout::SizeConstraint::SetNoConstraint - - + + + + + 0 + 24 + + - Plandomizer File + Plandomizer - - + + - Browse + Plandomizer File - + @@ -1795,42 +1835,15 @@ - - - - - 0 - 24 - - + + - Plandomizer + Browse - - - - Performance - - - - - - - Classic Mode - - - - - - - Start With Random Item - - - @@ -2108,13 +2121,6 @@ Logic Tweaks - - - - Open DRC - - - @@ -2129,6 +2135,13 @@ + + + + Open DRC + + + @@ -2137,36 +2150,33 @@ In-Game Preferences - - - + + + - + - Gyroscope + UI Display - - - Off - + - Off + On - On + Off - + @@ -2191,17 +2201,17 @@ - - + + - + - First-Person Camera + Camera - + Standard @@ -2209,24 +2219,24 @@ - Reverse Up/Down + Reverse Left/Right - - + + - + - Camera + First-Person Camera - + Standard @@ -2234,32 +2244,35 @@ - Reverse Left/Right + Reverse Up/Down - - + + - + - UI Display + Gyroscope - + + + Off + - On + Off - Off + On @@ -2269,9 +2282,6 @@ - - - @@ -2470,7 +2480,7 @@ color: lightgray ArrowCursor - true + false diff --git a/gui/desktop/option_descriptions.hpp b/gui/desktop/option_descriptions.hpp index f1329c46..3a5f26db 100644 --- a/gui/desktop/option_descriptions.hpp +++ b/gui/desktop/option_descriptions.hpp @@ -123,6 +123,10 @@ static std::unordered_map optionDescriptions = { "num_required_dungeons", "Select the number of randomly-chosen bosses that are required. The door to Puppet Ganon will not unlock until you've defeated all of these bosses. Sectors with required bosses will be marked on the map.", }, + { + "required_boss_items", + "Required dungeon bosses will drop required items (e.g. Triforce Shards). Applies to \"Standard\" and \"Race Mode\" dungeons.", + }, { "randomize_charts", "Randomizes which sector is drawn on each Triforce/Treasure Chart." diff --git a/gui/wiiu/OptionActions.cpp b/gui/wiiu/OptionActions.cpp index f5b052fc..2728dfa8 100644 --- a/gui/wiiu/OptionActions.cpp +++ b/gui/wiiu/OptionActions.cpp @@ -471,6 +471,11 @@ namespace OptionCB { return fromBool(conf.settings.remove_swords); } + std::string toggleBossItems() { + conf.settings.required_boss_items = !conf.settings.required_boss_items; + return fromBool(conf.settings.required_boss_items); + } + std::string toggleTrials() { conf.settings.skip_rematch_bosses = !conf.settings.skip_rematch_bosses; return fromBool(conf.settings.skip_rematch_bosses); @@ -1032,6 +1037,8 @@ std::string getValue(const Option& option) { return fromBool(conf.settings.do_not_generate_spoiler_log); case Option::RemoveSwords: return fromBool(conf.settings.remove_swords); + case Option::RequiredBossItems: + return fromBool(conf.settings.required_boss_items); case Option::SkipRefights: return fromBool(conf.settings.skip_rematch_bosses); case Option::InvertCompass: @@ -1250,6 +1257,8 @@ TriggerCallback getCallback(const Option& option) { return &toggleSpoilerLog; case Option::RemoveSwords: return &toggleSwords; + case Option::RequiredBossItems: + return &toggleBossItems; case Option::SkipRefights: return &toggleTrials; case Option::InvertCompass: @@ -1350,6 +1359,7 @@ std::pair getNameDesc(const Option& option) { {RemoveSwords, {"Remove Swords", "Controls whether swords will be placed throughout the game."}}, {NumRequiredDungeons, {"Number of Required Bosses", "Select the number of randomly-chosen bosses that are required. The door to Puppet Ganon will not unlock until you've defeated all of these bosses. Sectors with required bosses will be marked on the map. Applies to \"Standard\" and \"Race Mode\" dungeons."}}, + {RequiredBossItems, {"Require Boss Items", "Required dungeon bosses will drop required items (e.g. Triforce Shards). Applies to \"Standard\" and \"Race Mode\" dungeons."}}, {RandomCharts, {"Randomize Charts", "Randomizes which sector is drawn on each Triforce/Treasure Chart."}}, {CTMC, {"Chest Type Matches Contents", "Changes the chest type to reflect its contents. A metal chest has a progress item, a key chest has a dungeon key, and a wooden chest has a non-progress item or a consumable.\nKey chests are dark wood chests that use a custom texture based on Big Key Chests. Keys for non-required dungeons in race mode will be in wooden chests."}}, diff --git a/gui/wiiu/OptionActions.hpp b/gui/wiiu/OptionActions.hpp index 42b6f5f9..c77e8f42 100644 --- a/gui/wiiu/OptionActions.hpp +++ b/gui/wiiu/OptionActions.hpp @@ -77,6 +77,7 @@ namespace OptionCB { std::string toggleDungeonWarps(); std::string toggleSpoilerLog(); std::string toggleSwords(); + std::string toggleBossItems(); std::string toggleTrials(); std::string toggleInvertCompass(); std::string cycleNumDungeons(); diff --git a/gui/wiiu/Page.cpp b/gui/wiiu/Page.cpp index 659f9945..82d2d7a3 100644 --- a/gui/wiiu/Page.cpp +++ b/gui/wiiu/Page.cpp @@ -617,6 +617,7 @@ AdvancedPage::AdvancedPage() { buttonColumns[1].emplace_back(std::make_unique(Option::CTMC)); buttonColumns[1].emplace_back(std::make_unique(Option::Plandomizer)); buttonColumns[1].emplace_back(std::make_unique(Option::RandomItemSlideItem)); + buttonColumns[1].emplace_back(std::make_unique(Option::RequiredBossItems)); } void AdvancedPage::open() { diff --git a/logic/Fill.cpp b/logic/Fill.cpp index d3ebb2cb..114018a6 100644 --- a/logic/Fill.cpp +++ b/logic/Fill.cpp @@ -586,6 +586,81 @@ static FillError handleDungeonItems(WorldPool& worlds, ItemPool& itemPool) return FillError::NONE; } +static void generateRaceModeItems(const LocationPool& raceModeLocations, ItemPool& raceModeItems, ItemPool& itemsToChooseFrom, ItemPool& mainItemPool) +{ + shufflePool(itemsToChooseFrom); + while (!itemsToChooseFrom.empty() && raceModeItems.size() < raceModeLocations.size()) + { + raceModeItems.push_back(popRandomElement(itemsToChooseFrom)); + } + // Add back any unused elements + addElementsToPool(mainItemPool, itemsToChooseFrom); +} + +// Place progression items in specific locations at the end of dungeons to require the player +// to beat those dungeons. +static FillError placeRaceModeItems(WorldPool& worlds, ItemPool& itemPool, LocationPool& allLocations) +{ + LocationPool raceModeLocations; + ItemPool raceModeItems; + for (auto& world : worlds) + { + if(world.getSettings().required_boss_items) { + for (auto& [name, dungeon] : world.dungeons) + { + if (dungeon.isRequiredDungeon) + { + auto raceModeLocation = dungeon.raceModeLocation; + // If this location already has an item placed at it, then skip it + if (raceModeLocation->currentItem.getGameItemId() != GameItem::INVALID) + { + continue; + } + raceModeLocations.push_back(raceModeLocation); + } + } + } + } + + // Build up the list of race mode items starting with triforce shards... + auto triforceShards = filterAndEraseFromPool(itemPool, [](const Item& item){return item.isTriforceShard();}); + generateRaceModeItems(raceModeLocations, raceModeItems, triforceShards, itemPool); + + // Then swords... + auto swords = filterAndEraseFromPool(itemPool, [](const Item& item){return item.getGameItemId() == GameItem::ProgressiveSword;}); + generateRaceModeItems(raceModeLocations, raceModeItems, swords, itemPool); + + // Then bows... + auto bows = filterAndEraseFromPool(itemPool, [](const Item& item){return item.getGameItemId() == GameItem::ProgressiveBow;}); + generateRaceModeItems(raceModeLocations, raceModeItems, bows, itemPool); + + // Then the rest of the major items if necessary. + auto majorItems = filterAndEraseFromPool(itemPool, [](const Item& item){return item.isMajorItem();}); + generateRaceModeItems(raceModeLocations, raceModeItems, majorItems, itemPool); + + // logItemPool("raceModeItems", raceModeItems); + + if (raceModeItems.size() < raceModeLocations.size()) + { + Utility::platformLog("WARNING: Not enough major items to place at race mode locations."); + } + + // Then place the items in the race mode locations + FillError err; + FILL_ERROR_CHECK(assumedFill(worlds, raceModeItems, itemPool, raceModeLocations)); + + // Set race mode locations which had items placed at them as having expected items + for (auto raceModeLoc : raceModeLocations) + { + raceModeLoc->hasExpectedItem = true; + } + + // Recalculate major items since new items may now be required depending on + // what items were placed at race mode locations + determineMajorItems(worlds, itemPool, allLocations); + return FillError::NONE; +} + static FillError placeNonProgressLocationPlandomizerItems(WorldPool& worlds, ItemPool& itemPool) { LOG_TO_DEBUG("Placing Non-Progress Plandomizer Items"); @@ -671,9 +746,10 @@ FillError fill(WorldPool& worlds) determineMajorItems(worlds, itemPool, allLocations); FILL_ERROR_CHECK(placeNonProgressLocationPlandomizerItems(worlds, itemPool)); - // Handle dungeon items first if necessary. Generally + // Handle dungeon items and race mode dungeons first if necessary. Generally // we need to place items that go into more restrictive location pools first before // we can place other items. + FILL_ERROR_CHECK(placeRaceModeItems(worlds, itemPool, allLocations)); FILL_ERROR_CHECK(handleDungeonItems(worlds, itemPool)); // Recalculate major items again since new items may now be required depending on diff --git a/logic/Hints.cpp b/logic/Hints.cpp index 0e99ecbe..f46b3152 100644 --- a/logic/Hints.cpp +++ b/logic/Hints.cpp @@ -683,7 +683,7 @@ static HintError assignHoHoHints(World& world, WorldPool& worlds, std::listcurrentItem.isTriforceShard()) + if (location->currentItem.isTriforceShard() && !(world.getSettings().required_boss_items && location->isRaceModeLocation)) { LOG_AND_RETURN_IF_ERR(generateItemHintMessage(location, hints)); } diff --git a/logic/LogicTests.cpp b/logic/LogicTests.cpp index 4273366c..56a1d426 100644 --- a/logic/LogicTests.cpp +++ b/logic/LogicTests.cpp @@ -190,6 +190,7 @@ void runLogicTests(Config& newConfig) TEST(settings1, settings1.hint_importance, "hint importance"); TEST(settings1, settings1.open_drc, "open_drc"); TEST(settings1, settings1.randomize_charts, "randomize charts"); + TEST(settings1, settings1.required_boss_items, "required_boss_items charts"); TEST(settings1, settings1.randomize_starting_island, "random starting island"); TEST(settings1, settings1.randomize_dungeon_entrances, "randomize dungeon entrances"); settings1.randomize_cave_entrances = ShuffleCaveEntrances::Disabled; @@ -221,8 +222,9 @@ void runLogicTests(Config& newConfig) TEST(settings2, dummy, "randomize cave entrances"); TEST(settings2, settings2.randomize_dungeon_entrances, "randomize dungeon entrances"); TEST(settings2, settings2.randomize_starting_island, "randomize starting island"); - TEST(settings2, settings2.open_drc, "open_drc"); + TEST(settings1, settings1.required_boss_items, "required_boss_items charts"); TEST(settings2, settings2.randomize_charts, "randomize charts"); + TEST(settings2, settings2.open_drc, "open_drc"); TEST(settings2, settings2.hint_importance, "hint importance"); TEST(settings2, settings2.clearer_hints, "clearer hints"); TEST(settings2, settings2.use_always_hints, "use always hints"); diff --git a/logic/Search.cpp b/logic/Search.cpp index 195011ca..061d9ae6 100644 --- a/logic/Search.cpp +++ b/logic/Search.cpp @@ -153,7 +153,7 @@ LocationPool search(const SearchMode& searchMode, WorldPool& worlds, ItemPool it std::set accessibleEvents = {}; bool newEventsOrExits = false; // Continuously loop through events and exits until no new events or exits are - // found. Since they can unlock each other, this is necessart for proper sphere calculations + // found. Since they can unlock each other, this is necessary for proper sphere calculations do { newEventsOrExits = false; diff --git a/logic/World.cpp b/logic/World.cpp index cf54eecf..06a1bccd 100644 --- a/logic/World.cpp +++ b/logic/World.cpp @@ -459,7 +459,51 @@ World::WorldLoadingError World::determineRaceModeDungeons(WorldPool& worlds) do { shufflePool(dungeonPool); - int setRaceModeDungeons = 0; + size_t setRaceModeDungeons = 0; + // Loop through all the dungeons and see if any of them have items plandomized + // within them (or within their dependent locations). If they have major items + // plandomized, then select those dungeons as race mode dungeons + if (settings.plandomizer && settings.progression_dungeons == ProgressionDungeons::RaceMode) + { + for (const Dungeon& dungeon : dungeonPool) + { + auto allDungeonLocations = dungeon.locations; + // Add any outside dependent locations from this dungeon's locations + const auto& outsideLocs = dungeon.getOutsideDependentLocations(); + allDungeonLocations.insert(allDungeonLocations.end(), outsideLocs.begin(), outsideLocs.end()); + for (auto dungeonLocation : allDungeonLocations) + { + if (plandomizer.locations.contains(dungeonLocation) && !plandomizer.locations[dungeonLocation].isJunkItem()) + { + if(settings.required_boss_items) { + // However, if the dungeon's naturally assigned race mode location is supposed to have a progress item and + // it is plandomized junk or excluded then that's an error on the user's part. + Location* raceModeLocation = dungeon.raceModeLocation; + bool raceModeLocationIsAcceptable = raceModeLocation->progression && (!plandomizer.locations.contains(raceModeLocation) || !plandomizer.locations[raceModeLocation].isJunkItem()); + if (dungeon.hasNaturalRaceModeLocation && !raceModeLocationIsAcceptable) + { + ErrorLog::getInstance().log("Plandomizer Error: Junk item placed at race mode location in dungeon \"" + dungeon.name + "\" with potentially major item"); + LOG_ERR_AND_RETURN(WorldLoadingError::PLANDOMIZER_ERROR); + } + } + + LOG_TO_DEBUG("Chose race mode dungeon : " + dungeon.name); + dungeons[dungeon.name].isRequiredDungeon = true; + setRaceModeDungeons++; + break; + } + } + } + } + + // If too many are set, return an error + if (setRaceModeDungeons > settings.num_required_dungeons) + { + ErrorLog::getInstance().log("Plandomizer Error: Too many dungeons set with potentially major items"); + ErrorLog::getInstance().log("Set number of race mode dungeons: " + std::to_string(setRaceModeDungeons)); + ErrorLog::getInstance().log("Maximum set race mode dungeons: " + std::to_string(settings.num_required_dungeons)); + LOG_ERR_AND_RETURN(WorldLoadingError::PLANDOMIZER_ERROR); + } // Now check again and fill in any more dungeons that may be necessary // Also set non-race mode dungeons locations as non-progress @@ -470,9 +514,10 @@ World::WorldLoadingError World::determineRaceModeDungeons(WorldPool& worlds) { continue; } - // If this dungeon's race mode location is excluded, then skip it + // If this dungeon's race mode location is (excluded) or (bosses have required items + // and this dungeon has junk placed at its race mode location), then skip it auto raceModeLocation = dungeon.raceModeLocation; - bool raceModeLocationIsAcceptable = raceModeLocation->progression; + bool raceModeLocationIsAcceptable = raceModeLocation->progression && (!settings.required_boss_items || (!plandomizer.locations.contains(raceModeLocation) || !plandomizer.locations[raceModeLocation].isJunkItem())); if (raceModeLocationIsAcceptable && setRaceModeDungeons < settings.num_required_dungeons) { LOG_TO_DEBUG("Chose race mode dungeon : " + dungeon.name); diff --git a/options.cpp b/options.cpp index ad1b0989..b4ac8acf 100644 --- a/options.cpp +++ b/options.cpp @@ -82,6 +82,7 @@ void Settings::resetDefaultSettings() { add_shortcut_warps_between_dungeons = false; do_not_generate_spoiler_log = false; remove_swords = false; + required_boss_items = false; skip_rematch_bosses = true; invert_sea_compass_x_axis = false; num_required_dungeons = 0; @@ -356,6 +357,8 @@ uint8_t Settings::getSetting(const Option& option) const { return jalhalla_required; case Option::MolgeraRequired: return molgera_required; + case Option::RequiredBossItems: + return required_boss_items; default: return 0; } @@ -563,6 +566,8 @@ void Settings::setSetting(const Option& option, const size_t& value) { jalhalla_required = value; return; case Option::MolgeraRequired: molgera_required = value; return; + case Option::RequiredBossItems: + required_boss_items = value; return; default: return; } diff --git a/options.hpp b/options.hpp index b5c3759a..ddc36d09 100644 --- a/options.hpp +++ b/options.hpp @@ -128,6 +128,7 @@ enum struct Option { // Additional Randomization Options RemoveSwords, NumRequiredDungeons, + RequiredBossItems, RandomCharts, CTMC, @@ -298,6 +299,7 @@ class Settings { bool add_shortcut_warps_between_dungeons; bool do_not_generate_spoiler_log; bool remove_swords; + bool required_boss_items; bool skip_rematch_bosses; bool invert_sea_compass_x_axis; uint8_t num_required_dungeons; diff --git a/seedgen/config.cpp b/seedgen/config.cpp index e22e379f..7dab6c91 100644 --- a/seedgen/config.cpp +++ b/seedgen/config.cpp @@ -217,6 +217,7 @@ ConfigError Config::loadFromFile(const fspath& filePath, const fspath& preferenc GET_FIELD(root, "damage_multiplier", settings.damage_multiplier); GET_FIELD(root, "chest_type_matches_contents", settings.chest_type_matches_contents) GET_FIELD(root, "remove_swords", settings.remove_swords) + GET_FIELD(root, "required_boss_items", settings.required_boss_items) GET_FIELD(root, "starting_pohs", settings.starting_pohs) GET_FIELD(root, "starting_hcs", settings.starting_hcs) @@ -551,6 +552,7 @@ YAML::Node Config::settingsToYaml() const { SET_FIELD(root, "damage_multiplier", settings.damage_multiplier) SET_FIELD(root, "chest_type_matches_contents", settings.chest_type_matches_contents) SET_FIELD(root, "remove_swords", settings.remove_swords) + SET_FIELD(root, "required_boss_items", settings.required_boss_items) SET_FIELD(root, "starting_pohs", settings.starting_pohs) SET_FIELD(root, "starting_hcs", settings.starting_hcs) @@ -771,6 +773,7 @@ static const std::vector