mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-21 05:34:10 -04:00
update generator with hint stuff
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
#include "hints.hpp"
|
||||
|
||||
#include "../utility/text.hpp"
|
||||
#include "search.hpp"
|
||||
#include "world.hpp"
|
||||
#include "../utility/platform.hpp"
|
||||
#include "../utility/string.hpp"
|
||||
#include "../utility/text.hpp"
|
||||
#include "../utility/random.hpp"
|
||||
#include "../randomizer.hpp"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
@@ -16,15 +21,854 @@ namespace randomizer::logic::hints {
|
||||
{"Temple of Time", "<dark green>"},
|
||||
{"City in the Sky", "<yellow>"},
|
||||
{"Palace of Twilight", "<purple>"},
|
||||
// {"Hyrule Castle", "<silver>"}
|
||||
{"Hyrule Castle", "<silver>"}
|
||||
};
|
||||
|
||||
static const std::map<std::string, std::string> bossColors = {
|
||||
{"Diababa", "<green>"},
|
||||
{"Fyrus", "<red>"},
|
||||
{"Morpheel", "<blue>"},
|
||||
{"Stallord", "<orange>"},
|
||||
{"Blizzeta", "<light blue>"},
|
||||
{"Armogohma", "<dark green>"},
|
||||
{"Argorok", "<yellow>"},
|
||||
{"Zant", "<purple>"},
|
||||
{"Ganondorf", "<silver>"}
|
||||
};
|
||||
|
||||
static location::Location* GetHintableLocation(const location::LocationPool& locations) {
|
||||
for (const auto location : locations) {
|
||||
if (!location->IsHinted()) {
|
||||
return location;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void CalculatePossiblePathLocations(world::WorldPool& worlds) {
|
||||
LOG_TO_DEBUG("Calculating Possible Path Locations");
|
||||
// First generate the goal location keys and also remove items from non-
|
||||
// progress locations since they shouldn't be considered when determining
|
||||
// path locations
|
||||
std::unordered_map<location::Location*, item::Item*> nonRequiredLocations{};
|
||||
for (const auto& world : worlds) {
|
||||
// Defeat Ganondorf is always a goal location
|
||||
world->AddGoalLocation(world->GetLocation("Defeat Ganondorf"));
|
||||
// Required dungeons also have goal locations
|
||||
for (const auto& dungeon : world->GetDungeonTable() | std::views::values) {
|
||||
if (dungeon->IsRequired()) {
|
||||
world->AddGoalLocation(dungeon->GetGoalLocation());
|
||||
}
|
||||
}
|
||||
|
||||
// Collect items at non-progress locations to give back later
|
||||
for (const auto& location: world->GetAllLocations()) {
|
||||
if (!location->IsProgression()) {
|
||||
nonRequiredLocations[location] = location->GetCurrentItem();
|
||||
location->RemoveCurrentItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine path locations for every progression location with a major item by going through
|
||||
// and seeing if taking away the item at each location can still access the other locations
|
||||
for (const auto& world : worlds) {
|
||||
for (const auto& potentialPathLocation : world->GetAllLocations()) {
|
||||
auto itemAtLocation = potentialPathLocation->GetCurrentItem();
|
||||
|
||||
// Skip over locations with removed items
|
||||
if (itemAtLocation == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this is not a progression location with a major item, skip over it
|
||||
if (!potentialPathLocation->IsProgression() || !itemAtLocation->IsMajor()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Take the item away from the location
|
||||
potentialPathLocation->RemoveCurrentItem();
|
||||
|
||||
// Run a search without the item
|
||||
auto search = search::Search::Accessible(&worlds);
|
||||
search.SearchWorlds();
|
||||
|
||||
// Check to see if we can reach each progression location. For each progression location that we can't reach,
|
||||
// add the potential path location as a path location
|
||||
for (auto& location : world->GetAllLocations()) {
|
||||
if (!search._visitedLocations.contains(location)) {
|
||||
location->AddPathLocation(potentialPathLocation);
|
||||
}
|
||||
}
|
||||
|
||||
// Then give back the location's item
|
||||
potentialPathLocation->SetCurrentItem(itemAtLocation);
|
||||
}
|
||||
}
|
||||
|
||||
// Give back non-progress items
|
||||
for (auto& [location, item] : nonRequiredLocations) {
|
||||
location->SetCurrentItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculates the passed in location's hint importance, as well as recursively calculates the importance of any
|
||||
// locations that this one may unlock, but haven't had their own importance calculated yet.
|
||||
// By the time we call this function, we're only trying to determine if a location is
|
||||
// possibly required or not required.
|
||||
//
|
||||
// This function returns any location that blocks the passed in location from being not required
|
||||
static location::Location* CalculateLocationHintImportance(
|
||||
location::Location* location,
|
||||
std::unordered_set<location::Location*>& currentlyChecking)
|
||||
{
|
||||
// If we already know this location's importance, we don't need to calculate it
|
||||
if (location->GetImportance() != location::Importance::UNKNOWN) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Get all the progressive chain locations for this location's item.
|
||||
// Filter out locations that aren't progression, are already in this
|
||||
// location's path locations, have an item that we can trivially know
|
||||
// is barren, or have already been deemed as not required.
|
||||
std::unordered_set<location::Location*> chainLocations{};
|
||||
std::unordered_set pathLocations(
|
||||
location->GetPathLocations().begin(),
|
||||
location->GetPathLocations().end()
|
||||
);
|
||||
for (const auto& loc : location->GetCurrentItem()->GetChainLocations()) {
|
||||
if (loc->IsProgression() &&
|
||||
loc->GetCurrentItem()->IsMajor() &&
|
||||
!pathLocations.contains(loc) &&
|
||||
!loc->GetCurrentItem()->CanBeInBarrenRegion() &&
|
||||
loc->GetImportance() != location::Importance::NOT_REQUIRED)
|
||||
{
|
||||
chainLocations.insert(loc);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the current location, as the item at this location obviously can't lead to itself
|
||||
chainLocations.erase(location);
|
||||
|
||||
// If there are no chain locations left, then this item is not required
|
||||
if (chainLocations.empty()) {
|
||||
location->SetImportance(location::Importance::NOT_REQUIRED);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Get all items from this location's logically required path locations
|
||||
item_pool::ItemPool logicallyRequiredItems{};
|
||||
for (auto pathLocation : pathLocations) {
|
||||
logicallyRequiredItems.push_back(pathLocation->GetCurrentItem());
|
||||
}
|
||||
|
||||
// Perform an importance search to see which locations are reachable without using this item in any way
|
||||
auto worlds = &location->GetWorld()->GetRandomizer()->GetWorlds();
|
||||
auto importanceSearch = search::Search::LocationImportance(worlds, location, logicallyRequiredItems);
|
||||
importanceSearch.SearchWorlds();
|
||||
|
||||
// Any locations that we found can be subtracted out of our chain locations because the item at this location
|
||||
// does not help reach them in any way.
|
||||
std::erase_if(chainLocations, [&](location::Location* loc) {
|
||||
return importanceSearch._visitedLocations.contains(loc);
|
||||
});
|
||||
|
||||
// If any remaining chain locations are also currently being checked for their hint importance,
|
||||
// then we subtract them out. This will allow us to only check locations which aren't potentially
|
||||
// dependent on each other's items. If we go through all the other locations first and don't
|
||||
// find anything that makes this location possibly required, we'd only be left with locations
|
||||
// currently being checked that are dependent on one another. If these location's items are only
|
||||
// potentially dependent on each other, then they're both not required.
|
||||
std::erase_if(chainLocations, [&](location::Location* loc) {
|
||||
return currentlyChecking.contains(loc);
|
||||
});
|
||||
|
||||
// Check to see if any remaining chain locations are at least possible required
|
||||
for (auto chainLocation : chainLocations) {
|
||||
// If any remaining chain location hasn't had its hint importance calculated, then do so now
|
||||
if (chainLocation->GetImportance() == location::Importance::UNKNOWN) {
|
||||
currentlyChecking.insert(chainLocation);
|
||||
CalculateLocationHintImportance(chainLocation, currentlyChecking);
|
||||
currentlyChecking.erase(chainLocation);
|
||||
}
|
||||
// If any remaining chain location is at least possibly required, then this location is also possibly required
|
||||
if (chainLocation->GetImportance() > location::Importance::NOT_REQUIRED) {
|
||||
location->SetImportance(location::Importance::POSSIBLY_REQUIRED);
|
||||
return chainLocation;
|
||||
}
|
||||
}
|
||||
|
||||
// If all remaining chain locations were not required, then this location is also not required
|
||||
location->SetImportance(location::Importance::NOT_REQUIRED);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void CalculatePossibleBarrenRegions(const world::WorldPool& worlds) {
|
||||
LOG_TO_DEBUG("Calculating Barren Regions");
|
||||
for (const auto& world : worlds) {
|
||||
auto defeatGanondorf = world->GetLocation("Defeat Ganondorf");
|
||||
std::unordered_set ganondorfPathLocations(
|
||||
defeatGanondorf->GetPathLocations().begin(),
|
||||
defeatGanondorf->GetPathLocations().end()
|
||||
);
|
||||
|
||||
for (auto& location: world->GetAllLocations()) {
|
||||
// If this location is progression, then add its hint regions to
|
||||
// the set of potentially barren regions
|
||||
if (location->IsProgression()) {
|
||||
for (const auto& locAccess : location->GetAccessList()) {
|
||||
for (const auto& hintRegion : locAccess->GetArea()->GetHintRegions()) {
|
||||
world->GetBarrenRegions()[hintRegion] = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set importance for locations that we can easily calculate as required or not
|
||||
// required right now.
|
||||
|
||||
// Locations which contain barren items, or whose chain locations
|
||||
// all contain barren items are not required
|
||||
if (!location->GetCurrentItem()->IsMajor() || std::ranges::none_of(
|
||||
location->GetCurrentItem()->GetChainLocations(), [](location::Location* loc) {
|
||||
return loc->GetCurrentItem()->IsMajor();
|
||||
}
|
||||
)) {
|
||||
location->SetImportance(location::Importance::NOT_REQUIRED);
|
||||
// Locations which are on the path to Ganondorf are always required
|
||||
} else if (ganondorfPathLocations.contains(location) || location == defeatGanondorf) {
|
||||
location->SetImportance(location::Importance::REQUIRED);
|
||||
}
|
||||
}
|
||||
|
||||
// Any location which appears in the playthrough, but doesn't have an importance set
|
||||
// yet, is going to be possibly required
|
||||
const auto& playthrough = world->GetRandomizer()->GetPlaythroughSpheres();
|
||||
for (const auto& sphere : playthrough) {
|
||||
for (auto loc : sphere) {
|
||||
if (loc->GetImportance() == location::Importance::UNKNOWN) {
|
||||
loc->SetImportance(location::Importance::POSSIBLY_REQUIRED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining locations which don't have a hint importance set yet are either possibly required or not required.
|
||||
// These locations' hint importance take much longer to calculate if we have complex hint importance on
|
||||
for (const auto& location : world->GetAllLocations()) {
|
||||
if (location->GetImportance() == location::Importance::UNKNOWN) {
|
||||
std::unordered_set<location::Location*> currentlyChecking{};
|
||||
auto blockLocation = CalculateLocationHintImportance(location, currentlyChecking);
|
||||
if (blockLocation) {
|
||||
LOG_TO_DEBUG(location->GetName() + ": " + location->GetCurrentItem()->GetName() +
|
||||
" possibly required due to " + blockLocation->GetName() + ": " + blockLocation->GetCurrentItem()->GetName());
|
||||
} else {
|
||||
LOG_TO_DEBUG(location->GetName() + ": " + location->GetCurrentItem()->GetName() + " not required.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now loop through all the progression locations again and remove
|
||||
// any regions from the barren regions which have any possibly required or
|
||||
// required items (unless they can be in a barren region). Otherwise, add
|
||||
// the location to the list of locations in the barren region
|
||||
for (auto location : world->GetAllLocations()) {
|
||||
if (!location->IsProgression()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto& locAccess : location->GetAccessList()) {
|
||||
for (const auto& hintRegion : locAccess->GetArea()->GetHintRegions()) {
|
||||
if (world->GetBarrenRegions().contains(hintRegion)) {
|
||||
if (location->GetImportance() == location::Importance::NOT_REQUIRED ||
|
||||
location->GetCurrentItem()->CanBeInBarrenRegion())
|
||||
{
|
||||
world->GetBarrenRegions()[hintRegion].push_back(location);
|
||||
} else {
|
||||
world->GetBarrenRegions().erase(hintRegion);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_TO_DEBUG("Barren regions for world " + std::to_string(world->GetID()));
|
||||
for (const auto& region : world->GetBarrenRegions() | std::views::keys) {
|
||||
LOG_TO_DEBUG("- " + region)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chooses the appropriate plurality for the given text
|
||||
static std::string ProcessHintPlurality(const std::string& text, bool plural) {
|
||||
auto pluralStartIndex = text.find('|');
|
||||
auto pluralEndIndex = text.find('|', pluralStartIndex + 1);
|
||||
|
||||
auto pluralSubstr = text.substr(pluralStartIndex + 1, pluralEndIndex - pluralStartIndex - 1);
|
||||
auto pluralSubstrParts = utility::str::Split(pluralSubstr, '/');
|
||||
std::string& chosenPluralityStr = plural ? pluralSubstrParts[1] : pluralSubstrParts[0];
|
||||
|
||||
return text.substr(0, pluralStartIndex) + chosenPluralityStr + text.substr(pluralEndIndex + 1);
|
||||
}
|
||||
|
||||
static Text GeneratePathHintText(location::Location* location, location::Location* goalLocation) {
|
||||
// Collect all the hint regions this location is in
|
||||
std::set<std::string> hintRegionNames{};
|
||||
for (const auto& locAcc : location->GetAccessList()) {
|
||||
for (const auto& region : locAcc->GetArea()->GetHintRegions()) {
|
||||
hintRegionNames.insert(region);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the text object for each region and surround it with the color red
|
||||
std::vector<Text> hintRegionText{};
|
||||
std::ranges::transform(hintRegionNames, std::back_inserter(hintRegionText), [&](const std::string& region) {
|
||||
return addColor(getTextObject(region), Text::RED);
|
||||
});
|
||||
|
||||
// Make the regions into a listing
|
||||
Text hintRegionListing = makeTextListing(hintRegionText);
|
||||
|
||||
// Get the goal name and apply its color
|
||||
const auto& goalName = goalLocation->GetGoalName();
|
||||
const auto& goalColor = bossColors.at(goalName);
|
||||
Text goalNameText = goalColor + getTextObject(goalName) + "<white>";
|
||||
|
||||
// Construct the full text
|
||||
Text fullText = getTextObject("Path Hint");
|
||||
fullText.Replace("<regions>", hintRegionListing);
|
||||
fullText.Replace("<goal name>", goalNameText);
|
||||
|
||||
// Handle plurality if necessary
|
||||
for (auto& langText : fullText.mText) {
|
||||
if (!langText.empty() && utility::str::Contains(langText, '|')) {
|
||||
langText = ProcessHintPlurality(langText, hintRegionNames.size() > 1);
|
||||
}
|
||||
}
|
||||
|
||||
return fullText;
|
||||
}
|
||||
|
||||
static Text GenerateBarrenHintText(const std::string& region) {
|
||||
auto fullText = getTextObject("Barren Hint");
|
||||
auto regionText = addColor(getTextObject(region), Text::PURPLE);
|
||||
fullText.Replace("<region>", regionText);
|
||||
return fullText;
|
||||
}
|
||||
|
||||
static Text GenerateItemHintText(location::Location* location) {
|
||||
// Collect all the hint regions this location is in
|
||||
std::set<std::string> hintRegionNames{};
|
||||
for (const auto& locAcc : location->GetAccessList()) {
|
||||
for (const auto& region : locAcc->GetArea()->GetHintRegions()) {
|
||||
hintRegionNames.insert(region);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the text object for each region and surround it with the color red
|
||||
std::vector<Text> hintRegionText{};
|
||||
std::ranges::transform(hintRegionNames, std::back_inserter(hintRegionText), [&](const std::string& region) {
|
||||
return addColor(getTextObject(region, Text::PRETTY), Text::RED);
|
||||
});
|
||||
|
||||
// Make the regions into a listing
|
||||
Text hintRegionListing = makeTextListing(hintRegionText);
|
||||
// Get item text with color added
|
||||
auto textType = Text::PRETTY;
|
||||
Text itemText = addColor(getTextObject(location->GetCurrentItem()->GetName(), textType), Text::GREEN);
|
||||
|
||||
// TODO: Cryptic Text
|
||||
|
||||
Text fullText = getTextObject("Item Hint");
|
||||
fullText.Replace("<regions>", hintRegionListing);
|
||||
fullText.Replace("<Item Pretty or Cryptic Name>", itemText);
|
||||
|
||||
// Handle plurality if necessary
|
||||
for (auto& langText : fullText.mText) {
|
||||
if (!langText.empty() && utility::str::Contains(langText, '|')) {
|
||||
langText = ProcessHintPlurality(langText, hintRegionNames.size() > 1);
|
||||
}
|
||||
}
|
||||
|
||||
return fullText;
|
||||
}
|
||||
|
||||
static Text GenerateLocationHintText(location::Location* location) {
|
||||
// TODO: Cryptic Text
|
||||
auto textType = Text::PRETTY;
|
||||
const auto& itemText = addColor(getTextObject(location->GetCurrentItem()->GetName(), textType), Text::GREEN);
|
||||
Text fullText = getTextObject("Location Hint");
|
||||
fullText.Replace("<Item Pretty or Cryptic Name>", itemText);
|
||||
fullText.Replace("<Location Name>", addColor(Text{location->GetName()}, Text::RED));
|
||||
|
||||
return fullText;
|
||||
}
|
||||
|
||||
HintGenerator::HintGenerator(world::World* world) : _world(world) {}
|
||||
|
||||
void HintGenerator::GeneratePathHints() {
|
||||
|
||||
// Shuffle each pool of path locations so that their orders are random
|
||||
std::vector<location::Location*> goalLocationsList{};
|
||||
auto defeatGanondorf = this->_world->GetLocation("Defeat Ganondorf");
|
||||
for (auto& goalLocation : this->_world->GetGoalLocations()) {
|
||||
utility::random::ShufflePool(goalLocation->GetPathLocations());
|
||||
// Initially we want to create hints for required dungeon's goal locations
|
||||
// and then add Ganondorf in once we have at least 1 hint for each required dungeon
|
||||
if (goalLocation != defeatGanondorf) {
|
||||
goalLocationsList.push_back(goalLocation);
|
||||
}
|
||||
}
|
||||
|
||||
bool addedGanondorfPathLocation = false;
|
||||
auto numPathHints = this->_world->Setting("Number of Path Hints").GetCurrentOptionAsNumber();
|
||||
for (size_t i = 0; i < numPathHints; ++i) {
|
||||
// Try to get at least one hint for each required dungeon first
|
||||
location::Location* goalLocation{};
|
||||
if (i < goalLocationsList.size()) {
|
||||
goalLocation = goalLocationsList[i];
|
||||
} else {
|
||||
// Once we've pulled from all required dungeons, then add ganondorf
|
||||
// to the list and choose randomly
|
||||
if (i == goalLocationsList.size() && !addedGanondorfPathLocation) {
|
||||
addedGanondorfPathLocation = true;
|
||||
goalLocationsList.push_back(defeatGanondorf);
|
||||
}
|
||||
|
||||
if (goalLocationsList.empty()) {
|
||||
LOG_TO_DEBUG("No more possible path hints")
|
||||
break;
|
||||
}
|
||||
|
||||
goalLocation = utility::random::RandomElement(goalLocationsList);
|
||||
}
|
||||
|
||||
// Collect all valid path locations to hint for this location
|
||||
std::vector<location::Location*> validPathLocations{};
|
||||
for (auto location : goalLocation->GetPathLocations()) {
|
||||
// Don't choose locations which have expected items or that are already hinted
|
||||
if (location->HasExpectedItem() || location->IsHinted()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// This is unlikely, but also don't choose any locations which are logically
|
||||
// necessary for accessing *every* possible hint sign if we're placing path hints
|
||||
// on hint signs.
|
||||
if (this->_world->Setting("Path Hints on Hint Signs") != "Off") {
|
||||
// Get a list of all hint signs where we can place path hints to use later
|
||||
auto possiblePathHintSigns = search::GetPossibleHintSigns(location);
|
||||
// Remove the ones which wouldn't be relevant
|
||||
std::erase_if(possiblePathHintSigns, [](location::Location* loc) {
|
||||
const auto& hintSignPlacement = loc->GetWorld()->Setting("Path Hints on Hint Signs");
|
||||
return (hintSignPlacement == "Overworld" && loc->HasCategories("Dungeon")) ||
|
||||
(hintSignPlacement == "Dungeon" && loc->HasCategories("Overworld"));
|
||||
});
|
||||
|
||||
// If none are left, then don't choose this location
|
||||
if (possiblePathHintSigns.empty()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
validPathLocations.push_back(location);
|
||||
}
|
||||
|
||||
auto hintLocation = GetHintableLocation(validPathLocations);
|
||||
if (hintLocation == nullptr) {
|
||||
LOG_TO_DEBUG("No more path locations to hint for " + goalLocation->GetName())
|
||||
utility::container::Erase(goalLocationsList, goalLocation);
|
||||
--i;
|
||||
continue;
|
||||
}
|
||||
|
||||
hintLocation->SetHinted(true);
|
||||
LOG_TO_DEBUG("Chose " + hintLocation->GetName() + " as path hint for " + goalLocation->GetName());
|
||||
auto hintText = GeneratePathHintText(hintLocation, goalLocation);
|
||||
this->_pathHints.emplace_back(hintText, PathHint{goalLocation, hintLocation});
|
||||
}
|
||||
}
|
||||
|
||||
void HintGenerator::GenerateBarrenHints() {
|
||||
std::vector<std::string> barrenPool = {};
|
||||
std::vector<double> barrenDistributions = {};
|
||||
for (auto& [barrenRegion, barrenLocations] : this->_world->GetBarrenRegions()) {
|
||||
barrenPool.push_back(barrenRegion);
|
||||
// The probability of a region being chosen for a barren hint is the square root
|
||||
// of how many locations are in that region
|
||||
barrenDistributions.push_back(sqrt(barrenLocations.size()));
|
||||
}
|
||||
|
||||
std::discrete_distribution<size_t> barrenDistribution(barrenDistributions.begin(), barrenDistributions.end());
|
||||
auto numBarrenHints = this->_world->Setting("Number of Barren Hints").GetCurrentOptionAsNumber();
|
||||
for (size_t i = 0; i < numBarrenHints; ++i) {
|
||||
if (barrenPool.empty()) {
|
||||
LOG_TO_DEBUG("No more barren regions to hint at.")
|
||||
break;
|
||||
}
|
||||
|
||||
auto regionIndex = barrenDistribution(utility::random::GetGenerator());
|
||||
auto& barrenRegion = barrenPool[regionIndex];
|
||||
// Set all locations in the selected barren region as hinted at
|
||||
// so we don't hint at them again
|
||||
for (auto location : this->_world->GetBarrenRegions()[barrenRegion]) {
|
||||
location->SetHinted(true);
|
||||
}
|
||||
|
||||
auto hintText = GenerateBarrenHintText(barrenRegion);
|
||||
this->_barrenHints.emplace_back(hintText, BarrenHint{barrenRegion});
|
||||
LOG_TO_DEBUG("Chose " + barrenRegion + " as hinted barren region");
|
||||
|
||||
// Erase the hinted region from the pool
|
||||
barrenPool.erase(barrenPool.begin() + regionIndex);
|
||||
barrenDistributions.erase(barrenDistributions.begin() + regionIndex);
|
||||
|
||||
// Reset the distribution
|
||||
barrenDistribution = std::discrete_distribution<size_t>(barrenDistributions.begin(), barrenDistributions.end());
|
||||
}
|
||||
}
|
||||
|
||||
void HintGenerator::GenerateItemHints() {
|
||||
location::LocationPool possibleItemHintLocations{};
|
||||
for (auto location : this->_world->GetAllLocations()) {
|
||||
// If the location is progression...
|
||||
// and has a major item...
|
||||
// and does not have an expected item...
|
||||
// and is not already hinted...
|
||||
// then it can be hinted as an item hint
|
||||
if (location->IsProgression() &&
|
||||
location->GetCurrentItem()->IsMajor() &&
|
||||
!location->HasExpectedItem() &&
|
||||
!location->IsHinted())
|
||||
{
|
||||
possibleItemHintLocations.push_back(location);
|
||||
}
|
||||
}
|
||||
|
||||
// Choose randomly until we've selected the appropriate number of item hints
|
||||
utility::random::ShufflePool(possibleItemHintLocations);
|
||||
auto numItemHints = this->_world->Setting("Number of Item Hints").GetCurrentOptionAsNumber();
|
||||
for (auto i = 0; i < numItemHints; ++i) {
|
||||
if (possibleItemHintLocations.empty()) {
|
||||
LOG_TO_DEBUG("No more possible item hint locations.")
|
||||
break;
|
||||
}
|
||||
auto hintLocation = possibleItemHintLocations.back();
|
||||
possibleItemHintLocations.pop_back();
|
||||
hintLocation->SetHinted(true);
|
||||
auto hintText = GenerateItemHintText(hintLocation);
|
||||
this->_itemHints.emplace_back(hintText, ItemHint{hintLocation});
|
||||
LOG_TO_DEBUG("Chose " + hintLocation->GetName() + ": " + hintLocation->GetCurrentItem()->GetName() + " as item hint location")
|
||||
}
|
||||
}
|
||||
|
||||
void HintGenerator::GenerateRemoteLocationHints() {
|
||||
// Return early if we're not prioritizing remote locations for location hints
|
||||
if (this->_world->Setting("Prioritize Remote Location Hints") == "Off") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Gather all the remote locations that are progression and haven't already been hinted
|
||||
std::vector<location::Location*> remoteLocations{};
|
||||
for (auto location : this->_world->GetAllLocations()) {
|
||||
if (location->IsProgression() &&
|
||||
location->HasCategories("Remote Location") &&
|
||||
!location->IsHinted())
|
||||
{
|
||||
remoteLocations.push_back(location);
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle the pool
|
||||
utility::random::ShufflePool(remoteLocations);
|
||||
|
||||
// Get as many remote locations as we can
|
||||
auto numLocationHints = this->_world->Setting("Number of Location Hints").GetCurrentOptionAsNumber();
|
||||
for (int i = 0; i < numLocationHints; ++i) {
|
||||
if (i >= remoteLocations.size()) {
|
||||
LOG_TO_DEBUG("All remote locations hinted");
|
||||
break;
|
||||
}
|
||||
auto hintLocation = remoteLocations[i];
|
||||
hintLocation->SetHinted(true);
|
||||
auto hintText = GenerateLocationHintText(hintLocation);
|
||||
this->_locationHints.emplace_back(hintText, LocationHint{hintLocation});
|
||||
LOG_TO_DEBUG("Chose " + hintLocation->GetName() + " as remote location hint location")
|
||||
}
|
||||
}
|
||||
|
||||
void HintGenerator::GenerateLocationHints() {
|
||||
// Collect all possible locations to hint
|
||||
std::vector<location::Location*> possibleLocationHintLocations{};
|
||||
for (auto location : this->_world->GetAllLocations()) {
|
||||
if (location->IsProgression() && !location->IsHinted() && !location->HasExpectedItem()) {
|
||||
possibleLocationHintLocations.push_back(location);
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle the pool
|
||||
utility::random::ShufflePool(possibleLocationHintLocations);
|
||||
|
||||
// Get the necessary number of hints we want. If we failed to generate enough hints for any of
|
||||
// the other hint types, compensate by adding the difference to the number of location hints
|
||||
auto numLocationHints = this->_world->Setting("Number of Location Hints").GetCurrentOptionAsNumber();
|
||||
auto expectedNumHints = this->_world->Setting("Number of Path Hints").GetCurrentOptionAsNumber() +
|
||||
this->_world->Setting("Number of Barren Hints").GetCurrentOptionAsNumber() +
|
||||
this->_world->Setting("Number of Item Hints").GetCurrentOptionAsNumber();
|
||||
auto totalNumHints = this->_pathHints.size() + this->_barrenHints.size() +
|
||||
this->_itemHints.size() + this->_locationHints.size();
|
||||
numLocationHints += expectedNumHints - totalNumHints;
|
||||
size_t curNumLocationHints = this->_locationHints.size();
|
||||
|
||||
// Choose location hints until we've filled up the amount we need. Since we previously may
|
||||
// have already chosen some remote location hints, start with the number we already have
|
||||
for (auto i = curNumLocationHints; i < numLocationHints; ++i) {
|
||||
if (possibleLocationHintLocations.empty()) {
|
||||
LOG_TO_DEBUG("No more possible location hint locations.")
|
||||
break;
|
||||
}
|
||||
auto hintLocation = possibleLocationHintLocations.back();
|
||||
possibleLocationHintLocations.pop_back();
|
||||
hintLocation->SetHinted(true);
|
||||
auto hintText = GenerateLocationHintText(hintLocation);
|
||||
this->_locationHints.emplace_back(hintText, LocationHint{hintLocation});
|
||||
LOG_TO_DEBUG("Chose " + hintLocation->GetName() + " as location hint location")
|
||||
}
|
||||
}
|
||||
|
||||
static void AssignHintSignHints(const location::LocationPool& hintSigns, std::vector<Hint> hints, world::World* world) {
|
||||
size_t hintsPerSign = std::ceil(static_cast<double>(hints.size()) / static_cast<double>(hintSigns.size()));
|
||||
auto& worlds = world->GetRandomizer()->GetWorlds();
|
||||
auto& hintSignHints = world->GetHintSignHints();
|
||||
auto hintSignHintsOriginal = hintSignHints;
|
||||
|
||||
// Keep trying to place hints until all have been logically placed at least once
|
||||
bool successfullyPlaceHints = false;
|
||||
int retryCount = 50;
|
||||
while (!successfullyPlaceHints) {
|
||||
--retryCount;
|
||||
if (retryCount < 0) {
|
||||
throw std::runtime_error("Failed to properly place hint sign hints. "
|
||||
"Try using a different seed for generation");
|
||||
}
|
||||
|
||||
// Clear any previous attempt at placing hints
|
||||
successfullyPlaceHints = true;
|
||||
hintSignHints = hintSignHintsOriginal;
|
||||
|
||||
for (auto& hint : hints) {
|
||||
item::Item* itemAtHintLocation{};
|
||||
location::Location* hintLocation{};
|
||||
if (auto pathHint = std::get_if<PathHint>(&hint.data)) {
|
||||
hintLocation = pathHint->hintedLocation;
|
||||
} else if (auto itemHint = std::get_if<ItemHint>(&hint.data)) {
|
||||
hintLocation = itemHint->location;
|
||||
}
|
||||
else if (auto locationHint = std::get_if<LocationHint>(&hint.data)) {
|
||||
hintLocation = locationHint->location;
|
||||
}
|
||||
|
||||
// Remove this item from the world and see which hint signs are available to place hints
|
||||
if (hintLocation != nullptr) {
|
||||
itemAtHintLocation = hintLocation->GetCurrentItem();
|
||||
hintLocation->RemoveCurrentItem();
|
||||
}
|
||||
|
||||
auto search = search::Search::Accessible(&worlds);
|
||||
search.SearchWorlds();
|
||||
location::LocationPool availableHintSigns{};
|
||||
for (const auto& sign : hintSigns) {
|
||||
if (search._visitedLocations.contains(sign) && hintSignHints[sign].size() < hintsPerSign) {
|
||||
availableHintSigns.emplace_back(sign);
|
||||
}
|
||||
}
|
||||
|
||||
if (availableHintSigns.empty()) {
|
||||
LOG_TO_DEBUG("No available hint signs to place hint " + hint.text.mText[Text::ENGLISH]);
|
||||
if (hintLocation != nullptr) {
|
||||
hintLocation->SetCurrentItem(itemAtHintLocation);
|
||||
}
|
||||
successfullyPlaceHints = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Place the hint at the hint sign
|
||||
auto hintSign = utility::random::RandomElement(availableHintSigns);
|
||||
hintSignHints[hintSign].push_back(hint);
|
||||
|
||||
if (hintLocation != nullptr) {
|
||||
hintLocation->SetCurrentItem(itemAtHintLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Once we've placed every hint at least once, duplicate hints
|
||||
// and place them randomly until all hint signs have the
|
||||
// necessary number of hints. Don't check for logic here since
|
||||
// every hint can already be logically accessed at some sign
|
||||
auto duplicateHints = hints;
|
||||
location::LocationPool availableHintSigns{};
|
||||
for (const auto& sign : hintSigns) {
|
||||
if (hintSignHints[sign].size() < hintsPerSign) {
|
||||
availableHintSigns.emplace_back(sign);
|
||||
}
|
||||
}
|
||||
utility::random::ShufflePool(availableHintSigns);
|
||||
while (!availableHintSigns.empty()) {
|
||||
// Reset the pool of duplicate hints if we run out.
|
||||
// This way we'll duplicate every hint at least n times
|
||||
// before potentially duplicating each hint n + 1 times
|
||||
if (duplicateHints.empty()) {
|
||||
duplicateHints = hints;
|
||||
}
|
||||
|
||||
auto hint = duplicateHints.back();
|
||||
duplicateHints.pop_back();
|
||||
location::Location* chosenSign{};
|
||||
for (auto sign : availableHintSigns) {
|
||||
// Don't give the same hint to a single sign multiple times
|
||||
if (!utility::container::ElementInContainer(hintSignHints[sign], hint)) {
|
||||
chosenSign = sign;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (chosenSign == nullptr) {
|
||||
LOG_TO_DEBUG("Could not find any gossip stones to place hint " + hint.text.mText[Text::ENGLISH] + " Trying a different hint.")
|
||||
continue;
|
||||
}
|
||||
|
||||
hintSignHints[chosenSign].push_back(hint);
|
||||
if (hintSignHints[chosenSign].size() >= hintsPerSign) {
|
||||
utility::container::Erase(availableHintSigns, chosenSign);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HintGenerator::DistributeHints() {
|
||||
|
||||
// Pools to distribute hints
|
||||
std::vector<Hint> overworldSignHints{};
|
||||
std::vector<Hint> dungeonSignHints{};
|
||||
std::vector<Hint> anySignHints{};
|
||||
|
||||
static constexpr int MIDNA = 0;
|
||||
static constexpr int HINT_SIGN = 1;
|
||||
|
||||
// Helper function for distributing hints to their appropriate places
|
||||
auto distributeHintTypeToPools = [&](std::vector<Hint>& hints, const std::string& hintType) {
|
||||
std::vector<int> hintAssignments{};
|
||||
if (this->_world->Setting(hintType + " on Midna") != "Off") {
|
||||
hintAssignments.push_back(MIDNA);
|
||||
}
|
||||
if (this->_world->Setting(hintType + " on Hint Signs") != "Off") {
|
||||
hintAssignments.push_back(HINT_SIGN);
|
||||
}
|
||||
|
||||
// If the user didn't choose any place to assign these hints, return early
|
||||
if (hintAssignments.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < hints.size(); ++i) {
|
||||
auto& pathHint = hints[i];
|
||||
auto distributeTo = hintAssignments[i % hintAssignments.size()];
|
||||
if (distributeTo == MIDNA) {
|
||||
this->_world->GetMidnaHints().push_back(pathHint);
|
||||
} else if (distributeTo == HINT_SIGN) {
|
||||
auto& pathHintSigns = this->_world->Setting(hintType + " on Hint Signs");
|
||||
if (pathHintSigns == "Overworld") {
|
||||
overworldSignHints.push_back(pathHint);
|
||||
} else if (pathHintSigns == "Dungeon") {
|
||||
dungeonSignHints.push_back(pathHint);
|
||||
} else if (pathHintSigns == "Any") {
|
||||
anySignHints.push_back(pathHint);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
distributeHintTypeToPools(this->_pathHints, "Path Hints");
|
||||
distributeHintTypeToPools(this->_barrenHints, "Barren Hints");
|
||||
distributeHintTypeToPools(this->_itemHints, "Item Hints");
|
||||
distributeHintTypeToPools(this->_locationHints, "Location Hints");
|
||||
|
||||
// Get our pools of overworld and dungeon signs
|
||||
location::LocationPool overworldSignLocations{};
|
||||
location::LocationPool dungeonSignLocations{};
|
||||
for (auto hintSign : this->_world->GetHintSignLocations()) {
|
||||
if (hintSign->HasCategories("Overworld")) {
|
||||
overworldSignLocations.push_back(hintSign);
|
||||
}
|
||||
if (hintSign->HasCategories("Dungeon")) {
|
||||
dungeonSignLocations.push_back(hintSign);
|
||||
}
|
||||
}
|
||||
|
||||
utility::random::ShufflePool(anySignHints);
|
||||
// Distribute our hints which can be on any sign to either overworld or dungeon. Ideally
|
||||
// we fill up each pool of hints to match the number of signs there are equally. Only do
|
||||
// this if we're already placing some hints on only dungeon, or only overworld hint signs.
|
||||
if (!overworldSignHints.empty() || !dungeonSignHints.empty()) {
|
||||
for (size_t i = 0; i < anySignHints.size(); ++i) {
|
||||
const auto& anySignHint = anySignHints[i];
|
||||
size_t hintsPerOverworldSign = overworldSignHints.size() / overworldSignLocations.size();
|
||||
size_t hintsPerDungeonSign = dungeonSignHints.size() / dungeonSignLocations.size();
|
||||
|
||||
if (hintsPerOverworldSign < hintsPerDungeonSign) {
|
||||
overworldSignHints.push_back(anySignHint);
|
||||
} else if (hintsPerDungeonSign < hintsPerOverworldSign) {
|
||||
dungeonSignHints.push_back(anySignHint);
|
||||
} else {
|
||||
if (dungeonSignHints.empty() || (dungeonSignHints.size() % dungeonSignLocations.size() != 0 && i % 2 != 0)) {
|
||||
dungeonSignHints.push_back(anySignHint);
|
||||
} else {
|
||||
overworldSignHints.push_back(anySignHint);
|
||||
}
|
||||
}
|
||||
}
|
||||
anySignHints.clear();
|
||||
}
|
||||
|
||||
// Now distribute hints to our hint signs
|
||||
// If no hints have to be constrained to either overworld or dungeon signs,
|
||||
// then distribute all hints over all signs
|
||||
if (overworldSignHints.empty() && dungeonSignHints.empty()) {
|
||||
AssignHintSignHints(this->_world->GetHintSignLocations(), anySignHints, this->_world);
|
||||
}
|
||||
if (!overworldSignHints.empty()) {
|
||||
AssignHintSignHints(overworldSignLocations, overworldSignHints, this->_world);
|
||||
}
|
||||
if (!dungeonSignHints.empty()) {
|
||||
AssignHintSignHints(dungeonSignLocations, dungeonSignHints, this->_world);
|
||||
}
|
||||
}
|
||||
|
||||
void HintGenerator::FinalizeHintSignText() {
|
||||
for (const auto& [sign, hints] : this->_world->GetHintSignHints()) {
|
||||
auto key = sign->GetName() + " Text";
|
||||
auto& signText = this->_world->AddNewText(key);
|
||||
|
||||
// Put on all the hints
|
||||
for (const auto& hint : hints) {
|
||||
signText += hint.text;
|
||||
signText.PadToNextBox();
|
||||
}
|
||||
|
||||
// pop off last '\n' so we don't have an extra blank textbox
|
||||
for (auto& text : signText.mText) {
|
||||
text.pop_back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the player which dungeons are required on the sign in front of Link's House
|
||||
static void GenerateRequiredDungeonsHint(world::WorldPool& worlds) {
|
||||
for (const auto& world : worlds) {
|
||||
auto& requiredDungeonText = world->AddNewText("Links House Sign");
|
||||
// Use dungeonColors to loop through in base game dungeon order
|
||||
for (const auto& [dungeonName, color] : dungeonColors) {
|
||||
// Hyrule Castle is implicitly required
|
||||
if (dungeonName == "Hyrule Castle") {
|
||||
continue;
|
||||
}
|
||||
auto dungeon = world->GetDungeon(dungeonName);
|
||||
if (dungeon->IsRequired()) {
|
||||
requiredDungeonText += color + getTextObject(dungeonName) + "\n";
|
||||
@@ -42,7 +886,7 @@ namespace randomizer::logic::hints {
|
||||
const std::list<std::string>& textNames,
|
||||
Text::Color color) {
|
||||
auto itemName = world->GetLocation(locationName)->GetCurrentItem()->GetName();
|
||||
auto itemStandardName = addColor(getTextObject(itemName), color, 1, true);
|
||||
auto itemStandardName = addColor(getTextObject(itemName), color);
|
||||
auto itemPrettyName = addColor(getTextObject(itemName, Text::PRETTY), color);
|
||||
for (const auto& textName : textNames) {
|
||||
auto& text = world->AddNewText(textName);
|
||||
@@ -103,34 +947,102 @@ namespace randomizer::logic::hints {
|
||||
if (numRequiredDungeons > 0) {
|
||||
midnaHintText += getTextObject("Midna Hints Required Dungeons Intro At Least One Dungeon");
|
||||
midnaHintText.Replace("<required dungeon count>", std::to_string(numRequiredDungeons));
|
||||
// Add newlines to begin listing dungeons on the next textbox
|
||||
midnaHintText += "\n\n\n\n";
|
||||
midnaHintText.PadToNextBox();
|
||||
|
||||
// Then loop through again to add the dungeon names.
|
||||
// Use dungeonColors to loop through in base game dungeon order
|
||||
int displayedRequiredDungeons = 0;
|
||||
for (const auto& [dungeonName, color] : dungeonColors) {
|
||||
// Hyrule Castle is implicitly required
|
||||
if (dungeonName == "Hyrule Castle") {
|
||||
continue;
|
||||
}
|
||||
auto dungeon = world->GetDungeon(dungeonName);
|
||||
if (dungeon->IsRequired()) {
|
||||
++displayedRequiredDungeons;
|
||||
midnaHintText += color + getTextObject(dungeonName) + "<white>";
|
||||
// Add newline after every dungeon except the last one
|
||||
if (displayedRequiredDungeons < numRequiredDungeons) {
|
||||
midnaHintText += "\n";
|
||||
}
|
||||
midnaHintText += color + getTextObject(dungeonName) + "<white>\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
midnaHintText += getTextObject("Midna Hints Required Dungeons Intro Zero Dungeons");
|
||||
}
|
||||
midnaHintText.PadToNextBox();
|
||||
|
||||
midnaHintText.BreakLines();
|
||||
// Separate Midna hints based on type
|
||||
std::vector<Hint> midnaPathHints{};
|
||||
std::vector<Hint> midnaBarrenHints{};
|
||||
std::vector<Hint> midnaItemHints{};
|
||||
std::vector<Hint> midnaLocationHints{};
|
||||
for (auto& hint : world->GetMidnaHints()) {
|
||||
if (std::holds_alternative<PathHint>(hint.data)) {
|
||||
midnaPathHints.push_back(hint);
|
||||
} else if (std::holds_alternative<BarrenHint>(hint.data)) {
|
||||
midnaBarrenHints.push_back(hint);
|
||||
} else if (std::holds_alternative<ItemHint>(hint.data)) {
|
||||
midnaItemHints.push_back(hint);
|
||||
} else if (std::holds_alternative<LocationHint>(hint.data)) {
|
||||
midnaLocationHints.push_back(hint);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for adding hint text based on type
|
||||
auto addHintsToMidnaText = [&](const std::vector<Hint>& hints, const std::string& type) {
|
||||
if (!hints.empty()) {
|
||||
midnaHintText += getTextObject("Midna Hints " + type + " Intro");
|
||||
midnaHintText.PadToNextBox();
|
||||
for (auto& [text, data] : hints) {
|
||||
midnaHintText += text;
|
||||
midnaHintText.PadToNextBox();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addHintsToMidnaText(midnaPathHints, "Path Hints");
|
||||
|
||||
// Barren hints are presented slightly differently
|
||||
if (!midnaBarrenHints.empty()) {
|
||||
midnaHintText += getTextObject("Midna Hints Barren Hints Intro");
|
||||
midnaHintText.PadToNextBox();
|
||||
for (auto& [text, data] : midnaBarrenHints) {
|
||||
midnaHintText += addColor(getTextObject(std::get<BarrenHint>(data).region), Text::PURPLE) + "\n";
|
||||
}
|
||||
midnaHintText.PadToNextBox();
|
||||
}
|
||||
|
||||
addHintsToMidnaText(midnaItemHints, "Item Hints");
|
||||
addHintsToMidnaText(midnaLocationHints, "Location Hints");
|
||||
|
||||
// pop off the last new line so we don't get an extra blank textbox
|
||||
for (auto& text : midnaHintText.mText) {
|
||||
text.pop_back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GenerateAllHints(world::WorldPool& worlds) {
|
||||
|
||||
utility::platform::Log("Generating Hints...");
|
||||
|
||||
CalculatePossiblePathLocations(worlds);
|
||||
CalculatePossibleBarrenRegions(worlds);
|
||||
|
||||
for (const auto& world : worlds) {
|
||||
auto hintGenerator = HintGenerator{world.get()};
|
||||
// Select barren hints first, as we don't want hinted barren regions to conflict with
|
||||
// location hints
|
||||
hintGenerator.GenerateBarrenHints();
|
||||
// Select remote location hints next as these are the most specific ones.
|
||||
hintGenerator.GenerateRemoteLocationHints();
|
||||
// Then come path and item hints which hint at specific locations that have good items
|
||||
hintGenerator.GeneratePathHints();
|
||||
hintGenerator.GenerateItemHints();
|
||||
// And finally regular location hints can just be any location
|
||||
hintGenerator.GenerateLocationHints();
|
||||
|
||||
hintGenerator.DistributeHints();
|
||||
hintGenerator.FinalizeHintSignText();
|
||||
}
|
||||
|
||||
GenerateRequiredDungeonsHint(worlds);
|
||||
GenerateItemTextReplacements(worlds);
|
||||
GenerateMidnaHintsText(worlds);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user