mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-02 16:16:20 -04:00
first pass at hint system
This commit is contained in:
@@ -1663,6 +1663,7 @@ set(DUSK_FILES
|
||||
src/dusk/randomizer/generator/logic/flatten/flatten.hpp
|
||||
src/dusk/randomizer/generator/logic/flatten/simplify_algebraic.cpp
|
||||
src/dusk/randomizer/generator/logic/flatten/simplify_algebraic.hpp
|
||||
src/dusk/randomizer/generator/logic/hint_types.hpp
|
||||
src/dusk/randomizer/generator/logic/hints.cpp
|
||||
src/dusk/randomizer/generator/logic/hints.hpp
|
||||
src/dusk/randomizer/generator/logic/item.cpp
|
||||
|
||||
@@ -10,10 +10,6 @@
|
||||
#include "JSystem/JHostIO/JORReflexible.h"
|
||||
#include "helpers/endian.h"
|
||||
|
||||
#if TARGET_PC
|
||||
#include <unordered_map>
|
||||
#endif
|
||||
|
||||
static const int DEFAULT_SELECT_ITEM_INDEX = 0;
|
||||
static const int MAX_SELECT_ITEM = 4;
|
||||
static const int SELECT_ITEM_NUM = 2;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/map_loader_definitions.h"
|
||||
#include "dusk/ui/rando_config.hpp"
|
||||
#include "dusk/randomizer/generator/randomizer.hpp"
|
||||
#include "dusk/randomizer/generator/logic/search.hpp"
|
||||
#include "dusk/randomizer/generator/utility/string.hpp"
|
||||
#include "dusk/randomizer/game/randomizer_context.hpp"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "JSystem/JMessage/control.h"
|
||||
#include "d/d_msg_class.h"
|
||||
#include "d/d_save.h"
|
||||
#include "randomizer_context.hpp"
|
||||
#include "custom_flow_ids.hpp"
|
||||
|
||||
@@ -44,7 +45,7 @@ char* GetFormatedTextOverride(u32 key, std::string& text) {
|
||||
}
|
||||
|
||||
u8 getLanguageForOverride() {
|
||||
u8 language = randomizer::Text::ENGLISH;
|
||||
u8 language = dSv_player_config_c::LANGUAGE_ENGLISH;
|
||||
if (dusk::version::isRegionPal()) {
|
||||
language = dComIfGs_getPalLanguage();
|
||||
}/* else if (dusk::version::isRegionJpn()) {
|
||||
|
||||
@@ -1090,6 +1090,21 @@ void parseObjPatchData(stage_tgsc_data_class& object, const YAML::Node& patchNod
|
||||
}
|
||||
}
|
||||
|
||||
static std::array<u8, 20> CreateAttributeData(const YAML::Node& node, const std::string& name) {
|
||||
auto attributesStr = node.as<std::string>();
|
||||
auto attributesVec = HexToBytes(attributesStr);
|
||||
if (attributesVec.size() != 16) {
|
||||
throw std::runtime_error(fmt::format("Attributes for Text Override {} "
|
||||
"are the wrong length. (Expected: 16, Actual: {}", name, attributesVec.size()));
|
||||
}
|
||||
|
||||
std::array<u8, 20> attributes{};
|
||||
for (size_t i = 0; i < attributesVec.size(); ++i) {
|
||||
attributes[i + 4] = attributesVec[i];
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
RandomizerContext WriteSeedData(randomizer::logic::world::World* world) {
|
||||
RandomizerContext randoData{};
|
||||
|
||||
@@ -1499,6 +1514,92 @@ RandomizerContext WriteSeedData(randomizer::logic::world::World* world) {
|
||||
message->type = 1;
|
||||
message->msg_index = handleCustomMessageID(flowNode["inf index"]);
|
||||
message->next_node_idx = handleCustomFlowID(flowNode["next flow index"]);
|
||||
|
||||
// If a custom message is too long, split it up among additional flow/message nodes
|
||||
if (message->msg_index >= BASE_CUSTOM_MSG_AND_FLOW_ID) {
|
||||
auto textName = flowNode["inf index"].as<std::string>();
|
||||
if (world->GetTextDatabase().contains(textName)) {
|
||||
auto& text = world->GetTextObject(textName);
|
||||
if (text.IsTooLong()) {
|
||||
// Get the attributes for the text at this ID. Pretty inefficient since we
|
||||
// have to loop through every element unfortunately
|
||||
std::optional<std::array<u8, 20>> customAttributes{};
|
||||
auto textOverrides = LOAD_EMBED_YAML(RANDO_DATA_PATH "text/text_overrides.yaml");
|
||||
for (const auto& overrideNode : textOverrides) {
|
||||
const auto& overrideName = overrideNode["Name"].as<std::string>();
|
||||
if (overrideName == textName && overrideNode["Attributes"]) {
|
||||
customAttributes = CreateAttributeData(overrideNode["Attributes"], textName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add each split text as a new custom message entry. The original entry
|
||||
// still exists but has been sliced down to fit properly.
|
||||
auto extraText = text.SplitToFitTextLimits();
|
||||
std::vector<mesg_flow_node> newMsgFlows{};
|
||||
for (size_t i = 0; i < extraText.size(); i++) {
|
||||
// Add this custom text to the world
|
||||
auto extraTextName = textName + std::to_string(i + 1);
|
||||
world->AddNewText(extraTextName) = extraText[i];
|
||||
|
||||
// Kinda silly, but means we don't need to create another function
|
||||
// to handle direct string names.
|
||||
YAML::Node node{};
|
||||
node["name"] = extraTextName;
|
||||
// Create new Flow and Message Ids for the split text object
|
||||
auto newCustomFlowIndex = handleCustomFlowID(node["name"]);
|
||||
auto newCustomMessageIndex = handleCustomMessageID(node["name"]);
|
||||
|
||||
// Create the new flow node. We're storing its own flow index with
|
||||
// itself for now, but we'll shift it back to the previous node later
|
||||
mesg_flow_node newMsgFlow{};
|
||||
newMsgFlow.type = 1;
|
||||
newMsgFlow.msg_index = newCustomMessageIndex;
|
||||
newMsgFlow.next_node_idx = newCustomFlowIndex;
|
||||
|
||||
newMsgFlows.push_back(newMsgFlow);
|
||||
|
||||
//Add the custom text to the rando data
|
||||
u32 key = (CUSTOM_BMG_GROUP << 16) | newCustomMessageIndex;
|
||||
for (auto language : randomizer::supportedLanguages) {
|
||||
std::string newText = extraText[i].mText[language];
|
||||
randomizer::applyMessageCodes(newText);
|
||||
randoData.mTextOverrides[language][key] = newText;
|
||||
}
|
||||
|
||||
// Add custom attribute data as well if it exists
|
||||
if (customAttributes.has_value()) {
|
||||
auto attributes = customAttributes.value();
|
||||
|
||||
// Set the message id in the attribute data
|
||||
attributes[4] = newCustomMessageIndex >> 8;
|
||||
attributes[5] = newCustomMessageIndex & 0xFF;
|
||||
|
||||
randoData.mAttributeOverrides[key] = attributes;
|
||||
}
|
||||
}
|
||||
|
||||
// Shift all the next_node_idx fields back a node and set the original
|
||||
// next node idx as the next node idx for the final of the new flows
|
||||
auto finalNodeIdx = message->next_node_idx;
|
||||
message->next_node_idx = newMsgFlows[0].next_node_idx;
|
||||
for (size_t i = 0; i < newMsgFlows.size(); i++) {
|
||||
auto& curFlow = newMsgFlows[i];
|
||||
auto curFlowIdx = curFlow.next_node_idx;
|
||||
if (i == newMsgFlows.size() - 1) {
|
||||
curFlow.next_node_idx = finalNodeIdx;
|
||||
} else {
|
||||
curFlow.next_node_idx = newMsgFlows[i + 1].next_node_idx;
|
||||
}
|
||||
|
||||
// Also Add the new custom flows to our rando data
|
||||
u32 key = (CUSTOM_BMG_GROUP << 16) | curFlowIdx;
|
||||
randoData.mFlowPatches[key] = std::bit_cast<u64>(curFlow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto index : indices) {
|
||||
u32 key = (groupNo << 16) | index;
|
||||
@@ -1540,17 +1641,7 @@ RandomizerContext WriteSeedData(randomizer::logic::world::World* world) {
|
||||
|
||||
// If we have custom attributes
|
||||
if (overrideNode["Attributes"]) {
|
||||
auto attributesStr = overrideNode["Attributes"].as<std::string>();
|
||||
auto attributesVec = HexToBytes(attributesStr);
|
||||
if (attributesVec.size() != 16) {
|
||||
throw std::runtime_error(fmt::format("Attributes for Text Override {} "
|
||||
"are the wrong length. (Expected: 16, Actual: {}", name, attributesVec.size()));
|
||||
}
|
||||
|
||||
std::array<u8, 20> attributes{};
|
||||
for (size_t i = 0; i < attributesVec.size(); ++i) {
|
||||
attributes[i + 4] = attributesVec[i];
|
||||
}
|
||||
auto attributes = CreateAttributeData(overrideNode["Attributes"], name);
|
||||
|
||||
// Set the message id in the attribute data
|
||||
attributes[4] = messageId >> 8;
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
#include <dolphin/types.h>
|
||||
|
||||
#include <array>
|
||||
#include <list>
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <list>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "dusk/randomizer/generator/randomizer.hpp"
|
||||
|
||||
/*
|
||||
* Class holding all the information necessary for playing
|
||||
|
||||
@@ -1469,6 +1469,7 @@
|
||||
- Npc
|
||||
- Kakariko Village
|
||||
- ARC
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Name Lookup:
|
||||
- Talo Sharpshooting
|
||||
@@ -1492,6 +1493,7 @@
|
||||
- Overworld
|
||||
- Npc
|
||||
- Kakariko Village
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Name Lookup:
|
||||
- Ilia Memory Reward
|
||||
@@ -1883,6 +1885,7 @@
|
||||
- Npc
|
||||
- Hidden Village
|
||||
- ARC
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Name Lookup:
|
||||
- Skybook From Impaz
|
||||
@@ -1894,6 +1897,7 @@
|
||||
- Overworld
|
||||
- Npc
|
||||
- Hidden Village
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Freestanding Item:
|
||||
- Stage: 63
|
||||
@@ -2215,6 +2219,7 @@
|
||||
- Overworld
|
||||
- Hyrule Field - Lanayru Province
|
||||
- ARC
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Chest:
|
||||
- Stage: 30
|
||||
@@ -2478,6 +2483,7 @@
|
||||
- Npc
|
||||
- Castle Town
|
||||
- ARC
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Name Lookup:
|
||||
- Jovani 60 Poe Soul Reward
|
||||
@@ -3556,6 +3562,7 @@
|
||||
- Npc
|
||||
- Lake Hylia
|
||||
- ARC
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Name Lookup:
|
||||
- Plumm Fruit Balloon Minigame
|
||||
@@ -3644,6 +3651,7 @@
|
||||
- Npc
|
||||
- Upper Zoras River
|
||||
- ARC
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Name Lookup:
|
||||
- Iza Helping Hand
|
||||
@@ -3656,6 +3664,7 @@
|
||||
- Npc
|
||||
- Upper Zoras River
|
||||
- ARC
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Name Lookup:
|
||||
- Iza Raging Rapids Minigame
|
||||
@@ -4066,6 +4075,7 @@
|
||||
- Npc
|
||||
- Snowpeak Province
|
||||
- ARC
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Name Lookup:
|
||||
- Snowboard Racing Prize
|
||||
@@ -4712,6 +4722,7 @@
|
||||
- Npc
|
||||
- Cave of Ordeals
|
||||
- ARC
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Name Lookup:
|
||||
- Cave of Ordeals Great Fairy Reward
|
||||
@@ -4937,7 +4948,7 @@
|
||||
- Dungeon Reward
|
||||
- REL
|
||||
- ARC
|
||||
Goal Location: True
|
||||
Goal Name: Diababa
|
||||
Metadata:
|
||||
FLW Message:
|
||||
- Group: 5
|
||||
@@ -5206,7 +5217,7 @@
|
||||
- Dungeon Reward
|
||||
- REL
|
||||
- ARC
|
||||
Goal Location: True
|
||||
Goal Name: Fyrus
|
||||
Metadata:
|
||||
FLW Message:
|
||||
- Group: 5
|
||||
@@ -5531,7 +5542,7 @@
|
||||
- Dungeon Reward
|
||||
- REL
|
||||
- ARC
|
||||
Goal Location: True
|
||||
Goal Name: Morpheel
|
||||
Metadata:
|
||||
FLW Message:
|
||||
- Group: 5
|
||||
@@ -5719,6 +5730,7 @@
|
||||
Categories:
|
||||
- Dungeon
|
||||
- Arbiters Grounds
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Chest:
|
||||
- Stage: 26
|
||||
@@ -5804,7 +5816,7 @@
|
||||
- Dungeon
|
||||
- Arbiters Grounds
|
||||
- Dungeon Reward
|
||||
Goal Location: True
|
||||
Goal Name: Stallord
|
||||
Metadata:
|
||||
Name Lookup:
|
||||
- Arbiters Grounds Dungeon Reward
|
||||
@@ -6094,7 +6106,7 @@
|
||||
- Dungeon Reward
|
||||
- REL
|
||||
- ARC
|
||||
Goal Location: True
|
||||
Goal Name: Blizzeta
|
||||
Metadata:
|
||||
FLW Message:
|
||||
- Group: 5
|
||||
@@ -6329,7 +6341,7 @@
|
||||
- Dungeon Reward
|
||||
- REL
|
||||
- ARC
|
||||
Goal Location: True
|
||||
Goal Name: Armogohma
|
||||
Metadata:
|
||||
FLW Message:
|
||||
- Group: 5
|
||||
@@ -6619,6 +6631,7 @@
|
||||
- Dungeon
|
||||
- City in the Sky
|
||||
- ARC
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Chest:
|
||||
- Stage: 14
|
||||
@@ -6655,7 +6668,7 @@
|
||||
- Dungeon Reward
|
||||
- REL
|
||||
- ARC
|
||||
Goal Location: True
|
||||
Goal Name: Argorok
|
||||
Metadata:
|
||||
FLW Message:
|
||||
- Group: 5
|
||||
@@ -6810,6 +6823,7 @@
|
||||
- Cutscene
|
||||
- Dungeon
|
||||
- Palace of Twilight
|
||||
- Remote Location
|
||||
Metadata:
|
||||
Freestanding Item:
|
||||
- Stage: 15
|
||||
@@ -6866,7 +6880,7 @@
|
||||
- Palace of Twilight
|
||||
- Heart Container
|
||||
- Dungeon Reward
|
||||
Goal Location: True
|
||||
Goal Name: Zant
|
||||
Metadata:
|
||||
Freestanding Item:
|
||||
- Stage: 16
|
||||
@@ -7200,6 +7214,7 @@
|
||||
Original Item: Game Beatable
|
||||
Categories:
|
||||
- Placeholder
|
||||
Goal Name: Ganondorf
|
||||
Metadata:
|
||||
- None
|
||||
|
||||
|
||||
@@ -480,6 +480,97 @@
|
||||
- Master Sword: "The door to the past will require striking at least the Master Sword into the Master Sword pedestal."
|
||||
- Light Sword: "The door to the past will require striking the Light Sword into the Master Sword pedestal."
|
||||
|
||||
#############################
|
||||
## Hints ##
|
||||
#############################
|
||||
|
||||
- Name: Number of Path Hints
|
||||
Default Option: 3
|
||||
Options:
|
||||
- 0-7: "Select the number of path hints you would like to generate."
|
||||
|
||||
- Name: Path Hints on Midna
|
||||
Default Option: "Off"
|
||||
Options:
|
||||
- "Off": "Path Hints will not be part of Midna's hint text."
|
||||
- "On": "Path hints will be part of Midna's hint text. If path hints are also placed on hint signs, then the hints will be split evenly between the two options."
|
||||
|
||||
- Name: Path Hints on Hint Signs
|
||||
Default Option: "Off"
|
||||
Options:
|
||||
- "Off": "Path Hints will not be placed on hint signs."
|
||||
- "Overworld": "Path hints will be placed only on overworld hint signs. If path hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
|
||||
- "Dungeon": "Path hints will be placed only on dungeon hint signs. If path hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
|
||||
- "Any": "Path hints can be placed at any hint sign. If path hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
|
||||
|
||||
- Name: Number of Barren Hints
|
||||
Default Option: 3
|
||||
Options:
|
||||
- 0-7: "Select the number of barren hints you would like to generate."
|
||||
|
||||
- Name: Barren Hints on Midna
|
||||
Default Option: "On"
|
||||
Options:
|
||||
- "Off": "Barren Hints will not be part of Midna's hint text."
|
||||
- "On": "Barren hints will be part of Midna's hint text. If barren hints are also placed on hint signs, then the hints will be split evenly between the two options."
|
||||
#- "On + Region Hint Signs": "Barren hints will be part of Midna's hint text. The hint signs within Midna's hinted barren regions will also declare the region as barren. If barren hints are also placed on hint signs, then the hints will be split evenly between the two options."
|
||||
|
||||
- Name: Barren Hints on Hint Signs
|
||||
Default Option: "Off"
|
||||
Options:
|
||||
- "Off": "Barren Hints will not be placed on hint signs."
|
||||
- "Overworld": "Barren hints will be placed only on overworld hint signs. If barren hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
|
||||
- "Dungeon": "Barren hints will be placed only on dungeon hint signs. If barren hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
|
||||
- "Any": "Barren hints can be placed at any hint sign. If barren hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
|
||||
|
||||
- Name: Number of Item Hints
|
||||
Default Option: 1
|
||||
Options:
|
||||
- 0-7: "Select the number of item hints you would like to generate."
|
||||
|
||||
- Name: Item Hints on Midna
|
||||
Default Option: "Off"
|
||||
Options:
|
||||
- "Off": "Item Hints will not be part of Midna's hint text."
|
||||
- "On": "Item hints will be part of Midna's hint text. If item hints are also placed on hint signs, then the hints will be split evenly between the two options."
|
||||
|
||||
- Name: Item Hints on Hint Signs
|
||||
Default Option: "Overworld"
|
||||
Options:
|
||||
- "Off": "Item Hints will not be placed on hint signs."
|
||||
- "Overworld": "Item hints will be placed only on overworld hint signs. If item hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
|
||||
- "Dungeon": "Item hints will be placed only on dungeon hint signs. If item hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
|
||||
- "Any": "Item hints can be placed at any hint sign. If item hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
|
||||
|
||||
- Name: Number of Location Hints
|
||||
Default Option: 5
|
||||
Options:
|
||||
- 0-7: "Select the number of location hints you would like to generate."
|
||||
|
||||
- Name: Location Hints on Midna
|
||||
Default Option: "Off"
|
||||
Options:
|
||||
- "Off": "Location Hints will not be part of Midna's hint text."
|
||||
- "On": "Location hints will be part of Midna's hint text. If location hints are also placed on hint signs, then the hints will be split evenly between the two options."
|
||||
|
||||
- Name: Location Hints on Hint Signs
|
||||
Default Option: "Overworld"
|
||||
Options:
|
||||
- "Off": "Location Hints will not be placed on hint signs."
|
||||
- "Overworld": "Location hints will be placed only on overworld hint signs. If location hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
|
||||
- "Dungeon": "Location hints will be placed only on dungeon hint signs. If location hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
|
||||
- "Any": "Location hints can be placed at any hint sign. If location hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
|
||||
|
||||
- Name: Prioritize Remote Location Hints
|
||||
Default Option: "On"
|
||||
Options:
|
||||
- "Off": "All locations will be treated equally for location hints."
|
||||
- "On": "Certain locations which are time-consuming to check will be given priority for location hints."
|
||||
|
||||
#############################
|
||||
## Entrance Randomizer ##
|
||||
#############################
|
||||
|
||||
- Name: Randomize Starting Spawn
|
||||
Default Option: "Off"
|
||||
Options:
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
Number of Path Hints: 4
|
||||
Path Hints on Midna: Off
|
||||
Path Hints on Hint Signs: Dungeon
|
||||
Number of Barren Hints: 4
|
||||
Barren Hints on Midna: Off
|
||||
Barren Hints on Hint Signs: Dungeon
|
||||
Number of Item Hints: 4
|
||||
Item Hints on Midna: Off
|
||||
Item Hints on Hint Signs: Dungeon
|
||||
Number of Location Hints: 4
|
||||
Location Hints on Midna: Off
|
||||
Location Hints on Hint Signs: Dungeon
|
||||
Prioritize Remote Location Hints: On
|
||||
@@ -0,0 +1,14 @@
|
||||
Seed: TESTTESTTEST
|
||||
Number of Path Hints: 0
|
||||
Path Hints on Midna: Off
|
||||
Path Hints on Hint Signs: Off
|
||||
Number of Barren Hints: 0
|
||||
Barren Hints on Midna: Off
|
||||
Barren Hints on Hint Signs: Off
|
||||
Number of Item Hints: 0
|
||||
Item Hints on Midna: Off
|
||||
Item Hints on Hint Signs: Off
|
||||
Number of Location Hints: 0
|
||||
Location Hints on Midna: Off
|
||||
Location Hints on Hint Signs: Off
|
||||
Prioritize Remote Location Hints: Off
|
||||
@@ -0,0 +1,14 @@
|
||||
Seed: TESTTESTTEST
|
||||
Number of Path Hints: 7
|
||||
Path Hints on Midna: On
|
||||
Path Hints on Hint Signs: Any
|
||||
Number of Barren Hints: 7
|
||||
Barren Hints on Midna: On
|
||||
Barren Hints on Hint Signs: Any
|
||||
Number of Item Hints: 7
|
||||
Item Hints on Midna: On
|
||||
Item Hints on Hint Signs: Any
|
||||
Number of Location Hints: 7
|
||||
Location Hints on Midna: On
|
||||
Location Hints on Hint Signs: Any
|
||||
Prioritize Remote Location Hints: On
|
||||
@@ -0,0 +1,13 @@
|
||||
Number of Path Hints: 4
|
||||
Path Hints on Midna: Off
|
||||
Path Hints on Hint Signs: Overworld
|
||||
Number of Barren Hints: 4
|
||||
Barren Hints on Midna: Off
|
||||
Barren Hints on Hint Signs: Overworld
|
||||
Number of Item Hints: 4
|
||||
Item Hints on Midna: Off
|
||||
Item Hints on Hint Signs: Overworld
|
||||
Number of Location Hints: 4
|
||||
Location Hints on Midna: Off
|
||||
Location Hints on Hint Signs: Overworld
|
||||
Prioritize Remote Location Hints: On
|
||||
@@ -1510,11 +1510,11 @@ North Gerudo Desert:
|
||||
Cryptic:
|
||||
Text: the {northern desert}
|
||||
|
||||
Bublin Camp:
|
||||
Bulblin Camp:
|
||||
Standard:
|
||||
Text: Bublin Camp
|
||||
Text: Bulblin Camp
|
||||
Pretty:
|
||||
Text: "{Bublin Camp}"
|
||||
Text: "{Bulblin Camp}"
|
||||
Cryptic:
|
||||
Text: a {camp of enemies}
|
||||
|
||||
@@ -1526,6 +1526,14 @@ Mirror Chamber:
|
||||
Cryptic:
|
||||
Text: a {chamber of chains}
|
||||
|
||||
Snowpeak Mountain:
|
||||
Standard:
|
||||
Text: Snowpeak Mountain
|
||||
Pretty:
|
||||
Text: "{Snowpeak Mountain}"
|
||||
Cryptic:
|
||||
Text: a {snowy mountain}
|
||||
|
||||
Forest Temple:
|
||||
Standard:
|
||||
Text: Forest Temple
|
||||
@@ -1538,7 +1546,7 @@ Goron Mines:
|
||||
Standard:
|
||||
Text: Goron Mines
|
||||
Pretty:
|
||||
Text: the {Goron Mines}
|
||||
Text: "{Goron Mines}"
|
||||
Cryptic:
|
||||
Text: a {volcanic mine}
|
||||
|
||||
@@ -1546,7 +1554,7 @@ Lakebed Temple:
|
||||
Standard:
|
||||
Text: Lakebed Temple
|
||||
Pretty:
|
||||
Text: the {Lakebed Temple}
|
||||
Text: "{Lakebed Temple}"
|
||||
Cryptic:
|
||||
Text: an {underground lake}
|
||||
|
||||
@@ -1562,7 +1570,7 @@ Snowpeak Ruins:
|
||||
Standard:
|
||||
Text: Snowpeak Ruins
|
||||
Pretty:
|
||||
Text: the {Snowpeak Ruins}
|
||||
Text: "{Snowpeak Ruins}"
|
||||
Cryptic:
|
||||
Text: a {snowy mansion}
|
||||
|
||||
@@ -1578,7 +1586,7 @@ City in the Sky:
|
||||
Standard:
|
||||
Text: City in the Sky
|
||||
Pretty:
|
||||
Text: the {City in the Sky}
|
||||
Text: "{City in the Sky}"
|
||||
Cryptic:
|
||||
Text: the {skies above}
|
||||
|
||||
@@ -1594,10 +1602,64 @@ Hyrule Castle:
|
||||
Standard:
|
||||
Text: Hyrule Castle
|
||||
Pretty:
|
||||
Text: the {Hyrule Castle}
|
||||
Text: "{Hyrule Castle}"
|
||||
Cryptic:
|
||||
Text: the {kingdom's castle}
|
||||
|
||||
# BOSS/GOAL NAMES
|
||||
Diababa:
|
||||
Standard:
|
||||
Text: Diababa
|
||||
|
||||
Fyrus:
|
||||
Standard:
|
||||
Text: Fyrus
|
||||
|
||||
Morpheel:
|
||||
Standard:
|
||||
Text: Morpheel
|
||||
|
||||
Stallord:
|
||||
Standard:
|
||||
Text: Stallord
|
||||
|
||||
Blizzeta:
|
||||
Standard:
|
||||
Text: Blizzeta
|
||||
|
||||
Armogohma:
|
||||
Standard:
|
||||
Text: Armogohma
|
||||
|
||||
Argorok:
|
||||
Standard:
|
||||
Text: Argorok
|
||||
|
||||
Zant:
|
||||
Standard:
|
||||
Text: Zant
|
||||
|
||||
Ganondorf:
|
||||
Standard:
|
||||
Text: Ganondorf
|
||||
|
||||
# HINT TEMPLATES
|
||||
Path Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that <regions> |is/are| on the path to <goal name>.
|
||||
|
||||
Barren Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that visiting <region> is a foolish choice.
|
||||
|
||||
Item Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that <Item Pretty or Cryptic Name> can be found at <regions>.
|
||||
|
||||
Location Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that the reward for <Location Name> is <Item Pretty or Cryptic Name>.
|
||||
|
||||
# NO REQUIRED DUNGEON TEXT
|
||||
No Required Dungeons Text:
|
||||
Standard:
|
||||
@@ -2212,6 +2274,26 @@ Midna Hints Required Dungeons Intro At Least One Dungeon:
|
||||
Text: |-
|
||||
There are <red><required dungeon count><white> required dungeons:
|
||||
|
||||
Midna Hints Path Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
The following <red>paths<white> lie before you:
|
||||
|
||||
Midna Hints Barren Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
There is <purple>nothing<white> to be found in these regions:
|
||||
|
||||
Midna Hints Item Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
These <green>items<white> may be of use to you:
|
||||
|
||||
Midna Hints Location Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
These <red>locations<white> are known to me:
|
||||
|
||||
Ordon Hint Sign Text:
|
||||
Standard:
|
||||
Text: |-
|
||||
|
||||
@@ -1510,11 +1510,11 @@ North Gerudo Desert:
|
||||
Cryptic:
|
||||
Text: the {northern desert}
|
||||
|
||||
Bublin Camp:
|
||||
Bulblin Camp:
|
||||
Standard:
|
||||
Text: Bublin Camp
|
||||
Text: Bulblin Camp
|
||||
Pretty:
|
||||
Text: "{Bublin Camp}"
|
||||
Text: "{Bulblin Camp}"
|
||||
Cryptic:
|
||||
Text: a {camp of enemies}
|
||||
|
||||
@@ -1538,7 +1538,7 @@ Goron Mines:
|
||||
Standard:
|
||||
Text: Goron Mines
|
||||
Pretty:
|
||||
Text: the {Goron Mines}
|
||||
Text: "{Goron Mines}"
|
||||
Cryptic:
|
||||
Text: a {volcanic mine}
|
||||
|
||||
@@ -1546,7 +1546,7 @@ Lakebed Temple:
|
||||
Standard:
|
||||
Text: Lakebed Temple
|
||||
Pretty:
|
||||
Text: the {Lakebed Temple}
|
||||
Text: "{Lakebed Temple}"
|
||||
Cryptic:
|
||||
Text: an {underground lake}
|
||||
|
||||
@@ -1562,7 +1562,7 @@ Snowpeak Ruins:
|
||||
Standard:
|
||||
Text: Snowpeak Ruins
|
||||
Pretty:
|
||||
Text: the {Snowpeak Ruins}
|
||||
Text: "{Snowpeak Ruins}"
|
||||
Cryptic:
|
||||
Text: a {snowy mansion}
|
||||
|
||||
@@ -1578,7 +1578,7 @@ City in the Sky:
|
||||
Standard:
|
||||
Text: City in the Sky
|
||||
Pretty:
|
||||
Text: the {City in the Sky}
|
||||
Text: "{City in the Sky}"
|
||||
Cryptic:
|
||||
Text: the {skies above}
|
||||
|
||||
@@ -1594,10 +1594,64 @@ Hyrule Castle:
|
||||
Standard:
|
||||
Text: Hyrule Castle
|
||||
Pretty:
|
||||
Text: the {Hyrule Castle}
|
||||
Text: "{Hyrule Castle}"
|
||||
Cryptic:
|
||||
Text: the {kingdom's castle}
|
||||
|
||||
# BOSS/GOAL NAMES
|
||||
Diababa:
|
||||
Standard:
|
||||
Text: Diababa
|
||||
|
||||
Fyrus:
|
||||
Standard:
|
||||
Text: Fyrus
|
||||
|
||||
Morpheel:
|
||||
Standard:
|
||||
Text: Morpheel
|
||||
|
||||
Stallord:
|
||||
Standard:
|
||||
Text: Stallord
|
||||
|
||||
Blizzeta:
|
||||
Standard:
|
||||
Text: Blizzeta
|
||||
|
||||
Armogohma:
|
||||
Standard:
|
||||
Text: Armogohma
|
||||
|
||||
Argorok:
|
||||
Standard:
|
||||
Text: Argorok
|
||||
|
||||
Zant:
|
||||
Standard:
|
||||
Text: Zant
|
||||
|
||||
Ganondorf:
|
||||
Standard:
|
||||
Text: Ganondorf
|
||||
|
||||
# HINT TEMPLATES
|
||||
Path Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that <regions> |is/are| on the path to <goal name>.
|
||||
|
||||
Barren Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that visiting <region> is a foolish choice.
|
||||
|
||||
Item Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that <Item Pretty or Cryptic Name> can be found at <regions>.
|
||||
|
||||
Location Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that the reward for <Location Name> is <Item Pretty or Cryptic Name>.
|
||||
|
||||
# NO REQUIRED DUNGEON TEXT
|
||||
No Required Dungeons Text:
|
||||
Standard:
|
||||
@@ -2212,6 +2266,26 @@ Midna Hints Required Dungeons Intro At Least One Dungeon:
|
||||
Text: |-
|
||||
There are <red><required dungeon count><white> required dungeons:
|
||||
|
||||
Midna Hints Path Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
The following <red>paths<white> lie before you:
|
||||
|
||||
Midna Hints Barren Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
There is <purple>nothing<white> to be found in these regions:
|
||||
|
||||
Midna Hints Item Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
These <green>items<white> may be of use to you:
|
||||
|
||||
Midna Hints Location Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
These <red>locations<white> are known to me:
|
||||
|
||||
Ordon Hint Sign Text:
|
||||
Standard:
|
||||
Text: |-
|
||||
|
||||
@@ -1510,11 +1510,11 @@ North Gerudo Desert:
|
||||
Cryptic:
|
||||
Text: the {northern desert}
|
||||
|
||||
Bublin Camp:
|
||||
Bulblin Camp:
|
||||
Standard:
|
||||
Text: Bublin Camp
|
||||
Text: Bulblin Camp
|
||||
Pretty:
|
||||
Text: "{Bublin Camp}"
|
||||
Text: "{Bulblin Camp}"
|
||||
Cryptic:
|
||||
Text: a {camp of enemies}
|
||||
|
||||
@@ -1538,7 +1538,7 @@ Goron Mines:
|
||||
Standard:
|
||||
Text: Goron Mines
|
||||
Pretty:
|
||||
Text: the {Goron Mines}
|
||||
Text: "{Goron Mines}"
|
||||
Cryptic:
|
||||
Text: a {volcanic mine}
|
||||
|
||||
@@ -1546,7 +1546,7 @@ Lakebed Temple:
|
||||
Standard:
|
||||
Text: Lakebed Temple
|
||||
Pretty:
|
||||
Text: the {Lakebed Temple}
|
||||
Text: "{Lakebed Temple}"
|
||||
Cryptic:
|
||||
Text: an {underground lake}
|
||||
|
||||
@@ -1562,7 +1562,7 @@ Snowpeak Ruins:
|
||||
Standard:
|
||||
Text: Snowpeak Ruins
|
||||
Pretty:
|
||||
Text: the {Snowpeak Ruins}
|
||||
Text: "{Snowpeak Ruins}"
|
||||
Cryptic:
|
||||
Text: a {snowy mansion}
|
||||
|
||||
@@ -1578,7 +1578,7 @@ City in the Sky:
|
||||
Standard:
|
||||
Text: City in the Sky
|
||||
Pretty:
|
||||
Text: the {City in the Sky}
|
||||
Text: "{City in the Sky}"
|
||||
Cryptic:
|
||||
Text: the {skies above}
|
||||
|
||||
@@ -1594,10 +1594,64 @@ Hyrule Castle:
|
||||
Standard:
|
||||
Text: Hyrule Castle
|
||||
Pretty:
|
||||
Text: the {Hyrule Castle}
|
||||
Text: "{Hyrule Castle}"
|
||||
Cryptic:
|
||||
Text: the {kingdom's castle}
|
||||
|
||||
# BOSS/GOAL NAMES
|
||||
Diababa:
|
||||
Standard:
|
||||
Text: Diababa
|
||||
|
||||
Fyrus:
|
||||
Standard:
|
||||
Text: Fyrus
|
||||
|
||||
Morpheel:
|
||||
Standard:
|
||||
Text: Morpheel
|
||||
|
||||
Stallord:
|
||||
Standard:
|
||||
Text: Stallord
|
||||
|
||||
Blizzeta:
|
||||
Standard:
|
||||
Text: Blizzeta
|
||||
|
||||
Armogohma:
|
||||
Standard:
|
||||
Text: Armogohma
|
||||
|
||||
Argorok:
|
||||
Standard:
|
||||
Text: Argorok
|
||||
|
||||
Zant:
|
||||
Standard:
|
||||
Text: Zant
|
||||
|
||||
Ganondorf:
|
||||
Standard:
|
||||
Text: Ganondorf
|
||||
|
||||
# HINT TEMPLATES
|
||||
Path Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that <regions> |is/are| on the path to <goal name>.
|
||||
|
||||
Barren Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that visiting <region> is a foolish choice.
|
||||
|
||||
Item Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that <Item Pretty or Cryptic Name> can be found at <regions>.
|
||||
|
||||
Location Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that the reward for <Location Name> is <Item Pretty or Cryptic Name>.
|
||||
|
||||
# NO REQUIRED DUNGEON TEXT
|
||||
No Required Dungeons Text:
|
||||
Standard:
|
||||
@@ -2212,6 +2266,26 @@ Midna Hints Required Dungeons Intro At Least One Dungeon:
|
||||
Text: |-
|
||||
There are <red><required dungeon count><white> required dungeons:
|
||||
|
||||
Midna Hints Path Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
The following <red>paths<white> lie before you:
|
||||
|
||||
Midna Hints Barren Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
There is <purple>nothing<white> to be found in these regions:
|
||||
|
||||
Midna Hints Item Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
These <green>items<white> may be of use to you:
|
||||
|
||||
Midna Hints Location Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
These <red>locations<white> are known to me:
|
||||
|
||||
Ordon Hint Sign Text:
|
||||
Standard:
|
||||
Text: |-
|
||||
|
||||
@@ -1510,11 +1510,11 @@ North Gerudo Desert:
|
||||
Cryptic:
|
||||
Text: the {northern desert}
|
||||
|
||||
Bublin Camp:
|
||||
Bulblin Camp:
|
||||
Standard:
|
||||
Text: Bublin Camp
|
||||
Text: Bulblin Camp
|
||||
Pretty:
|
||||
Text: "{Bublin Camp}"
|
||||
Text: "{Bulblin Camp}"
|
||||
Cryptic:
|
||||
Text: a {camp of enemies}
|
||||
|
||||
@@ -1538,7 +1538,7 @@ Goron Mines:
|
||||
Standard:
|
||||
Text: Goron Mines
|
||||
Pretty:
|
||||
Text: the {Goron Mines}
|
||||
Text: "{Goron Mines}"
|
||||
Cryptic:
|
||||
Text: a {volcanic mine}
|
||||
|
||||
@@ -1546,7 +1546,7 @@ Lakebed Temple:
|
||||
Standard:
|
||||
Text: Lakebed Temple
|
||||
Pretty:
|
||||
Text: the {Lakebed Temple}
|
||||
Text: "{Lakebed Temple}"
|
||||
Cryptic:
|
||||
Text: an {underground lake}
|
||||
|
||||
@@ -1562,7 +1562,7 @@ Snowpeak Ruins:
|
||||
Standard:
|
||||
Text: Snowpeak Ruins
|
||||
Pretty:
|
||||
Text: the {Snowpeak Ruins}
|
||||
Text: "{Snowpeak Ruins}"
|
||||
Cryptic:
|
||||
Text: a {snowy mansion}
|
||||
|
||||
@@ -1578,7 +1578,7 @@ City in the Sky:
|
||||
Standard:
|
||||
Text: City in the Sky
|
||||
Pretty:
|
||||
Text: the {City in the Sky}
|
||||
Text: "{City in the Sky}"
|
||||
Cryptic:
|
||||
Text: the {skies above}
|
||||
|
||||
@@ -1594,10 +1594,64 @@ Hyrule Castle:
|
||||
Standard:
|
||||
Text: Hyrule Castle
|
||||
Pretty:
|
||||
Text: the {Hyrule Castle}
|
||||
Text: "{Hyrule Castle}"
|
||||
Cryptic:
|
||||
Text: the {kingdom's castle}
|
||||
|
||||
# BOSS/GOAL NAMES
|
||||
Diababa:
|
||||
Standard:
|
||||
Text: Diababa
|
||||
|
||||
Fyrus:
|
||||
Standard:
|
||||
Text: Fyrus
|
||||
|
||||
Morpheel:
|
||||
Standard:
|
||||
Text: Morpheel
|
||||
|
||||
Stallord:
|
||||
Standard:
|
||||
Text: Stallord
|
||||
|
||||
Blizzeta:
|
||||
Standard:
|
||||
Text: Blizzeta
|
||||
|
||||
Armogohma:
|
||||
Standard:
|
||||
Text: Armogohma
|
||||
|
||||
Argorok:
|
||||
Standard:
|
||||
Text: Argorok
|
||||
|
||||
Zant:
|
||||
Standard:
|
||||
Text: Zant
|
||||
|
||||
Ganondorf:
|
||||
Standard:
|
||||
Text: Ganondorf
|
||||
|
||||
# HINT TEMPLATES
|
||||
Path Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that <regions> |is/are| on the path to <goal name>.
|
||||
|
||||
Barren Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that visiting <region> is a foolish choice.
|
||||
|
||||
Item Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that <Item Pretty or Cryptic Name> can be found at <regions>.
|
||||
|
||||
Location Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that the reward for <Location Name> is <Item Pretty or Cryptic Name>.
|
||||
|
||||
# NO REQUIRED DUNGEON TEXT
|
||||
No Required Dungeons Text:
|
||||
Standard:
|
||||
@@ -2212,6 +2266,26 @@ Midna Hints Required Dungeons Intro At Least One Dungeon:
|
||||
Text: |-
|
||||
There are <red><required dungeon count><white> required dungeons:
|
||||
|
||||
Midna Hints Path Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
The following <red>paths<white> lie before you:
|
||||
|
||||
Midna Hints Barren Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
There is <purple>nothing<white> to be found in these regions:
|
||||
|
||||
Midna Hints Item Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
These <green>items<white> may be of use to you:
|
||||
|
||||
Midna Hints Location Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
These <red>locations<white> are known to me:
|
||||
|
||||
Ordon Hint Sign Text:
|
||||
Standard:
|
||||
Text: |-
|
||||
|
||||
@@ -1510,11 +1510,11 @@ North Gerudo Desert:
|
||||
Cryptic:
|
||||
Text: the {northern desert}
|
||||
|
||||
Bublin Camp:
|
||||
Bulblin Camp:
|
||||
Standard:
|
||||
Text: Bublin Camp
|
||||
Text: Bulblin Camp
|
||||
Pretty:
|
||||
Text: "{Bublin Camp}"
|
||||
Text: "{Bulblin Camp}"
|
||||
Cryptic:
|
||||
Text: a {camp of enemies}
|
||||
|
||||
@@ -1538,7 +1538,7 @@ Goron Mines:
|
||||
Standard:
|
||||
Text: Goron Mines
|
||||
Pretty:
|
||||
Text: the {Goron Mines}
|
||||
Text: "{Goron Mines}"
|
||||
Cryptic:
|
||||
Text: a {volcanic mine}
|
||||
|
||||
@@ -1546,7 +1546,7 @@ Lakebed Temple:
|
||||
Standard:
|
||||
Text: Lakebed Temple
|
||||
Pretty:
|
||||
Text: the {Lakebed Temple}
|
||||
Text: "{Lakebed Temple}"
|
||||
Cryptic:
|
||||
Text: an {underground lake}
|
||||
|
||||
@@ -1562,7 +1562,7 @@ Snowpeak Ruins:
|
||||
Standard:
|
||||
Text: Snowpeak Ruins
|
||||
Pretty:
|
||||
Text: the {Snowpeak Ruins}
|
||||
Text: "{Snowpeak Ruins}"
|
||||
Cryptic:
|
||||
Text: a {snowy mansion}
|
||||
|
||||
@@ -1578,7 +1578,7 @@ City in the Sky:
|
||||
Standard:
|
||||
Text: City in the Sky
|
||||
Pretty:
|
||||
Text: the {City in the Sky}
|
||||
Text: "{City in the Sky}"
|
||||
Cryptic:
|
||||
Text: the {skies above}
|
||||
|
||||
@@ -1594,10 +1594,64 @@ Hyrule Castle:
|
||||
Standard:
|
||||
Text: Hyrule Castle
|
||||
Pretty:
|
||||
Text: the {Hyrule Castle}
|
||||
Text: "{Hyrule Castle}"
|
||||
Cryptic:
|
||||
Text: the {kingdom's castle}
|
||||
|
||||
# BOSS/GOAL NAMES
|
||||
Diababa:
|
||||
Standard:
|
||||
Text: Diababa
|
||||
|
||||
Fyrus:
|
||||
Standard:
|
||||
Text: Fyrus
|
||||
|
||||
Morpheel:
|
||||
Standard:
|
||||
Text: Morpheel
|
||||
|
||||
Stallord:
|
||||
Standard:
|
||||
Text: Stallord
|
||||
|
||||
Blizzeta:
|
||||
Standard:
|
||||
Text: Blizzeta
|
||||
|
||||
Armogohma:
|
||||
Standard:
|
||||
Text: Armogohma
|
||||
|
||||
Argorok:
|
||||
Standard:
|
||||
Text: Argorok
|
||||
|
||||
Zant:
|
||||
Standard:
|
||||
Text: Zant
|
||||
|
||||
Ganondorf:
|
||||
Standard:
|
||||
Text: Ganondorf
|
||||
|
||||
# HINT TEMPLATES
|
||||
Path Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that <regions> |is/are| on the path to <goal name>.
|
||||
|
||||
Barren Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that visiting <region> is a foolish choice.
|
||||
|
||||
Item Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that <Item Pretty or Cryptic Name> can be found at <regions>.
|
||||
|
||||
Location Hint:
|
||||
Standard:
|
||||
Text: <fast>They say that the reward for <Location Name> is <Item Pretty or Cryptic Name>.
|
||||
|
||||
# NO REQUIRED DUNGEON TEXT
|
||||
No Required Dungeons Text:
|
||||
Standard:
|
||||
@@ -2212,6 +2266,26 @@ Midna Hints Required Dungeons Intro At Least One Dungeon:
|
||||
Text: |-
|
||||
There are <red><required dungeon count><white> required dungeons:
|
||||
|
||||
Midna Hints Path Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
The following <red>paths<white> lie before you:
|
||||
|
||||
Midna Hints Barren Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
There is <purple>nothing<white> to be found in these regions:
|
||||
|
||||
Midna Hints Item Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
These <green>items<white> may be of use to you:
|
||||
|
||||
Midna Hints Location Hints Intro:
|
||||
Standard:
|
||||
Text: |-
|
||||
These <red>locations<white> are known to me:
|
||||
|
||||
Ordon Hint Sign Text:
|
||||
Standard:
|
||||
Text: |-
|
||||
|
||||
@@ -377,7 +377,7 @@
|
||||
|
||||
# Completely Custom Text IDs
|
||||
- Name: Custom Midna Call Need Something Text
|
||||
Attributes: 0x07F60000150D0000FF00000000000400
|
||||
Attributes: 0x07D30000150D0100FF000D001F1F0400
|
||||
|
||||
- Name: Custom Midna Call 3 Choice Text
|
||||
Attributes: 0x07F8000015000000FF00000000000400
|
||||
@@ -386,10 +386,10 @@
|
||||
Attributes: 0x07F8000015000000FF00000000000400
|
||||
|
||||
- Name: Custom Midna Call Hints Text
|
||||
Attributes: 0x07F60000150D0000FF00000000000400
|
||||
Attributes: 0x07D30000150D0100FF000D001F1F0400
|
||||
|
||||
- Name: Return to Spawn Dungeon Intro Text
|
||||
Attributes: 0x07F60000150D0000FF00000000000400
|
||||
Attributes: 0x07D30000150D0100FF000D001F1F0400
|
||||
|
||||
- Name: Return to Spawn Dungeon Choice Text
|
||||
Attributes: 0x07F60000150D0000FF00000000000400
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
#include "../utility/random.hpp"
|
||||
#include "../utility/string.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <ranges>
|
||||
|
||||
namespace randomizer::logic::fill
|
||||
{
|
||||
@@ -64,16 +65,18 @@ namespace randomizer::logic::fill
|
||||
}
|
||||
}
|
||||
|
||||
void AssumedFill(world::WorldPool& worlds,
|
||||
item_pool::ItemPool& itemsToPlacePool,
|
||||
const item_pool::ItemPool& itemsNotYetPlaced,
|
||||
location::LocationPool allowedLocations,
|
||||
const int& worldToFill /* = -1 */)
|
||||
location::LocationPool AssumedFill(
|
||||
world::WorldPool& worlds,
|
||||
item_pool::ItemPool& itemsToPlacePool,
|
||||
const item_pool::ItemPool& itemsNotYetPlaced,
|
||||
location::LocationPool allowedLocations,
|
||||
const int& worldToFill /* = -1 */)
|
||||
{
|
||||
// Assumed Fill may sometimes place items in such a way that accidentally locks out being able to place specific items
|
||||
// later on. Allow the algorithm to retry a reasonable amount of times before returning an error.
|
||||
int retries = 10;
|
||||
bool unsuccessfulPlacement = true;
|
||||
location::LocationPool rollbacks = {};
|
||||
while (unsuccessfulPlacement)
|
||||
{
|
||||
if (retries <= 0)
|
||||
@@ -100,7 +103,7 @@ namespace randomizer::logic::fill
|
||||
|
||||
utility::random::ShufflePool(itemsToPlacePool);
|
||||
auto itemsToPlace = itemsToPlacePool;
|
||||
location::LocationPool rollbacks = {};
|
||||
rollbacks.clear();
|
||||
|
||||
while (!itemsToPlace.empty())
|
||||
{
|
||||
@@ -182,6 +185,8 @@ namespace randomizer::logic::fill
|
||||
rollbacks.push_back(spotToFill);
|
||||
}
|
||||
}
|
||||
|
||||
return rollbacks;
|
||||
}
|
||||
|
||||
void FastFill(item_pool::ItemPool& itemsToPlace, location::LocationPool allowedLocations)
|
||||
@@ -277,7 +282,12 @@ namespace randomizer::logic::fill
|
||||
|
||||
// Place goal items at goal locations
|
||||
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
|
||||
AssumedFill(worlds, goalItems, completeItemPool, goalLocations);
|
||||
auto filledLocations = AssumedFill(worlds, goalItems, completeItemPool, goalLocations);
|
||||
|
||||
// Goal items placed at goal locations are expected items
|
||||
for (auto location: filledLocations) {
|
||||
location->SetExpectedItem(true);
|
||||
}
|
||||
|
||||
// Determine required dungeons now that we placed goal location items
|
||||
world->DetermineRequiredDungeons();
|
||||
@@ -317,7 +327,11 @@ namespace randomizer::logic::fill
|
||||
(item->GetName() == "Ordon Pumpkin" || item->GetName() == "Ordon Cheese"));
|
||||
});
|
||||
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
|
||||
AssumedFill(worlds, smallKeys, completeItemPool, dungeonLocations);
|
||||
auto filledLocations = AssumedFill(worlds, smallKeys, completeItemPool, dungeonLocations);
|
||||
// Own Dungeon small keys are expected items
|
||||
for (auto location: filledLocations) {
|
||||
location->SetExpectedItem(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Big Keys
|
||||
@@ -327,7 +341,11 @@ namespace randomizer::logic::fill
|
||||
[&](const auto& item)
|
||||
{ return item == dungeon_->GetBigKey(); });
|
||||
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
|
||||
AssumedFill(worlds, bigKeys, completeItemPool, dungeonLocations);
|
||||
auto filledLocations = AssumedFill(worlds, bigKeys, completeItemPool, dungeonLocations);
|
||||
// Own Dungeon big keys are expected items
|
||||
for (auto location: filledLocations) {
|
||||
location->SetExpectedItem(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Place maps and compasses last with fast fill since they're junk items
|
||||
@@ -525,7 +543,7 @@ namespace randomizer::logic::fill
|
||||
LOG_TO_DEBUG("Caching timeforms for world " + std::to_string(world->GetID()));
|
||||
auto& exitTimeFormCache = world->GetExitTimeFormCache();
|
||||
exitTimeFormCache.clear();
|
||||
for (const auto& [areaName, area] : world->GetAreaTable())
|
||||
for (const auto& area : world->GetAreaTable() | std::views::values)
|
||||
{
|
||||
const auto& areaFormTimes = searchWithItems._areaFormTime[area.get()];
|
||||
for (const auto& exit : area->GetExits())
|
||||
@@ -535,10 +553,12 @@ namespace randomizer::logic::fill
|
||||
for (const auto& formTime : requirement::FormTime::ALL_FORM_TIMES)
|
||||
{
|
||||
if (formTime & areaFormTimes &&
|
||||
requirement::EvaluateRequirementAtFormTime(req,
|
||||
&searchWithItems,
|
||||
formTime,
|
||||
world.get()))
|
||||
requirement::EvaluateRequirementAtFormTime(
|
||||
req,
|
||||
&searchWithItems,
|
||||
formTime,
|
||||
world.get()
|
||||
))
|
||||
{
|
||||
exitTimeFormCache[exit] |= formTime;
|
||||
}
|
||||
|
||||
@@ -22,12 +22,16 @@ namespace randomizer::logic::fill
|
||||
* This is important for the assumed fill algorithm since we need to assume we have these items.
|
||||
* @param allowedLocations Locations where items in itemsToPlacePool are allowed to be filled.
|
||||
* @param worldToFill A specific world to fill. If -1 (default), then all worlds are considered
|
||||
*
|
||||
* @return The locations which had items placed at them during this fill call
|
||||
*/
|
||||
void AssumedFill(world::WorldPool& worlds,
|
||||
item_pool::ItemPool& itemsToPlacePool,
|
||||
const item_pool::ItemPool& itemsNotYetPlaced,
|
||||
location::LocationPool allowedLocations,
|
||||
const int& worldToFill = -1);
|
||||
location::LocationPool AssumedFill(
|
||||
world::WorldPool& worlds,
|
||||
item_pool::ItemPool& itemsToPlacePool,
|
||||
const item_pool::ItemPool& itemsNotYetPlaced,
|
||||
location::LocationPool allowedLocations,
|
||||
const int& worldToFill = -1
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Places items in locations completely randomly without any logic checks.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "../utility/text.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
namespace randomizer::logic::location {
|
||||
class Location;
|
||||
}
|
||||
|
||||
namespace randomizer::logic::hints {
|
||||
|
||||
struct PathHint {
|
||||
location::Location* goalLocation;
|
||||
location::Location* hintedLocation;
|
||||
bool operator==(const PathHint&) const = default;
|
||||
};
|
||||
|
||||
struct BarrenHint {
|
||||
std::string region;
|
||||
bool operator==(const BarrenHint&) const = default;
|
||||
};
|
||||
|
||||
struct ItemHint {
|
||||
location::Location* location;
|
||||
bool operator==(const ItemHint&) const = default;
|
||||
};
|
||||
|
||||
struct LocationHint {
|
||||
location::Location* location;
|
||||
bool operator==(const LocationHint&) const = default;
|
||||
};
|
||||
|
||||
using HintData = std::variant<std::monostate, PathHint, BarrenHint, ItemHint, LocationHint>;
|
||||
struct Hint {
|
||||
Text text{};
|
||||
HintData data;
|
||||
|
||||
bool operator==(const Hint&) const = default;
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
#include "hints.hpp"
|
||||
|
||||
#include "dusk/randomizer/generator/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,32 +947,100 @@ 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);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "hint_types.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace randomizer::logic::world {
|
||||
@@ -9,6 +11,26 @@ using WorldPool = std::vector<std::unique_ptr<World>>;
|
||||
|
||||
namespace randomizer::logic::hints {
|
||||
|
||||
void GenerateAllHints(world::WorldPool& worldPool);
|
||||
class HintGenerator {
|
||||
public:
|
||||
HintGenerator(world::World* world);
|
||||
|
||||
void GeneratePathHints();
|
||||
void GenerateBarrenHints();
|
||||
void GenerateItemHints();
|
||||
void GenerateRemoteLocationHints();
|
||||
void GenerateLocationHints();
|
||||
void DistributeHints();
|
||||
void FinalizeHintSignText();
|
||||
|
||||
private:
|
||||
world::World* _world{};
|
||||
std::vector<Hint> _pathHints{};
|
||||
std::vector<Hint> _barrenHints{};
|
||||
std::vector<Hint> _itemHints{};
|
||||
std::vector<Hint> _locationHints{};
|
||||
};
|
||||
|
||||
void GenerateAllHints(world::WorldPool& worldPool);
|
||||
|
||||
}
|
||||
@@ -105,7 +105,11 @@ namespace randomizer::logic::item
|
||||
return this->_gameWinningItem;
|
||||
}
|
||||
|
||||
std::list<Location*> Item::GetChainLocations() const
|
||||
void Item::AddChainLocation(location::Location* location) {
|
||||
this->_chainLocations.push_back(location);
|
||||
}
|
||||
|
||||
std::list<location::Location*> Item::GetChainLocations() const
|
||||
{
|
||||
return this->_chainLocations;
|
||||
}
|
||||
@@ -150,6 +154,29 @@ namespace randomizer::logic::item
|
||||
return this->_stamp;
|
||||
}
|
||||
|
||||
bool Item::CanBeInBarrenRegion() const {
|
||||
return !this->IsMajor() ||
|
||||
(this->IsDungeonSmallKey() && this->_world->Setting("Small Keys").IsAnyOf("Vanilla", "Own Dungeon")) ||
|
||||
(this->IsBigKey() && this->_world->Setting("Big Keys").IsAnyOf("Vanilla", "Own Dungeon"));
|
||||
}
|
||||
|
||||
bool Item::IsSameOrSimilarItem(Item* otherItem) const {
|
||||
if (this == otherItem) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Heart Piece Items
|
||||
std::set<const Item*> heartPieceItems = {
|
||||
this->_world->GetItem("Heart Container"),
|
||||
this->_world->GetItem("Piece of Heart"),
|
||||
};
|
||||
if (heartPieceItems.contains(this) && heartPieceItems.contains(otherItem)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Item::operator==(const Item& rhs) const
|
||||
{
|
||||
return this->_id == rhs._id && this->_world->GetID() == rhs._world->GetID();
|
||||
|
||||
@@ -8,7 +8,9 @@ namespace randomizer::logic::world
|
||||
{
|
||||
class World;
|
||||
};
|
||||
class Location;
|
||||
namespace randomizer::logic::location {
|
||||
class Location;
|
||||
}
|
||||
namespace randomizer::logic::item
|
||||
{
|
||||
enum Importance
|
||||
@@ -43,7 +45,8 @@ namespace randomizer::logic::item
|
||||
bool IsMinor() const;
|
||||
bool isJunk() const;
|
||||
bool IsGameWinningItem() const;
|
||||
std::list<Location*> GetChainLocations() const;
|
||||
void AddChainLocation(location::Location* location);
|
||||
std::list<location::Location*> GetChainLocations() const;
|
||||
bool IsDungeonSmallKey() const;
|
||||
bool IsBigKey() const;
|
||||
bool IsDungeonMap() const;
|
||||
@@ -52,6 +55,8 @@ namespace randomizer::logic::item
|
||||
bool IsShadowCrystal() const;
|
||||
bool IsBottle() const;
|
||||
bool IsStamp() const;
|
||||
bool CanBeInBarrenRegion() const;
|
||||
bool IsSameOrSimilarItem(Item* otherItem) const;
|
||||
|
||||
bool operator==(const Item& rhs) const;
|
||||
bool operator<(const Item& rhs) const;
|
||||
@@ -62,7 +67,7 @@ namespace randomizer::logic::item
|
||||
world::World* _world = nullptr;
|
||||
Importance _importance = INVALID;
|
||||
bool _gameWinningItem = false;
|
||||
std::list<Location*> _chainLocations;
|
||||
std::list<location::Location*> _chainLocations;
|
||||
|
||||
bool _dungeonSmallKey = false;
|
||||
bool _bigKey = false;
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace randomizer::logic::location
|
||||
const std::unordered_set<std::string>& categories,
|
||||
world::World* world,
|
||||
item::Item* originalItem,
|
||||
const bool& goalLocation,
|
||||
bool goalLocation,
|
||||
const std::string& goalName,
|
||||
const std::string& hintPriority,
|
||||
const YAML::Node& metadata):
|
||||
_id(id),
|
||||
@@ -19,6 +20,7 @@ namespace randomizer::logic::location
|
||||
_world(world),
|
||||
_originalItem(originalItem),
|
||||
_goalLocation(goalLocation),
|
||||
_goalName(goalName),
|
||||
_hintPriority(hintPriority),
|
||||
_metadata(metadata)
|
||||
{
|
||||
@@ -45,6 +47,10 @@ namespace randomizer::logic::location
|
||||
return this->_goalLocation;
|
||||
}
|
||||
|
||||
const std::string& Location::GetGoalName() const {
|
||||
return this->_goalName;
|
||||
}
|
||||
|
||||
void Location::SetCurrentItem(item::Item* item)
|
||||
{
|
||||
LOG_TO_DEBUG("Placed " + item->GetName() + " at " + this->GetName());
|
||||
@@ -77,7 +83,7 @@ namespace randomizer::logic::location
|
||||
return this->_trackedItem;
|
||||
}
|
||||
|
||||
void Location::SetKnownVanillaItem(const bool& hasKnownVanillaItem)
|
||||
void Location::SetKnownVanillaItem(bool hasKnownVanillaItem)
|
||||
{
|
||||
this->_hasKnownVanillaItem = hasKnownVanillaItem;
|
||||
}
|
||||
@@ -87,6 +93,15 @@ namespace randomizer::logic::location
|
||||
return this->_hasKnownVanillaItem;
|
||||
}
|
||||
|
||||
void Location::SetExpectedItem(bool hasExpectedItem) {
|
||||
this->_hasExpectedItem = hasExpectedItem;
|
||||
}
|
||||
|
||||
bool Location::HasExpectedItem() const {
|
||||
return this->_hasExpectedItem || this->_hasKnownVanillaItem;
|
||||
}
|
||||
|
||||
|
||||
void Location::SetProgression(const bool& progression)
|
||||
{
|
||||
this->_progression = progression;
|
||||
@@ -149,6 +164,22 @@ namespace randomizer::logic::location
|
||||
this->_registeredLocationCategories = registeredLocationCategories;
|
||||
}
|
||||
|
||||
void Location::AddPathLocation(Location* location) {
|
||||
this->_pathLocations.push_back(location);
|
||||
}
|
||||
|
||||
std::vector<Location*>& Location::GetPathLocations() {
|
||||
return this->_pathLocations;
|
||||
}
|
||||
|
||||
void Location::SetImportance(Importance importance) {
|
||||
this->_importance = importance;
|
||||
}
|
||||
|
||||
Importance Location::GetImportance() const {
|
||||
return this->_importance;
|
||||
}
|
||||
|
||||
const std::set<std::string>& GetAllRandomizerLocationNames() {
|
||||
static std::set<std::string> locationNames{};
|
||||
|
||||
|
||||
@@ -20,6 +20,13 @@ namespace randomizer::logic::area
|
||||
|
||||
namespace randomizer::logic::location
|
||||
{
|
||||
enum class Importance {
|
||||
UNKNOWN,
|
||||
NOT_REQUIRED,
|
||||
POSSIBLY_REQUIRED,
|
||||
REQUIRED,
|
||||
};
|
||||
|
||||
class Location
|
||||
{
|
||||
public:
|
||||
@@ -28,7 +35,8 @@ namespace randomizer::logic::location
|
||||
const std::unordered_set<std::string>& categories,
|
||||
world::World* world,
|
||||
item::Item* originalItem,
|
||||
const bool& goalLocation,
|
||||
bool goalLocation,
|
||||
const std::string& goalName,
|
||||
const std::string& hintPriority,
|
||||
const YAML::Node& metadata);
|
||||
|
||||
@@ -36,14 +44,17 @@ namespace randomizer::logic::location
|
||||
std::string GetName() const;
|
||||
world::World* GetWorld() const;
|
||||
bool IsGoalLocation() const;
|
||||
const std::string& GetGoalName() const;
|
||||
void SetCurrentItem(item::Item* currentItem);
|
||||
item::Item* GetCurrentItem() const;
|
||||
void RemoveCurrentItem();
|
||||
bool IsEmpty() const;
|
||||
item::Item* GetOriginalItem() const;
|
||||
item::Item* GetTrackedItem() const;
|
||||
void SetKnownVanillaItem(const bool& hasKnownVanillaItem);
|
||||
void SetKnownVanillaItem(bool hasKnownVanillaItem);
|
||||
bool HasKnownVanillaItem() const;
|
||||
void SetExpectedItem(bool hasExpectedItem);
|
||||
bool HasExpectedItem() const;
|
||||
void SetProgression(const bool& progression);
|
||||
bool IsProgression() const;
|
||||
void SetHinted(const bool& hinted);
|
||||
@@ -56,6 +67,10 @@ namespace randomizer::logic::location
|
||||
void SetComputedRequirement(const requirement::Requirement& computedRequirement);
|
||||
requirement::Requirement GetComputedRequirement();
|
||||
void SetRegisteredLocationCategories(std::unordered_set<std::string>* registeredLocationCategories);
|
||||
void AddPathLocation(Location* location);
|
||||
std::vector<Location*>& GetPathLocations();
|
||||
void SetImportance(Importance importance);
|
||||
Importance GetImportance() const;
|
||||
|
||||
/**
|
||||
* @brief Checks to see if the location has all the passed in categories. If a passed in category was never registered,
|
||||
@@ -84,13 +99,15 @@ namespace randomizer::logic::location
|
||||
|
||||
private:
|
||||
int _id = -1;
|
||||
std::string _name = "";
|
||||
std::string _name{};
|
||||
std::unordered_set<std::string> _categories = {};
|
||||
world::World* _world;
|
||||
item::Item* _originalItem = item::Nothing.get();
|
||||
bool _goalLocation = false;
|
||||
std::string _goalName{};
|
||||
item::Item* _currentItem = item::Nothing.get();
|
||||
bool _hasKnownVanillaItem = false;
|
||||
bool _hasExpectedItem = false;
|
||||
std::list<area::LocationAccess*> _locationAccessList = {};
|
||||
bool _progression = true; // Set as false later if applicable
|
||||
bool _hinted = false;
|
||||
@@ -105,6 +122,10 @@ namespace randomizer::logic::location
|
||||
*/
|
||||
std::unordered_set<std::string>* _registeredLocationCategories = nullptr;
|
||||
|
||||
// Hint related things
|
||||
std::vector<Location*> _pathLocations{};
|
||||
Importance _importance = Importance::UNKNOWN;
|
||||
|
||||
// Potential tracker stuff
|
||||
item::Item* _trackedItem = item::Nothing.get();
|
||||
};
|
||||
|
||||
@@ -149,6 +149,82 @@ namespace randomizer::logic::requirement
|
||||
return reqStr;
|
||||
}
|
||||
|
||||
// Return a set of all items mentioned in this requirement
|
||||
std::unordered_set<item::Item*> Requirement::getItems(world::World* world) const {
|
||||
std::unordered_set<item::Item*> items = {};
|
||||
switch (this->_type) {
|
||||
case Type::AND:
|
||||
case Type::OR:
|
||||
for (const auto& arg : this->_args) {
|
||||
items.merge(std::get<Requirement>(arg).getItems(world));
|
||||
}
|
||||
break;
|
||||
case Type::ITEM:
|
||||
items.insert(std::get<item::Item*>(this->_args[0]));
|
||||
break;
|
||||
case Type::COUNT:
|
||||
items.insert(std::get<item::Item*>(this->_args[1]));
|
||||
break;
|
||||
case Type::HEARTS:
|
||||
items.insert(world->GetItem("Piece of Heart"));
|
||||
items.insert(world->GetItem("Heart Container"));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
// Return the heart count necessary for this requirement
|
||||
int Requirement::getHeartCount(int curCount /*= 0*/) const {
|
||||
switch (this->_type) {
|
||||
case Type::AND:
|
||||
case Type::OR:
|
||||
for (const auto& arg : this->_args) {
|
||||
curCount = std::get<Requirement>(arg).getHeartCount(curCount);
|
||||
}
|
||||
break;
|
||||
case Type::HEARTS:
|
||||
if (std::get<int>(this->_args[0]) > curCount) {
|
||||
curCount = std::get<int>(this->_args[0]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return curCount;
|
||||
}
|
||||
|
||||
bool Requirement::operator==(const Requirement& other) const {
|
||||
// Types and argument counts must match
|
||||
if (_type != other._type || _args.size() != other._args.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check matches to account for different ordering
|
||||
std::vector<bool> matched(other._args.size(), false);
|
||||
|
||||
for (const auto& arg : _args) {
|
||||
bool foundMatch = false;
|
||||
for (size_t i = 0; i < other._args.size(); ++i) {
|
||||
if (!matched[i] && arg == other._args[i]) {
|
||||
matched[i] = true;
|
||||
foundMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If an argument in _args has no unassigned match in other._args, they are not equal
|
||||
if (!foundMatch) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Requirement ParseRequirementString(const std::string& reqStr,
|
||||
world::World* world,
|
||||
const bool& forceLogic /* = false */)
|
||||
@@ -555,6 +631,8 @@ namespace randomizer::logic::requirement
|
||||
int count;
|
||||
int eventIndex;
|
||||
int macroIndex;
|
||||
bool changedToOnlyItemsAtStart = false;
|
||||
bool evaluation = false;
|
||||
switch (req._type)
|
||||
{
|
||||
case Type::NOTHING:
|
||||
@@ -564,11 +642,27 @@ namespace randomizer::logic::requirement
|
||||
return false;
|
||||
|
||||
case Type::OR:
|
||||
return std::ranges::any_of(
|
||||
if (search->_searchMode == search::SearchMode::LOCATION_IMPORTANCE &&
|
||||
std::ranges::any_of(req._args, [&](const auto& arg) {
|
||||
return std::get<Requirement>(arg) == search->_assumedFalseReq;
|
||||
}) &&
|
||||
!search->_onlySearchWithItemsAtStart)
|
||||
{
|
||||
changedToOnlyItemsAtStart = true;
|
||||
search->_onlySearchWithItemsAtStart = true;
|
||||
}
|
||||
|
||||
evaluation = std::ranges::any_of(
|
||||
req._args,
|
||||
[&](const auto& arg)
|
||||
{ return EvaluateRequirementAtFormTime(std::get<Requirement>(arg), search, formTime, world); });
|
||||
|
||||
if (changedToOnlyItemsAtStart) {
|
||||
search->_onlySearchWithItemsAtStart = false;
|
||||
}
|
||||
|
||||
return evaluation;
|
||||
|
||||
case Type::AND:
|
||||
return std::ranges::all_of(
|
||||
req._args,
|
||||
@@ -577,11 +671,17 @@ namespace randomizer::logic::requirement
|
||||
|
||||
case Type::ITEM:
|
||||
item = std::get<item::Item*>(req._args[0]);
|
||||
if (search->_onlySearchWithItemsAtStart) {
|
||||
return search->_itemsAtStart.contains(item);
|
||||
}
|
||||
return search->_ownedItems.contains(item);
|
||||
|
||||
case Type::COUNT:
|
||||
count = std::get<int>(req._args[0]);
|
||||
item = std::get<item::Item*>(req._args[1]);
|
||||
if (search->_onlySearchWithItemsAtStart) {
|
||||
return search->_itemsAtStart.count(item) >= count;
|
||||
}
|
||||
return search->_ownedItems.count(item) >= count;
|
||||
|
||||
case Type::EVENT:
|
||||
@@ -614,6 +714,10 @@ namespace randomizer::logic::requirement
|
||||
|
||||
case Type::HEARTS:
|
||||
count = std::get<int>(req._args[0]);
|
||||
if (search->_searchMode == search::SearchMode::LOCATION_IMPORTANCE &&
|
||||
search->_assumedHeartCount >= count) {
|
||||
return true;
|
||||
}
|
||||
heartPiece = world->GetItem("Piece of Heart");
|
||||
heartContainer = world->GetItem("Heart Container");
|
||||
return search->_ownedItems.count(heartPiece) +
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
@@ -96,6 +97,10 @@ namespace randomizer::logic::requirement
|
||||
std::vector<Argument> _args;
|
||||
|
||||
std::string to_string() const;
|
||||
std::unordered_set<item::Item*> getItems(world::World* world) const;
|
||||
int getHeartCount(int curCount = 0) const;
|
||||
|
||||
bool operator==(const Requirement& other) const;
|
||||
};
|
||||
|
||||
Requirement ParseRequirementString(const std::string& reqStr,
|
||||
|
||||
@@ -18,8 +18,10 @@ namespace randomizer::logic::search
|
||||
world::WorldPool* worlds,
|
||||
const item_pool::ItemPool& items /* = {} */,
|
||||
const int& worldToSearch /* = -1 */,
|
||||
bool startingInventory /*= true */):
|
||||
_searchMode(searchMode), _worlds(worlds), _startingInventory(startingInventory)
|
||||
bool startingInventory /*= true */,
|
||||
location::Location* importanceLocation /*= nullptr*/):
|
||||
_searchMode(searchMode), _worlds(worlds), _startingInventory(startingInventory),
|
||||
_importanceLocation(importanceLocation)
|
||||
{
|
||||
// Set the items we should already own
|
||||
this->_ownedItems.insert(items.begin(), items.end());
|
||||
@@ -34,6 +36,7 @@ namespace randomizer::logic::search
|
||||
this->_ownedItems.insert(startingInventory.begin(), startingInventory.end());
|
||||
}
|
||||
}
|
||||
this->_itemsAtStart = _ownedItems;
|
||||
}
|
||||
|
||||
// Set search starting properties and add each world's root exits to _exitsToTry
|
||||
@@ -53,6 +56,33 @@ namespace randomizer::logic::search
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're doing an importance search, then we have to set up a few extra things
|
||||
if (this->_searchMode == SearchMode::LOCATION_IMPORTANCE && importanceLocation != nullptr) {
|
||||
auto importanceItem = importanceLocation->GetCurrentItem();
|
||||
|
||||
// Generate the logic requirement for what it would "mean" to obtain the item at this importance location.
|
||||
// We're going to assume that this logic requirement is always false when searching.
|
||||
// This ensures that we only evaluate to true the chain locations that this item absolutely does not help unlock
|
||||
auto count = this->_ownedItems.count(importanceItem) + 1;
|
||||
if (count == 1) {
|
||||
this->_assumedFalseReq = requirement::Requirement{requirement::Type::ITEM, {importanceItem}};
|
||||
} else {
|
||||
this->_assumedFalseReq = requirement::Requirement{requirement::Type::COUNT, {count, importanceItem}};
|
||||
}
|
||||
|
||||
// If this location is either a heart container or piece of heart, we'll pass along any assumed heart
|
||||
// count from this location or any of its path locations. This will properly take into account any
|
||||
// heart requirements that were necessary to get here and adjust hint importance accordingly.
|
||||
// (I.e. If Accessing Hyrule Castle requires 10 hearts, and this location has a heart piece, but its
|
||||
// locked by something in Hyrule Castle, then it'll always be not required.)
|
||||
if (importanceItem->IsSameOrSimilarItem(importanceItem->GetWorld()->GetItem("Heart Container"))) {
|
||||
this->_assumedHeartCount = this->_importanceLocation->GetComputedRequirement().getHeartCount();
|
||||
for (auto location : this->_importanceLocation->GetPathLocations()) {
|
||||
this->_assumedHeartCount = std::max(this->_assumedHeartCount, location->GetComputedRequirement().getHeartCount());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Search::SearchWorlds()
|
||||
@@ -224,8 +254,11 @@ namespace randomizer::logic::search
|
||||
|
||||
void Search::ProcessLocation(location::Location* location)
|
||||
{
|
||||
// Don't return if we aren't collecting items
|
||||
if (!this->_collectItems)
|
||||
// Don't return if we aren't collecting items, or if we're doing an importance search
|
||||
// and the item at this location is a similar item to our importance location
|
||||
if (!this->_collectItems ||
|
||||
(this->_searchMode == SearchMode::LOCATION_IMPORTANCE &&
|
||||
this->_importanceLocation->GetCurrentItem()->IsSameOrSimilarItem(location->GetCurrentItem())))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -661,4 +694,26 @@ namespace randomizer::logic::search
|
||||
return search._isBeatable;
|
||||
}
|
||||
|
||||
} // namespace randomizer::logic::search
|
||||
location::LocationPool GetPossibleHintSigns(location::Location* location) {
|
||||
// Remove the item at the location
|
||||
const auto itemAtLocation = location->GetCurrentItem();
|
||||
location->RemoveCurrentItem();
|
||||
|
||||
// Run an accessible search
|
||||
auto search = Search::Accessible(&location->GetWorld()->GetRandomizer()->GetWorlds());
|
||||
search.SearchWorlds();
|
||||
|
||||
// Give the item back
|
||||
location->SetCurrentItem(itemAtLocation);
|
||||
|
||||
// Return all the hint signs that the search found
|
||||
location::LocationPool hintSigns{};
|
||||
for (auto loc : search._visitedLocations) {
|
||||
if (loc->HasCategories("Hint Sign")) {
|
||||
hintSigns.push_back(loc);
|
||||
}
|
||||
}
|
||||
return hintSigns;
|
||||
}
|
||||
|
||||
} // namespace randomizer::logic::search
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "item_pool.hpp"
|
||||
#include "requirement.hpp"
|
||||
#include "location.hpp"
|
||||
#include "../utility/log.hpp"
|
||||
|
||||
#include <unordered_set>
|
||||
@@ -27,11 +29,6 @@ namespace randomizer::logic::item
|
||||
class Item;
|
||||
}
|
||||
|
||||
namespace randomizer::logic::location
|
||||
{
|
||||
class Location;
|
||||
}
|
||||
|
||||
namespace randomizer::logic::area
|
||||
{
|
||||
class EventAccess;
|
||||
@@ -54,7 +51,8 @@ namespace randomizer::logic::search
|
||||
ALL_LOCATIONS_REACHABLE,
|
||||
GENERATE_PLAYTHROUGH,
|
||||
SPHERE_ZERO,
|
||||
TRACKER_SPHERES
|
||||
TRACKER_SPHERES,
|
||||
LOCATION_IMPORTANCE,
|
||||
};
|
||||
|
||||
class Search
|
||||
@@ -65,7 +63,8 @@ namespace randomizer::logic::search
|
||||
world::WorldPool* worlds,
|
||||
const item_pool::ItemPool& items = {},
|
||||
const int& worldToSearch = -1,
|
||||
bool startingInventory = true);
|
||||
bool startingInventory = true,
|
||||
location::Location* importanceLocation = nullptr);
|
||||
|
||||
static auto Accessible(world::WorldPool* worlds,
|
||||
const item_pool::ItemPool& items = {},
|
||||
@@ -109,6 +108,14 @@ namespace randomizer::logic::search
|
||||
return Search(SearchMode::SPHERE_ZERO, worlds, items, worldToSearch);
|
||||
}
|
||||
|
||||
static auto LocationImportance(world::WorldPool* worlds,
|
||||
location::Location* importanceLocation,
|
||||
const item_pool::ItemPool& items = {},
|
||||
const int& worldToSearch = -1)
|
||||
{
|
||||
return Search(SearchMode::LOCATION_IMPORTANCE, worlds, items, worldToSearch, false, importanceLocation);
|
||||
}
|
||||
|
||||
void SearchWorlds();
|
||||
|
||||
/**
|
||||
@@ -159,6 +166,13 @@ namespace randomizer::logic::search
|
||||
std::list<std::list<entrance::Entrance*>> _entranceSpheres;
|
||||
|
||||
std::unordered_map<area::Area*, int> _areaFormTime;
|
||||
|
||||
// Variables used for Location Importance searches
|
||||
std::unordered_multiset<item::Item*> _itemsAtStart{};
|
||||
bool _onlySearchWithItemsAtStart = false;
|
||||
int _assumedHeartCount = 0;
|
||||
requirement::Requirement _assumedFalseReq = requirement::IMPOSSIBLE_REQUIREMENT;
|
||||
location::Location* _importanceLocation = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -173,4 +187,11 @@ namespace randomizer::logic::search
|
||||
const item_pool::ItemPool& items = {});
|
||||
void GeneratePlaythrough(randomizer::Randomizer* randomizer);
|
||||
bool GameBeatable(world::WorldPool* worlds, const item_pool::ItemPool& items = {});
|
||||
|
||||
/**
|
||||
* @brief Returns the pool of hint signs that the passed in location can be hinted at.
|
||||
* We don't want a hint sign to hint a location which is logically required to access
|
||||
* itself, as that is logically useless information.
|
||||
*/
|
||||
location::LocationPool GetPossibleHintSigns(location::Location* location);
|
||||
} // namespace randomizer::logic::search
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../randomizer.hpp"
|
||||
#include "../utility/file.hpp"
|
||||
#include "../utility/platform.hpp"
|
||||
#include "../utility/string.hpp"
|
||||
#include "../utility/yaml.hpp"
|
||||
|
||||
#include <version.h>
|
||||
@@ -245,7 +246,45 @@ namespace randomizer::logic::spoiler_log
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Hints
|
||||
// Print Hints
|
||||
spoilerLog << std::endl << "Hints:" << std::endl;
|
||||
for (const auto& world : worlds) {
|
||||
// Midna hints first
|
||||
spoilerLog << " World " << world->GetID() << ":" << std::endl;
|
||||
spoilerLog << " Midna:" << std::endl;
|
||||
auto midnaText = world->GetText("Custom Midna Call Hints Text");
|
||||
midnaText = utility::str::Replace(midnaText, "\n", "\n ");
|
||||
spoilerLog << " " << midnaText << std::endl;
|
||||
|
||||
spoilerLog << std::endl;
|
||||
// Put sign hints into a structure that will automatically sort them
|
||||
const auto& hintSignHints = world->GetHintSignHints();
|
||||
std::map<std::string, std::list<std::string>> hints{};
|
||||
for (const auto& [hintSign, hintsOnSign] : hintSignHints) {
|
||||
for (const auto& hint : hintsOnSign) {
|
||||
// Format the hints so they appear nice in the spoiler log
|
||||
auto hintText = hint.text.mText[Text::ENGLISH];
|
||||
hintText = utility::str::Replace(hintText, "\n", "\n ");
|
||||
// If this is a path hint, also include the location and item it's referring to
|
||||
if (auto pathHint = std::get_if<hints::PathHint>(&hint.data)) {
|
||||
hintText += " (" + pathHint->hintedLocation->GetName() + ": " + pathHint->hintedLocation->GetCurrentItem()->GetName() + ")";
|
||||
}
|
||||
hints[hintSign->GetName()].push_back(hintText);
|
||||
}
|
||||
}
|
||||
|
||||
// Then print them
|
||||
if (!hints.empty()) {
|
||||
spoilerLog << " Hint Signs:" << std::endl;
|
||||
for (const auto& [signName, hintsOnSign] : hints) {
|
||||
spoilerLog << " " << signName << ":" << std::endl;
|
||||
for (const auto& hint : hintsOnSign) {
|
||||
spoilerLog << " " << hint << std::endl;
|
||||
}
|
||||
}
|
||||
spoilerLog << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// Log Settings
|
||||
LogSettings(spoilerLog, randomizer);
|
||||
|
||||
@@ -181,7 +181,12 @@ namespace randomizer::logic::world
|
||||
}
|
||||
|
||||
auto originalItem = this->GetItem(originalItemName);
|
||||
auto goalLocation = locationNode["Goal Location"].as<bool>(false);
|
||||
bool goalLocation = false;
|
||||
std::string goalName = name;
|
||||
if (locationNode["Goal Name"]) {
|
||||
goalLocation = true;
|
||||
goalName = locationNode["Goal Name"].as<std::string>();
|
||||
}
|
||||
auto hintPriority = locationNode["Hint Priority"].as<std::string>("Never");
|
||||
auto metadata = locationNode["Metadata"];
|
||||
|
||||
@@ -200,6 +205,7 @@ namespace randomizer::logic::world
|
||||
this,
|
||||
originalItem,
|
||||
goalLocation,
|
||||
goalName,
|
||||
hintPriority,
|
||||
metadata);
|
||||
|
||||
@@ -662,7 +668,7 @@ namespace randomizer::logic::world
|
||||
// Some locations not being randomized can conflict with other settings. When
|
||||
// the appropriate location and setting conflict, these locations should have their item
|
||||
// removed and be set to nonprogress.
|
||||
for (auto& [locationName, location] : this->_locationTable)
|
||||
for (auto& location : this->_locationTable | std::views::values)
|
||||
{
|
||||
auto originalItem = location->GetOriginalItem();
|
||||
auto originalItemName = originalItem->GetName();
|
||||
@@ -753,12 +759,12 @@ namespace randomizer::logic::world
|
||||
void World::AssignGoalLocations()
|
||||
{
|
||||
std::unordered_map<std::string, location::LocationPool> dungeonGoalLocations = {};
|
||||
for (const auto& [dungeonName, dungeon] : this->_dungeons)
|
||||
for (const auto& dungeonName : this->_dungeons | std::views::keys)
|
||||
{
|
||||
dungeonGoalLocations[dungeonName] = {};
|
||||
}
|
||||
// Collect all the possible goal locations for each dungeon
|
||||
for (auto& [areaName, area] : this->_areaTable)
|
||||
for (auto& area : this->_areaTable | std::views::values)
|
||||
{
|
||||
for (const auto& locAcc : area->GetLocations())
|
||||
{
|
||||
@@ -1190,7 +1196,7 @@ namespace randomizer::logic::world
|
||||
location::LocationPool World::GetAllLocations(const bool& includeNonItemLocations /* = false */)
|
||||
{
|
||||
location::LocationPool locationPool = {};
|
||||
for (const auto& [locationName, location] : this->_locationTable)
|
||||
for (const auto& location : this->_locationTable | std::views::values)
|
||||
{
|
||||
if (includeNonItemLocations || !location->HasCategories("Non-Item Location"))
|
||||
{
|
||||
@@ -1200,6 +1206,19 @@ namespace randomizer::logic::world
|
||||
return locationPool;
|
||||
}
|
||||
|
||||
location::LocationPool World::GetHintSignLocations()
|
||||
{
|
||||
location::LocationPool locationPool = {};
|
||||
for (const auto& location : this->_locationTable | std::views::values)
|
||||
{
|
||||
if (location->HasCategories("Hint Sign"))
|
||||
{
|
||||
locationPool.emplace_back(location.get());
|
||||
}
|
||||
}
|
||||
return locationPool;
|
||||
}
|
||||
|
||||
area::Area* World::GetArea(const std::string& name, const bool& createIfNotFound /* = false */)
|
||||
{
|
||||
if (!this->_areaTable.contains(name))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "item_pool.hpp"
|
||||
#include "location.hpp"
|
||||
#include "requirement.hpp"
|
||||
#include "hint_types.hpp"
|
||||
|
||||
#include "../seedgen/settings.hpp"
|
||||
#include "../utility/log.hpp"
|
||||
@@ -133,6 +134,7 @@ namespace randomizer::logic::world
|
||||
item_pool::ItemPool& GetStartingItemPool();
|
||||
location::Location* GetLocation(const std::string& name);
|
||||
location::LocationPool GetAllLocations(const bool& includeNonItemLocations = false);
|
||||
location::LocationPool GetHintSignLocations();
|
||||
area::Area* GetArea(const std::string& name, const bool& createIfNotFound = false);
|
||||
area::Area* GetRootArea() const;
|
||||
const std::map<std::string, std::unique_ptr<area::Area>>& GetAreaTable() const;
|
||||
@@ -161,11 +163,28 @@ namespace randomizer::logic::world
|
||||
|
||||
return this->_textDatabase.at(name).at(type).mText.at(Text::ENGLISH);
|
||||
}
|
||||
|
||||
Text& GetTextObject(const std::string& name, Text::Type type = Text::STANDARD) {
|
||||
if (!this->_textDatabase.contains(name)) {
|
||||
throw std::runtime_error(name + " is not a text object in world " + std::to_string(this->_id));
|
||||
}
|
||||
return this->_textDatabase[name][type];
|
||||
}
|
||||
|
||||
// Make a new custom text entry for this world specifically and return a reference to it
|
||||
Text& AddNewText(const std::string& name, Text::Type type = Text::STANDARD) {
|
||||
return this->_textDatabase[name][type];
|
||||
}
|
||||
|
||||
void AddGoalLocation(location::Location* location) {
|
||||
this->_goalLocations.push_back(location);
|
||||
}
|
||||
|
||||
const auto& GetGoalLocations() {return this->_goalLocations;}
|
||||
auto& GetBarrenRegions() { return this->_barrenRegions;}
|
||||
auto& GetMidnaHints() { return this->_midnaHints;}
|
||||
auto& GetHintSignHints() { return this->_hintSignHints;}
|
||||
|
||||
private:
|
||||
int _id = -1;
|
||||
int _entranceIdCounter = 0;
|
||||
@@ -192,6 +211,12 @@ namespace randomizer::logic::world
|
||||
std::unordered_map<location::Location*, item::Item*> _plandomizerLocations = {};
|
||||
std::unordered_map<entrance::Entrance*, entrance::Entrance*> _plandomizerEntrances = {};
|
||||
|
||||
// Hint stuff
|
||||
std::list<location::Location*> _goalLocations = {};
|
||||
std::map<std::string, std::list<location::Location*>> _barrenRegions = {};
|
||||
std::vector<hints::Hint> _midnaHints{};
|
||||
std::unordered_map<location::Location*, std::vector<hints::Hint>> _hintSignHints = {};
|
||||
|
||||
Randomizer* _randomizer = nullptr;
|
||||
};
|
||||
} // namespace randomizer::logic::world
|
||||
|
||||
@@ -136,14 +136,21 @@ namespace randomizer
|
||||
}
|
||||
logic::fill::CacheExitTimeForms(this->_worlds);
|
||||
|
||||
// Flattening isn't used for anything yet, but flattens down the requirements for
|
||||
// each location and entrance into a single statement. This will be useful for hints and could potentially
|
||||
// be used to speed up the fill algorithm (but the fill algorithm is already pretty fast, so we'd only gain maybe like
|
||||
// 0.2 seconds back or something)
|
||||
// Flattens down the requirements for each location and entrance into a single statement.
|
||||
// This is used for calculating hint importance.
|
||||
utility::platform::Log("Flattening...");
|
||||
FlattenSearch search = FlattenSearch(this->_worlds.at(0).get());
|
||||
search.doSearch();
|
||||
|
||||
// Set chain locations once the flatten search is done
|
||||
for (auto& world : this->_worlds) {
|
||||
for (auto& location : world->GetAllLocations()) {
|
||||
for (auto& item : location->GetComputedRequirement().getItems(world.get())) {
|
||||
item->AddChainLocation(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utility::platform::Log("Filling Worlds...");
|
||||
logic::fill::FillWorlds(this->_worlds);
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "settings.hpp"
|
||||
#include "../utility/path.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
// forward declaration
|
||||
namespace YAML
|
||||
{
|
||||
|
||||
@@ -89,4 +89,20 @@ namespace randomizer::utility::str {
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string Replace(const std::string& originalStr,
|
||||
const std::string& oldStr,
|
||||
const std::string& replacementStr,
|
||||
uint32_t count) {
|
||||
std::string retStr = originalStr;
|
||||
auto startPos = retStr.find(oldStr);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (startPos == std::string::npos) {
|
||||
return retStr;
|
||||
}
|
||||
retStr.replace(startPos, oldStr.length(), replacementStr);
|
||||
startPos = retStr.find(oldStr, startPos + replacementStr.length());
|
||||
}
|
||||
return retStr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <iomanip>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
|
||||
namespace randomizer::utility::str {
|
||||
std::string toUTF8(const std::u16string& str);
|
||||
@@ -115,4 +117,8 @@ namespace randomizer::utility::str {
|
||||
}
|
||||
|
||||
std::optional<int> toInt(std::string_view str);
|
||||
std::string Replace(const std::string& originalStr,
|
||||
const std::string& oldStr,
|
||||
const std::string& replacementStr,
|
||||
uint32_t count = std::numeric_limits<uint32_t>::max());
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "text.hpp"
|
||||
|
||||
#include "string.hpp"
|
||||
#include "yaml.hpp"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <ranges>
|
||||
#include <unordered_map>
|
||||
|
||||
|
||||
@@ -15,25 +17,16 @@ namespace randomizer {
|
||||
}
|
||||
}
|
||||
|
||||
void Text::Replace(const std::string& oldStr, const Text& replacementText, int count/* = 1*/) {
|
||||
void Text::Replace(const std::string& oldText, const Text& replacementText, uint32_t count/* = max*/) {
|
||||
for (size_t i = 0; i < mText.size(); ++i) {
|
||||
auto& curString = mText[i];
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (auto startPos = curString.find(oldStr); startPos != std::string::npos) {
|
||||
curString.replace(startPos, oldStr.length(), replacementText.mText[i]);
|
||||
}
|
||||
}
|
||||
curString = utility::str::Replace(curString, oldText, replacementText.mText[i], count);
|
||||
}
|
||||
}
|
||||
|
||||
void Text::Replace(const std::string& oldStr, const std::string& replacementText, int count/* = 1*/) {
|
||||
for (size_t i = 0; i < mText.size(); ++i) {
|
||||
auto& curString = mText[i];
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (auto startPos = curString.find(oldStr); startPos != std::string::npos) {
|
||||
curString.replace(startPos, oldStr.length(), replacementText);
|
||||
}
|
||||
}
|
||||
void Text::Replace(const std::string& oldText, const std::string& replacementText, uint32_t count/* = max*/) {
|
||||
for (auto& text : mText) {
|
||||
text = utility::str::Replace(text, oldText, replacementText, count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +62,17 @@ namespace randomizer {
|
||||
}
|
||||
}
|
||||
|
||||
void Text::PadToNextBox() {
|
||||
BreakLines();
|
||||
for (auto& text : mText) {
|
||||
size_t numNewLines = std::ranges::count_if(text, [](char c){return c == '\n';});
|
||||
while (numNewLines == 0 || text.back() != '\n' || numNewLines % LINES_PER_BOX != 0) {
|
||||
text += '\n';
|
||||
++numNewLines;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Text::Empty() const {
|
||||
for (auto& text : mText) {
|
||||
if (!text.empty()) {
|
||||
@@ -78,6 +82,80 @@ namespace randomizer {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Text::IsTooLong() const {
|
||||
for (auto& text : mText) {
|
||||
auto numNewLines = std::ranges::count_if(text, [](char c){return c == '\n';});
|
||||
if (numNewLines > MAX_NEWLINES_PER_MESSAGE) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Will split this text object into multiple objects short enough to fit into individual
|
||||
// text ids
|
||||
std::vector<Text> Text::SplitToFitTextLimits() {
|
||||
if (this->IsTooLong()) {
|
||||
// Figure out how many new text objects we need to fit all the text
|
||||
size_t numTextObjects{1};
|
||||
for (auto& text : mText) {
|
||||
double numNewLines = std::ranges::count_if(text, [](char c){return c == '\n';});
|
||||
auto curTextSplitAmount = static_cast<size_t>(std::ceil(numNewLines / MAX_NEWLINES_PER_MESSAGE));
|
||||
numTextObjects = std::max(curTextSplitAmount, numTextObjects);
|
||||
}
|
||||
|
||||
std::vector<Text> splitText{numTextObjects};
|
||||
// Split each string into the appropriate number of objects
|
||||
for (size_t textIdx = 0; textIdx < mText.size(); ++textIdx) {
|
||||
auto& textStr = mText[textIdx];
|
||||
// Calculate how many newlines we're allowing in this string per message
|
||||
// Different languages may have different amounts of newlines
|
||||
double numNewLines = std::ranges::count_if(textStr, [](char c){return c == '\n';});
|
||||
auto newLinesPerMessage = static_cast<size_t>(std::ceil(numNewLines / numTextObjects));
|
||||
// Keep the number of lines as a multiple of how many lines are in a box so we don't split in the middle of a textbox
|
||||
while (newLinesPerMessage % LINES_PER_BOX != 0) {
|
||||
++newLinesPerMessage;
|
||||
}
|
||||
|
||||
size_t splitIdx = 0;
|
||||
do {
|
||||
size_t pos = 0;
|
||||
for (int i = 0; i < newLinesPerMessage; ++i) {
|
||||
pos = textStr.find('\n', pos);
|
||||
if (pos == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
|
||||
// Get the current split of the string
|
||||
auto curSplit = textStr.substr(0, pos);
|
||||
// Pop off the last newline since it's unnecessary
|
||||
if (curSplit.back() == '\n') {
|
||||
curSplit.pop_back();
|
||||
}
|
||||
splitText.at(splitIdx).mText[textIdx] = curSplit;
|
||||
++splitIdx;
|
||||
if (pos == std::string::npos) {
|
||||
textStr.clear();
|
||||
} else {
|
||||
textStr = textStr.substr(pos);
|
||||
}
|
||||
} while (!textStr.empty());
|
||||
}
|
||||
|
||||
// Recopy the front element to this object
|
||||
*this = splitText[0];
|
||||
|
||||
// Reassign the vector everything except the first element
|
||||
splitText.assign(splitText.begin() + 1, splitText.end());
|
||||
return splitText;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Text& Text::operator+=(const Text& rhs) {
|
||||
for (size_t i = 0; i < mText.size(); ++i) {
|
||||
mText[i] += rhs.mText[i];
|
||||
@@ -297,7 +375,7 @@ namespace randomizer {
|
||||
return tb.at(name).at(type).mText.at(language);
|
||||
}
|
||||
|
||||
Text addColor(const Text& t, Text::Color color, int count /* = 1*/, bool forceAround /* = false*/) {
|
||||
Text addColor(const Text& t, Text::Color color, int count /* = 1*/) {
|
||||
const static std::unordered_map<Text::Color, std::string> colorStrings = {
|
||||
{Text::WHITE, "<white>"},
|
||||
{Text::RED, "<red>"},
|
||||
@@ -320,11 +398,16 @@ namespace randomizer {
|
||||
}
|
||||
|
||||
Text text = t;
|
||||
if (forceAround) {
|
||||
text = colorStrings.at(color) + text + colorStrings.at(Text::WHITE);
|
||||
for (auto& langText : text.mText) {
|
||||
// If we don't have brackets indicating color, then surround the entire text
|
||||
if (langText.find('{') == std::string::npos && langText.find('}') == std::string::npos) {
|
||||
langText = colorStrings.at(color) + langText + colorStrings.at(Text::WHITE);
|
||||
} else {
|
||||
langText = utility::str::Replace(langText, "{", colorStrings.at(color), count);
|
||||
langText = utility::str::Replace(langText, "}", colorStrings.at(Text::WHITE), count);
|
||||
}
|
||||
}
|
||||
text.Replace("{", colorStrings.at(color), count);
|
||||
text.Replace("}", colorStrings.at(Text::WHITE), count);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -368,7 +451,7 @@ namespace randomizer {
|
||||
|
||||
// Skip over control codes since they don't get displayed
|
||||
std::string code{};
|
||||
for (const auto& [messageCode, replacement] : messageCodes) {
|
||||
for (const auto& messageCode : messageCodes | std::views::keys) {
|
||||
if (str.substr(i, messageCode.length()) == messageCode) {
|
||||
code = messageCode;
|
||||
break;
|
||||
@@ -432,4 +515,58 @@ namespace randomizer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text makeTextListing(std::vector<Text> texts) {
|
||||
if (texts.empty()) {
|
||||
return Text{};
|
||||
}
|
||||
if (texts.size() == 1) {
|
||||
return texts.front();
|
||||
}
|
||||
|
||||
// TODO: Other language listing rules
|
||||
std::string english{};
|
||||
std::string french{};
|
||||
std::string german{};
|
||||
std::string italian{};
|
||||
std::string spanish{};
|
||||
for (int i = 0; i < texts.size(); ++i) {
|
||||
auto& text = texts[i];
|
||||
|
||||
// English rules. Move other languages out of english when we have their rules
|
||||
if (i == 0) {
|
||||
english += text.mText[Text::ENGLISH];
|
||||
french += text.mText[Text::FRENCH];
|
||||
german += text.mText[Text::GERMAN];
|
||||
italian += text.mText[Text::ITALIAN];
|
||||
spanish += text.mText[Text::SPANISH];
|
||||
} else if (i == texts.size() - 1 && texts.size() == 2) {
|
||||
english += " and " + text.mText[Text::ENGLISH];
|
||||
french += " and " + text.mText[Text::FRENCH];
|
||||
german += " and " + text.mText[Text::GERMAN];
|
||||
italian += " and " + text.mText[Text::ITALIAN];
|
||||
spanish += " and " + text.mText[Text::SPANISH];
|
||||
} else if (i == texts.size() - 1) {
|
||||
english += ", and " + text.mText[Text::ENGLISH];
|
||||
french += ", and " + text.mText[Text::FRENCH];
|
||||
german += ", and " + text.mText[Text::GERMAN];
|
||||
italian += ", and " + text.mText[Text::ITALIAN];
|
||||
spanish += ", and " + text.mText[Text::SPANISH];
|
||||
} else {
|
||||
english += ", " + text.mText[Text::ENGLISH];
|
||||
french += ", " + text.mText[Text::FRENCH];
|
||||
german += ", " + text.mText[Text::GERMAN];
|
||||
italian += ", " + text.mText[Text::ITALIAN];
|
||||
spanish += ", " + text.mText[Text::SPANISH];
|
||||
}
|
||||
}
|
||||
|
||||
Text listingText{};
|
||||
listingText.mText[Text::ENGLISH] = english;
|
||||
listingText.mText[Text::FRENCH] = french;
|
||||
listingText.mText[Text::GERMAN] = german;
|
||||
listingText.mText[Text::ITALIAN] = italian;
|
||||
listingText.mText[Text::SPANISH] = spanish;
|
||||
return listingText;
|
||||
}
|
||||
}; // namespace Text
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <unordered_map>
|
||||
#include <limits>
|
||||
|
||||
namespace randomizer {
|
||||
class Text {
|
||||
@@ -59,6 +60,8 @@ namespace randomizer {
|
||||
|
||||
static constexpr size_t MAX_LINE_WIDTH_ITEM_TEXTBOX = 441;
|
||||
static constexpr size_t MAX_LINE_WIDTH_NORMAL_TEXTBOX = 750;
|
||||
static constexpr size_t MAX_NEWLINES_PER_MESSAGE = 40;
|
||||
static constexpr size_t LINES_PER_BOX = 4;
|
||||
|
||||
Text() = default;
|
||||
explicit Text(const std::string& str);
|
||||
@@ -69,15 +72,21 @@ namespace randomizer {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param oldStr the string to replace
|
||||
* @param oldText the string to replace
|
||||
* @param replacementText the Text object to replace the old string
|
||||
* @param count the number of occurrences to replace
|
||||
*/
|
||||
void Replace(const std::string& oldStr, const Text& replacementText, int count = 1);
|
||||
void Replace(const std::string& oldStr, const std::string& replacementText, int count = 1);
|
||||
void Replace(const std::string& oldText, const Text& replacementText, uint32_t count = std::numeric_limits<uint32_t>::max());
|
||||
void Replace(const std::string& oldText, const std::string& replacementText, uint32_t count = std::numeric_limits<uint32_t>::max());
|
||||
void BreakLines(int maxLineWidth = MAX_LINE_WIDTH_NORMAL_TEXTBOX);
|
||||
|
||||
// Inserts newlines to pad the text to the next box
|
||||
void PadToNextBox();
|
||||
void Capitalize();
|
||||
bool Empty() const;
|
||||
bool IsTooLong() const;
|
||||
std::vector<Text> SplitToFitTextLimits();
|
||||
bool operator==(const Text& t) const = default;
|
||||
Text& operator+=(const Text& rhs);
|
||||
Text& operator+=(const std::string& rhs);
|
||||
friend Text operator+(Text lhs, Text& rhs);
|
||||
@@ -112,12 +121,13 @@ namespace randomizer {
|
||||
const Text& getTextObject(const std::string& name, Text::Type type = Text::STANDARD);
|
||||
const std::string& getTextStr(const std::string& name, Text::Type type = Text::STANDARD, Text::Language language = Text::ENGLISH);
|
||||
|
||||
|
||||
Text addColor(const Text& text, Text::Color color, int count = 1, bool forceAround = false);
|
||||
Text addColor(const Text& t, Text::Color color, int count = 1);
|
||||
|
||||
// Adds newlines in appropriate places to properly break the text string for textboxes
|
||||
void breakLines(std::string& str, int maxLineWidth);
|
||||
|
||||
// Replaces the message codes in the string with the ingame hex equivalents
|
||||
void applyMessageCodes(std::string&);
|
||||
|
||||
Text makeTextListing(std::vector<Text> texts);
|
||||
}; // namespace Text
|
||||
|
||||
@@ -174,6 +174,59 @@ SelectButton& rando_config_button(
|
||||
return button;
|
||||
}
|
||||
|
||||
SelectButton& rando_config_number_button(
|
||||
Pane& leftPane, Pane& rightPane, std::string settingKey) {
|
||||
auto setting = FindSetting(settingKey);
|
||||
|
||||
// Helper function to call when we want to update the right pane
|
||||
auto updateRightPane = [setting, &rightPane] {
|
||||
rightPane.clear();
|
||||
auto info = setting->GetInfo();
|
||||
// Show all options/descriptions
|
||||
rightPane.add_text(info->GetDescriptions()[0]);
|
||||
};
|
||||
|
||||
// Helper function for changing the setting index based on button presses
|
||||
auto changeOptionIndex = [setting, updateRightPane](int change) {
|
||||
auto newIndex = setting->GetCurrentOptionIndex() + change;
|
||||
if (newIndex < 0) {
|
||||
newIndex = setting->GetInfo()->GetOptions().size() - 1;
|
||||
} else if (newIndex >= setting->GetInfo()->GetOptions().size()) {
|
||||
newIndex = 0;
|
||||
}
|
||||
setting->SetCurrentOption(newIndex);
|
||||
SaveRandomizerConfig();
|
||||
updateRightPane();
|
||||
};
|
||||
|
||||
auto& button = leftPane.add_select_button(ControlledSelectButton::Props{
|
||||
.key = settingKey,
|
||||
.getValue = [setting] { return setting->GetCurrentOption(); }
|
||||
})
|
||||
// Cycle through the options forward when the button is pressed
|
||||
.on_pressed([changeOptionIndex] {
|
||||
changeOptionIndex(1);
|
||||
});
|
||||
|
||||
// Get button component
|
||||
auto& comp = leftPane.register_control(button, rightPane, [updateRightPane](Pane&) {
|
||||
updateRightPane();
|
||||
});
|
||||
// Listen for left/right nav commands to cycle between the available options
|
||||
comp.listen(comp.root(), Rml::EventId::Keydown, [changeOptionIndex](Rml::Event& event) {
|
||||
auto cmd = map_nav_event(event);
|
||||
if (cmd == NavCommand::Left) {
|
||||
changeOptionIndex(-1);
|
||||
event.StopPropagation();
|
||||
} else if (cmd == NavCommand::Right) {
|
||||
changeOptionIndex(1);
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
NumberButton* rando_add_optional_setting(std::string optionValue, std::string optionsKeyPrefix,
|
||||
Pane& pane) {
|
||||
std::string fullOptionalKey = fmt::format("{} {}", optionsKeyPrefix, optionValue);
|
||||
@@ -1012,6 +1065,32 @@ RandomizerWindow::RandomizerWindow(dFile_select_c* fileSelect /*= nullptr*/) : m
|
||||
rando_config_button(leftPane, rightPane, "Ball and Chain Webs");
|
||||
});
|
||||
|
||||
add_tab("Hints", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
leftPane.add_section("Path Hints");
|
||||
rando_config_number_button(leftPane, rightPane, "Number of Path Hints");
|
||||
rando_config_button(leftPane, rightPane, "Path Hints on Midna");
|
||||
rando_config_button(leftPane, rightPane, "Path Hints on Hint Signs");
|
||||
|
||||
leftPane.add_section("Barren Hints");
|
||||
rando_config_number_button(leftPane, rightPane, "Number of Barren Hints");
|
||||
rando_config_button(leftPane, rightPane, "Barren Hints on Midna");
|
||||
rando_config_button(leftPane, rightPane, "Barren Hints on Hint Signs");
|
||||
|
||||
leftPane.add_section("Item Hints");
|
||||
rando_config_number_button(leftPane, rightPane, "Number of Item Hints");
|
||||
rando_config_button(leftPane, rightPane, "Item Hints on Midna");
|
||||
rando_config_button(leftPane, rightPane, "Item Hints on Hint Signs");
|
||||
|
||||
leftPane.add_section("Location Hints");
|
||||
rando_config_number_button(leftPane, rightPane, "Number of Location Hints");
|
||||
rando_config_button(leftPane, rightPane, "Location Hints on Midna");
|
||||
rando_config_button(leftPane, rightPane, "Location Hints on Hint Signs");
|
||||
rando_config_button(leftPane, rightPane, "Prioritize Remote Location Hints");
|
||||
});
|
||||
|
||||
add_tab("Starting Inventory", [this](Rml::Element* content) {
|
||||
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
|
||||
Reference in New Issue
Block a user