mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-17 04:53:46 -07:00
Compare commits
15 Commits
0.6.2-rc1
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
453d89460f | ||
|
|
28889e58aa | ||
|
|
f3c76399e0 | ||
|
|
8384a23fe2 | ||
|
|
bcd7d62d0b | ||
|
|
703f5a22fd | ||
|
|
1ee8e339af | ||
|
|
dffde64079 | ||
|
|
17bc184e28 | ||
|
|
0ba9ee0695 | ||
|
|
c40214e20f | ||
|
|
a3aac3d737 | ||
|
|
7bbe62019a | ||
|
|
b898b9d9e6 | ||
|
|
b217372fea |
@@ -84,12 +84,16 @@ def browse_files():
|
||||
def open_folder(folder_path):
|
||||
if is_linux:
|
||||
exe = which('xdg-open') or which('gnome-open') or which('kde-open')
|
||||
subprocess.Popen([exe, folder_path])
|
||||
elif is_macos:
|
||||
exe = which("open")
|
||||
subprocess.Popen([exe, folder_path])
|
||||
else:
|
||||
webbrowser.open(folder_path)
|
||||
return
|
||||
|
||||
if exe:
|
||||
subprocess.Popen([exe, folder_path])
|
||||
else:
|
||||
logging.warning(f"No file browser available to open {folder_path}")
|
||||
|
||||
|
||||
def update_settings():
|
||||
|
||||
@@ -33,7 +33,7 @@ from worlds.ladx.TrackerConsts import storage_key
|
||||
from worlds.ladx.ItemTracker import ItemTracker
|
||||
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
|
||||
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
|
||||
from worlds.ladx.Tracker import LocationTracker, MagpieBridge
|
||||
from worlds.ladx.Tracker import LocationTracker, MagpieBridge, Check
|
||||
|
||||
|
||||
class GameboyException(Exception):
|
||||
@@ -626,6 +626,11 @@ class LinksAwakeningContext(CommonContext):
|
||||
"password": self.password,
|
||||
})
|
||||
|
||||
# We can process linked items on already-checked checks now that we have slot_data
|
||||
if self.client.tracker:
|
||||
checked_checks = set(self.client.tracker.all_checks) - set(self.client.tracker.remaining_checks)
|
||||
self.add_linked_items(checked_checks)
|
||||
|
||||
# TODO - use watcher_event
|
||||
if cmd == "ReceivedItems":
|
||||
for index, item in enumerate(args["items"], start=args["index"]):
|
||||
@@ -641,6 +646,13 @@ class LinksAwakeningContext(CommonContext):
|
||||
sync_msg = [{'cmd': 'Sync'}]
|
||||
await self.send_msgs(sync_msg)
|
||||
|
||||
def add_linked_items(self, checks: typing.List[Check]):
|
||||
for check in checks:
|
||||
if check.value and check.linkedItem:
|
||||
linkedItem = check.linkedItem
|
||||
if 'condition' not in linkedItem or (self.slot_data and linkedItem['condition'](self.slot_data)):
|
||||
self.client.item_tracker.setExtraItem(check.linkedItem['item'], check.linkedItem['qty'])
|
||||
|
||||
item_id_lookup = get_locations_to_id()
|
||||
|
||||
async def run_game_loop(self):
|
||||
@@ -649,11 +661,7 @@ class LinksAwakeningContext(CommonContext):
|
||||
checkMetadataTable[check.id])] for check in ladxr_checks]
|
||||
self.new_checks(checks, [check.id for check in ladxr_checks])
|
||||
|
||||
for check in ladxr_checks:
|
||||
if check.value and check.linkedItem:
|
||||
linkedItem = check.linkedItem
|
||||
if 'condition' not in linkedItem or linkedItem['condition'](self.slot_data):
|
||||
self.client.item_tracker.setExtraItem(check.linkedItem['item'], check.linkedItem['qty'])
|
||||
self.add_linked_items(ladxr_checks)
|
||||
|
||||
async def victory():
|
||||
await self.send_victory()
|
||||
|
||||
@@ -1353,6 +1353,7 @@ class StartInventory(ItemDict):
|
||||
verify_item_name = True
|
||||
display_name = "Start Inventory"
|
||||
rich_text_doc = True
|
||||
max = 10000
|
||||
|
||||
|
||||
class StartInventoryPool(StartInventory):
|
||||
|
||||
6
Utils.py
6
Utils.py
@@ -635,6 +635,8 @@ def get_fuzzy_results(input_word: str, word_list: typing.Collection[str], limit:
|
||||
import jellyfish
|
||||
|
||||
def get_fuzzy_ratio(word1: str, word2: str) -> float:
|
||||
if word1 == word2:
|
||||
return 1.01
|
||||
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
|
||||
/ max(len(word1), len(word2)))
|
||||
|
||||
@@ -655,8 +657,10 @@ def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bo
|
||||
picks = get_fuzzy_results(input_text, possible_answers, limit=2)
|
||||
if len(picks) > 1:
|
||||
dif = picks[0][1] - picks[1][1]
|
||||
if picks[0][1] == 100:
|
||||
if picks[0][1] == 101:
|
||||
return picks[0][0], True, "Perfect Match"
|
||||
elif picks[0][1] == 100:
|
||||
return picks[0][0], True, "Case Insensitive Perfect Match"
|
||||
elif picks[0][1] < 75:
|
||||
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
|
||||
@@ -8,7 +8,11 @@ including [Contributing](contributing.md), [Adding Games](<adding games.md>), an
|
||||
|
||||
### My game has a restrictive start that leads to fill errors
|
||||
|
||||
Hint to the Generator that an item needs to be in sphere one with local_early_items. Here, `1` represents the number of "Sword" items to attempt to place in sphere one.
|
||||
A "restrictive start" here means having a combination of very few sphere 1 locations and potentially requiring more
|
||||
than one item to get a player to sphere 2.
|
||||
|
||||
One way to fix this is to hint to the Generator that an item needs to be in sphere one with local_early_items.
|
||||
Here, `1` represents the number of "Sword" items the Generator will attempt to place in sphere one.
|
||||
```py
|
||||
early_item_name = "Sword"
|
||||
self.multiworld.local_early_items[self.player][early_item_name] = 1
|
||||
@@ -18,15 +22,19 @@ Some alternative ways to try to fix this problem are:
|
||||
* Add more locations to sphere one of your world, potentially only when there would be a restrictive start
|
||||
* Pre-place items yourself, such as during `create_items`
|
||||
* Put items into the player's starting inventory using `push_precollected`
|
||||
* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a restrictive start
|
||||
* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a
|
||||
restrictive start
|
||||
|
||||
---
|
||||
|
||||
### I have multiple settings that change the item/location pool counts and need to balance them out
|
||||
### I have multiple options that change the item/location pool counts and need to make sure I am not submitting more/fewer items than locations
|
||||
|
||||
In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be unbalanced. But in real, complex situations, that might be unfeasible.
|
||||
In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be
|
||||
unbalanced. But in real, complex situations, that might be unfeasible.
|
||||
|
||||
If that's the case, you can create extra filler based on the difference between your unfilled locations and your itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations) to your list of items to submit
|
||||
If that's the case, you can create extra filler based on the difference between your unfilled locations and your
|
||||
itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations)
|
||||
to your list of items to submit
|
||||
|
||||
Note: to use self.create_filler(), self.get_filler_item_name() should be defined to only return valid filler item names
|
||||
```py
|
||||
@@ -39,7 +47,8 @@ for _ in range(total_locations - len(item_pool)):
|
||||
self.multiworld.itempool += item_pool
|
||||
```
|
||||
|
||||
A faster alternative to the `for` loop would be to use a [list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions):
|
||||
A faster alternative to the `for` loop would be to use a
|
||||
[list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions):
|
||||
```py
|
||||
item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))]
|
||||
```
|
||||
@@ -48,24 +57,39 @@ item_pool += [self.create_filler() for _ in range(total_locations - len(item_poo
|
||||
|
||||
### I learned about indirect conditions in the world API document, but I want to know more. What are they and why are they necessary?
|
||||
|
||||
The world API document mentions how to use `multiworld.register_indirect_condition` to register indirect conditions and **when** you should use them, but not *how* they work and *why* they are necessary. This is because the explanation is quite complicated.
|
||||
The world API document mentions how to use `multiworld.register_indirect_condition` to register indirect conditions and
|
||||
**when** you should use them, but not *how* they work and *why* they are necessary. This is because the explanation is
|
||||
quite complicated.
|
||||
|
||||
Region sweep (the algorithm that determines which regions are reachable) is a Breadth-First Search of the region graph. It starts from the origin region, checks entrances one by one, and adds newly reached regions and their entrances to the queue until there is nothing more to check.
|
||||
Region sweep (the algorithm that determines which regions are reachable) is a Breadth-First Search of the region graph.
|
||||
It starts from the origin region, checks entrances one by one, and adds newly reached regions and their entrances to
|
||||
the queue until there is nothing more to check.
|
||||
|
||||
For performance reasons, AP only checks every entrance once. However, if an entrance's access_rule depends on region access, then the following may happen:
|
||||
1. The entrance is checked and determined to be nontraversable because the region in its access_rule hasn't been reached yet during the graph search.
|
||||
For performance reasons, AP only checks every entrance once. However, if an entrance's access_rule depends on region
|
||||
access, then the following may happen:
|
||||
1. The entrance is checked and determined to be nontraversable because the region in its access_rule hasn't been
|
||||
reached yet during the graph search.
|
||||
2. Then, the region in its access_rule is determined to be reachable.
|
||||
|
||||
This entrance *would* be in logic if it were rechecked, but it won't be rechecked this cycle.
|
||||
To account for this case, AP would have to recheck all entrances every time a new region is reached until no new regions are reached.
|
||||
To account for this case, AP would have to recheck all entrances every time a new region is reached until no new
|
||||
regions are reached.
|
||||
|
||||
An indirect condition is how you can manually define that a specific entrance needs to be rechecked during region sweep if a specific region is reached during it.
|
||||
This keeps most of the performance upsides. Even in a game making heavy use of indirect conditions (ex: The Witness), using them is significantly faster than just "rechecking each entrance until nothing new is found".
|
||||
The reason entrance access rules using `location.can_reach` and `entrance.can_reach` are also affected is because they call `region.can_reach` on their respective parent/source region.
|
||||
An indirect condition is how you can manually define that a specific entrance needs to be rechecked during region sweep
|
||||
if a specific region is reached during it.
|
||||
This keeps most of the performance upsides. Even in a game making heavy use of indirect conditions (ex: The Witness),
|
||||
using them is significantly faster than just "rechecking each entrance until nothing new is found".
|
||||
The reason entrance access rules using `location.can_reach` and `entrance.can_reach` are also affected is because they
|
||||
call `region.can_reach` on their respective parent/source region.
|
||||
|
||||
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition, and that some games have very complex access rules.
|
||||
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682) being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of checking each entrance whenever a region has been reached, although this does come with a performance cost.
|
||||
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should be reasonable to know all entrance → region dependencies, making indirect conditions preferred because they are much faster.
|
||||
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition,
|
||||
and that some games have very complex access rules.
|
||||
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682)
|
||||
being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of
|
||||
checking each entrance whenever a region has been reached, although this does come with a performance cost.
|
||||
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should
|
||||
be reasonable to know all entrance → region dependencies, making indirect conditions preferred because they are
|
||||
much faster.
|
||||
|
||||
---
|
||||
|
||||
@@ -85,3 +109,16 @@ Common situations where this can happen include:
|
||||
Also, consider using the `options.as_dict("option_name", "option_two")` helper.
|
||||
* Using enums as Location/Item names in the datapackage. When building out `location_name_to_id` and `item_name_to_id`,
|
||||
make sure that you are not using your enum class for either the names or ids in these mappings.
|
||||
|
||||
---
|
||||
|
||||
### Some locations are technically possible to check with few or no items, but they'd be very tedious or frustrating. How do worlds deal with this?
|
||||
|
||||
Sometimes the game can be modded to skip these locations or make them less tedious. But when this issue is due to a fundamental aspect of the game, then the general answer is "soft logic" (and its subtypes like "combat logic", "money logic", etc.). For example: you can logically require that a player have several helpful items before fighting the final boss, even if a skilled player technically needs no items to beat it. Randomizer logic should describe what's *fun* rather than what's technically possible.
|
||||
|
||||
Concrete examples of soft logic include:
|
||||
- Defeating a boss might logically require health upgrades, damage upgrades, certain weapons, etc. that aren't strictly necessary.
|
||||
- Entering a high-level area might logically require access to enough other parts of the game that checking other locations should naturally get the player to the soft-required level.
|
||||
- Buying expensive shop items might logically require access to a place where you can quickly farm money, or logically require access to enough parts of the game that checking other locations should naturally generate enough money without grinding.
|
||||
|
||||
Remember that all items referenced by logic (however hard or soft) must be `progression`. Since you typically don't want to turn a ton of `filler` items into `progression` just for this, it's common to e.g. write money logic using only the rare "$100" item, so the dozens of "$1" and "$10" items in your world can remain `filler`.
|
||||
|
||||
@@ -151,8 +151,7 @@ class ItemTracker:
|
||||
def __init__(self, gameboy) -> None:
|
||||
self.gameboy = gameboy
|
||||
self.loadItems()
|
||||
pass
|
||||
extraItems = {}
|
||||
self.extraItems = {}
|
||||
|
||||
async def readRamByte(self, byte):
|
||||
return (await self.gameboy.read_memory_cache([byte]))[byte]
|
||||
|
||||
@@ -23,21 +23,12 @@ These steps can also be followed to launch the game and check for mod updates af
|
||||
### Manual Installation
|
||||
|
||||
1. Download and install Courier Mod Loader using the instructions on the release page
|
||||
* [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases)
|
||||
* [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases)
|
||||
2. Download and install the randomizer mod
|
||||
1. Download the latest TheMessengerRandomizerAP.zip from
|
||||
[The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases)
|
||||
2. Extract the zip file to `TheMessenger/Mods/` of your game's install location
|
||||
* You cannot have both the non-AP randomizer and the AP randomizer installed at the same time
|
||||
3. Optionally, Backup your save game
|
||||
* On Windows
|
||||
1. Press `Windows Key + R` to open run
|
||||
2. Type `%appdata%` to access AppData
|
||||
3. Navigate to `AppData/locallow/SabotageStudios/The Messenger`
|
||||
4. Rename `SaveGame.txt` to any name of your choice
|
||||
* On Linux
|
||||
1. Navigate to `steamapps/compatdata/764790/pfx/drive_c/users/steamuser/AppData/LocalLow/Sabotage Studio/The Messenger`
|
||||
2. Rename `SaveGame.txt` to any name of your choice
|
||||
1. Download the latest TheMessengerRandomizerAP.zip from
|
||||
[The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases)
|
||||
2. Extract the zip file to `TheMessenger/Mods/` of your game's install location
|
||||
* You cannot have both the non-AP randomizer and the AP randomizer installed at the same time
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
@@ -57,15 +48,15 @@ These steps can also be followed to launch the game and check for mod updates af
|
||||
1. Launch the game
|
||||
2. Navigate to `Options > Archipelago Options`
|
||||
3. Enter connection info using the relevant option buttons
|
||||
* **The game is limited to alphanumerical characters, `.`, and `-`.**
|
||||
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
|
||||
website.
|
||||
* If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game
|
||||
directory. When using this, all connection information must be entered in the file.
|
||||
* **The game is limited to alphanumerical characters, `.`, and `-`.**
|
||||
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
|
||||
website.
|
||||
* If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game
|
||||
directory. When using this, all connection information must be entered in the file.
|
||||
4. Select the `Connect to Archipelago` button
|
||||
5. Navigate to save file selection
|
||||
6. Start a new game
|
||||
* If you're already connected, deleting an existing save will not disconnect you and is completely safe.
|
||||
* If you're already connected, deleting an existing save will not disconnect you and is completely safe.
|
||||
|
||||
## Continuing a MultiWorld Game
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ chunksanity_starting_chunks: typing.List[str] = [
|
||||
ItemNames.South_Of_Varrock,
|
||||
ItemNames.Central_Varrock,
|
||||
ItemNames.Varrock_Palace,
|
||||
ItemNames.East_Of_Varrock,
|
||||
ItemNames.Lumberyard,
|
||||
ItemNames.West_Varrock,
|
||||
ItemNames.Edgeville,
|
||||
ItemNames.Barbarian_Village,
|
||||
|
||||
@@ -8,7 +8,9 @@ import requests
|
||||
# The CSVs are updated at this repository to be shared between generator and client.
|
||||
data_repository_address = "https://raw.githubusercontent.com/digiholic/osrs-archipelago-logic/"
|
||||
# The Github tag of the CSVs this was generated with
|
||||
data_csv_tag = "v1.5"
|
||||
data_csv_tag = "v2.0.4"
|
||||
# If true, generate using file names in the repository
|
||||
debug = False
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
@@ -26,98 +28,167 @@ if __name__ == "__main__":
|
||||
def load_location_csv():
|
||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
with open(os.path.join(this_dir, "locations_generated.py"), 'w+') as locPyFile:
|
||||
locPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
locPyFile.write("from ..Locations import LocationRow, SkillRequirement\n")
|
||||
locPyFile.write("\n")
|
||||
locPyFile.write("location_rows = [\n")
|
||||
with open(os.path.join(this_dir, "locations_generated.py"), 'w+') as loc_py_file:
|
||||
loc_py_file.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
loc_py_file.write("from ..Locations import LocationRow, SkillRequirement\n")
|
||||
loc_py_file.write("\n")
|
||||
loc_py_file.write("location_rows = [\n")
|
||||
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/locations.csv") as req:
|
||||
locations_reader = csv.reader(req.text.splitlines())
|
||||
for row in locations_reader:
|
||||
row_line = "LocationRow("
|
||||
row_line += str_format(row[0])
|
||||
row_line += str_format(row[1].lower())
|
||||
if debug:
|
||||
with open(os.path.join(this_dir, "locations.csv"), "r") as loc_file:
|
||||
locations_reader = csv.reader(loc_file.read().splitlines())
|
||||
parse_loc_file(loc_py_file, locations_reader)
|
||||
else:
|
||||
print("Loading: " + data_repository_address + "/" + data_csv_tag + "/locations.csv")
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/locations.csv") as req:
|
||||
if req.status_code == 200:
|
||||
locations_reader = csv.reader(req.text.splitlines())
|
||||
parse_loc_file(loc_py_file, locations_reader)
|
||||
else:
|
||||
print(str(req.status_code) + ": " + req.reason)
|
||||
loc_py_file.write("]\n")
|
||||
|
||||
region_strings = row[2].split(", ") if row[2] else []
|
||||
row_line += f"{str_list_to_py(region_strings)}, "
|
||||
|
||||
skill_strings = row[3].split(", ")
|
||||
row_line += "["
|
||||
if skill_strings:
|
||||
split_skills = [skill.split(" ") for skill in skill_strings if skill != ""]
|
||||
if split_skills:
|
||||
for split in split_skills:
|
||||
row_line += f"SkillRequirement('{split[0]}', {split[1]}), "
|
||||
row_line += "], "
|
||||
def parse_loc_file(loc_py_file, locations_reader):
|
||||
for row in locations_reader:
|
||||
# Skip the header row, if present
|
||||
if row[0] == "Location Name":
|
||||
continue
|
||||
row_line = "LocationRow("
|
||||
row_line += str_format(row[0])
|
||||
row_line += str_format(row[1].lower())
|
||||
|
||||
region_strings = row[2].split(", ") if row[2] else []
|
||||
row_line += f"{str_list_to_py(region_strings)}, "
|
||||
|
||||
skill_strings = row[3].split(", ")
|
||||
row_line += "["
|
||||
if skill_strings:
|
||||
split_skills = [skill.split(" ") for skill in skill_strings if skill != ""]
|
||||
if split_skills:
|
||||
for split in split_skills:
|
||||
row_line += f"SkillRequirement('{split[0]}', {split[1]}), "
|
||||
row_line += "], "
|
||||
|
||||
item_strings = row[4].split(", ") if row[4] else []
|
||||
row_line += f"{str_list_to_py(item_strings)}, "
|
||||
row_line += f"{row[5]})" if row[5] != "" else "0)"
|
||||
loc_py_file.write(f"\t{row_line},\n")
|
||||
|
||||
item_strings = row[4].split(", ") if row[4] else []
|
||||
row_line += f"{str_list_to_py(item_strings)}, "
|
||||
row_line += f"{row[5]})" if row[5] != "" else "0)"
|
||||
locPyFile.write(f"\t{row_line},\n")
|
||||
locPyFile.write("]\n")
|
||||
|
||||
def load_region_csv():
|
||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
with open(os.path.join(this_dir, "regions_generated.py"), 'w+') as regPyFile:
|
||||
regPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
regPyFile.write("from ..Regions import RegionRow\n")
|
||||
regPyFile.write("\n")
|
||||
regPyFile.write("region_rows = [\n")
|
||||
with open(os.path.join(this_dir, "regions_generated.py"), 'w+') as reg_py_file:
|
||||
reg_py_file.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
reg_py_file.write("from ..Regions import RegionRow\n")
|
||||
reg_py_file.write("\n")
|
||||
reg_py_file.write("region_rows = [\n")
|
||||
|
||||
if debug:
|
||||
with open(os.path.join(this_dir, "regions.csv"), "r") as region_file:
|
||||
regions_reader = csv.reader(region_file.read().splitlines())
|
||||
parse_region_file(reg_py_file, regions_reader)
|
||||
else:
|
||||
print("Loading: "+ data_repository_address + "/" + data_csv_tag + "/regions.csv")
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/regions.csv") as req:
|
||||
if req.status_code == 200:
|
||||
regions_reader = csv.reader(req.text.splitlines())
|
||||
parse_region_file(reg_py_file, regions_reader)
|
||||
else:
|
||||
print(str(req.status_code) + ": " + req.reason)
|
||||
reg_py_file.write("]\n")
|
||||
|
||||
|
||||
def parse_region_file(reg_py_file, regions_reader):
|
||||
for row in regions_reader:
|
||||
# Skip the header row, if present
|
||||
if row[0] == "Region Name":
|
||||
continue
|
||||
|
||||
row_line = "RegionRow("
|
||||
row_line += str_format(row[0])
|
||||
row_line += str_format(row[1])
|
||||
connections = row[2]
|
||||
row_line += f"{str_list_to_py(connections.split(', '))}, "
|
||||
resources = row[3]
|
||||
row_line += f"{str_list_to_py(resources.split(', '))})"
|
||||
reg_py_file.write(f"\t{row_line},\n")
|
||||
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/regions.csv") as req:
|
||||
regions_reader = csv.reader(req.text.splitlines())
|
||||
for row in regions_reader:
|
||||
row_line = "RegionRow("
|
||||
row_line += str_format(row[0])
|
||||
row_line += str_format(row[1])
|
||||
connections = row[2].replace("'", "\\'")
|
||||
row_line += f"{str_list_to_py(connections.split(', '))}, "
|
||||
resources = row[3].replace("'", "\\'")
|
||||
row_line += f"{str_list_to_py(resources.split(', '))})"
|
||||
regPyFile.write(f"\t{row_line},\n")
|
||||
regPyFile.write("]\n")
|
||||
|
||||
def load_resource_csv():
|
||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
with open(os.path.join(this_dir, "resources_generated.py"), 'w+') as resPyFile:
|
||||
resPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
resPyFile.write("from ..Regions import ResourceRow\n")
|
||||
resPyFile.write("\n")
|
||||
resPyFile.write("resource_rows = [\n")
|
||||
with open(os.path.join(this_dir, "resources_generated.py"), 'w+') as res_py_file:
|
||||
res_py_file.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
res_py_file.write("from ..Regions import ResourceRow\n")
|
||||
res_py_file.write("\n")
|
||||
res_py_file.write("resource_rows = [\n")
|
||||
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/resources.csv") as req:
|
||||
resource_reader = csv.reader(req.text.splitlines())
|
||||
for row in resource_reader:
|
||||
name = row[0].replace("'", "\\'")
|
||||
row_line = f"ResourceRow('{name}')"
|
||||
resPyFile.write(f"\t{row_line},\n")
|
||||
resPyFile.write("]\n")
|
||||
if debug:
|
||||
with open(os.path.join(this_dir, "resources.csv"), "r") as region_file:
|
||||
regions_reader = csv.reader(region_file.read().splitlines())
|
||||
parse_resources_file(res_py_file, regions_reader)
|
||||
else:
|
||||
print("Loading: " + data_repository_address + "/" + data_csv_tag + "/resources.csv")
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/resources.csv") as req:
|
||||
if req.status_code == 200:
|
||||
resource_reader = csv.reader(req.text.splitlines())
|
||||
parse_resources_file(res_py_file, resource_reader)
|
||||
else:
|
||||
print(str(req.status_code) + ": " + req.reason)
|
||||
res_py_file.write("]\n")
|
||||
|
||||
|
||||
def parse_resources_file(res_py_file, resource_reader):
|
||||
for row in resource_reader:
|
||||
# Skip the header row, if present
|
||||
if row[0] == "Resource Name":
|
||||
continue
|
||||
|
||||
name = row[0].replace("'", "\\'")
|
||||
row_line = f"ResourceRow('{name}')"
|
||||
res_py_file.write(f"\t{row_line},\n")
|
||||
|
||||
|
||||
def load_item_csv():
|
||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
with open(os.path.join(this_dir, "items_generated.py"), 'w+') as itemPyfile:
|
||||
itemPyfile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
itemPyfile.write("from BaseClasses import ItemClassification\n")
|
||||
itemPyfile.write("from ..Items import ItemRow\n")
|
||||
itemPyfile.write("\n")
|
||||
itemPyfile.write("item_rows = [\n")
|
||||
with open(os.path.join(this_dir, "items_generated.py"), 'w+') as item_py_file:
|
||||
item_py_file.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
item_py_file.write("from BaseClasses import ItemClassification\n")
|
||||
item_py_file.write("from ..Items import ItemRow\n")
|
||||
item_py_file.write("\n")
|
||||
item_py_file.write("item_rows = [\n")
|
||||
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/items.csv") as req:
|
||||
item_reader = csv.reader(req.text.splitlines())
|
||||
for row in item_reader:
|
||||
row_line = "ItemRow("
|
||||
row_line += str_format(row[0])
|
||||
row_line += f"{row[1]}, "
|
||||
if debug:
|
||||
with open(os.path.join(this_dir, "items.csv"), "r") as region_file:
|
||||
regions_reader = csv.reader(region_file.read().splitlines())
|
||||
parse_item_file(item_py_file, regions_reader)
|
||||
else:
|
||||
print("Loading: " + data_repository_address + "/" + data_csv_tag + "/items.csv")
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/items.csv") as req:
|
||||
if req.status_code == 200:
|
||||
item_reader = csv.reader(req.text.splitlines())
|
||||
parse_item_file(item_py_file, item_reader)
|
||||
else:
|
||||
print(str(req.status_code) + ": " + req.reason)
|
||||
item_py_file.write("]\n")
|
||||
|
||||
row_line += f"ItemClassification.{row[2]})"
|
||||
|
||||
itemPyfile.write(f"\t{row_line},\n")
|
||||
itemPyfile.write("]\n")
|
||||
def parse_item_file(item_py_file, item_reader):
|
||||
for row in item_reader:
|
||||
# Skip the header row, if present
|
||||
if row[0] == "Name":
|
||||
continue
|
||||
|
||||
row_line = "ItemRow("
|
||||
row_line += str_format(row[0])
|
||||
row_line += f"{row[1]}, "
|
||||
|
||||
row_line += f"ItemClassification.{row[2]})"
|
||||
|
||||
item_py_file.write(f"\t{row_line},\n")
|
||||
|
||||
|
||||
def str_format(s) -> str:
|
||||
@@ -128,7 +199,7 @@ if __name__ == "__main__":
|
||||
def str_list_to_py(str_list) -> str:
|
||||
ret_str = "["
|
||||
for s in str_list:
|
||||
ret_str += f"'{s}', "
|
||||
ret_str += str_format(s)
|
||||
ret_str += "]"
|
||||
return ret_str
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ item_rows = [
|
||||
ItemRow('Area: HAM Hideout', 1, ItemClassification.progression),
|
||||
ItemRow('Area: Lumbridge Farms', 1, ItemClassification.progression),
|
||||
ItemRow('Area: South of Varrock', 1, ItemClassification.progression),
|
||||
ItemRow('Area: East Varrock', 1, ItemClassification.progression),
|
||||
ItemRow('Area: Lumberyard', 1, ItemClassification.progression),
|
||||
ItemRow('Area: Central Varrock', 1, ItemClassification.progression),
|
||||
ItemRow('Area: Varrock Palace', 1, ItemClassification.progression),
|
||||
ItemRow('Area: West Varrock', 1, ItemClassification.progression),
|
||||
@@ -37,7 +37,58 @@ item_rows = [
|
||||
ItemRow('Progressive Armor', 6, ItemClassification.progression),
|
||||
ItemRow('Progressive Weapons', 6, ItemClassification.progression),
|
||||
ItemRow('Progressive Tools', 6, ItemClassification.useful),
|
||||
ItemRow('Progressive Ranged Weapons', 3, ItemClassification.useful),
|
||||
ItemRow('Progressive Ranged Weapon', 3, ItemClassification.useful),
|
||||
ItemRow('Progressive Ranged Armor', 3, ItemClassification.useful),
|
||||
ItemRow('Progressive Magic', 2, ItemClassification.useful),
|
||||
ItemRow('Progressive Magic Spell', 2, ItemClassification.useful),
|
||||
ItemRow('An Invitation to the Gielinor Games', 1, ItemClassification.filler),
|
||||
ItemRow('Settled\'s Crossbow', 1, ItemClassification.filler),
|
||||
ItemRow('The Stone of Jas', 1, ItemClassification.filler),
|
||||
ItemRow('Nieve\'s Phone Number', 1, ItemClassification.filler),
|
||||
ItemRow('Hannanie\'s Lost Sanity', 1, ItemClassification.filler),
|
||||
ItemRow('XP Waste', 1, ItemClassification.filler),
|
||||
ItemRow('Ten Free Pulls on the Squeal of Fortune', 1, ItemClassification.filler),
|
||||
ItemRow('Project Zanaris Beta Invite', 1, ItemClassification.filler),
|
||||
ItemRow('A Funny Feeling You Would Have Been Followed', 1, ItemClassification.filler),
|
||||
ItemRow('An Ominous Prediction From Gnome Child', 1, ItemClassification.filler),
|
||||
ItemRow('A Logic Error', 1, ItemClassification.filler),
|
||||
ItemRow('The Warding Skill', 1, ItemClassification.filler),
|
||||
ItemRow('A 1/2500 Chance At Your Very Own Pet Baron Sucellus, Redeemable at your Local Duke, Some Restrictions May Apply', 1, ItemClassification.filler),
|
||||
ItemRow('A Suspicious Email From Iagex.com Asking for your Password', 1, ItemClassification.filler),
|
||||
ItemRow('A Review on that Pull Request You\'ve Been Waiting On', 1, ItemClassification.filler),
|
||||
ItemRow('Fifty Billion RS3 GP (Worthless)', 1, ItemClassification.filler),
|
||||
ItemRow('Mod Ash\'s Coffee Cup', 1, ItemClassification.filler),
|
||||
ItemRow('An Embarrasing Photo of Zammorak at the Christmas Party', 1, ItemClassification.filler),
|
||||
ItemRow('Another Bug To Report', 1, ItemClassification.filler),
|
||||
ItemRow('1-Up Mushroom', 1, ItemClassification.filler),
|
||||
ItemRow('Empty White Hallways', 1, ItemClassification.filler),
|
||||
ItemRow('Area: Menaphos', 1, ItemClassification.filler),
|
||||
ItemRow('A Ratcatchers Dialogue Rewrite', 1, ItemClassification.filler),
|
||||
ItemRow('"Nostalgia"', 1, ItemClassification.filler),
|
||||
ItemRow('A Hornless Unicorn', 1, ItemClassification.filler),
|
||||
ItemRow('The Ability To Use ::bank', 1, ItemClassification.filler),
|
||||
ItemRow('Free Haircut at the Falador Hairdresser', 1, ItemClassification.filler),
|
||||
ItemRow('Nothing Interesting Happens', 1, ItemClassification.filler),
|
||||
ItemRow('Why Fletch?', 1, ItemClassification.filler),
|
||||
ItemRow('Evolution of Combat', 1, ItemClassification.filler),
|
||||
ItemRow('Care Pack: 10,000 GP', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 90 Steel Nails', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 25 Swordfish', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 50 Lobsters', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 100 Law Runes', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 300 Each Elemental Rune', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 100 Chaos Runes', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 100 Death Runes', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 100 Oak Logs', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 50 Willow Logs', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 50 Bronze Bars', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 200 Iron Ore', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 100 Coal Ore', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 100 Raw Trout', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 200 Leather', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 50 Energy Potion (4)', 2, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 200 Big Bones', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 10 Each Uncut gems', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 3 Rings of Forging', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 500 Rune Essence', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 200 Mind Runes', 1, ItemClassification.useful),
|
||||
]
|
||||
|
||||
@@ -19,37 +19,56 @@ location_rows = [
|
||||
LocationRow('Quest: Witch\'s Potion', 'quest', ['Rimmington', 'Port Sarim', ], [], [], 0),
|
||||
LocationRow('Quest: The Knight\'s Sword', 'quest', ['Falador', 'Varrock Palace', 'Mudskipper Point', 'South of Varrock', 'Windmill', 'Pie Dish', 'Port Sarim', ], [SkillRequirement('Cooking', 10), SkillRequirement('Mining', 10), ], [], 0),
|
||||
LocationRow('Quest: Goblin Diplomacy', 'quest', ['Goblin Village', 'Draynor Village', 'Falador', 'South of Varrock', 'Onion', ], [], [], 0),
|
||||
LocationRow('Quest: Pirate\'s Treasure', 'quest', ['Port Sarim', 'Karamja', 'Falador', ], [], [], 0),
|
||||
LocationRow('Quest: Pirate\'s Treasure', 'quest', ['Port Sarim', 'Karamja', 'Falador', 'Central Varrock', ], [], [], 0),
|
||||
LocationRow('Quest: Rune Mysteries', 'quest', ['Lumbridge', 'Wizard Tower', 'Central Varrock', ], [], [], 0),
|
||||
LocationRow('Quest: Misthalin Mystery', 'quest', ['Lumbridge Swamp', ], [], [], 0),
|
||||
LocationRow('Quest: The Corsair Curse', 'quest', ['Rimmington', 'Falador Farms', 'Corsair Cove', ], [], [], 0),
|
||||
LocationRow('Quest: X Marks the Spot', 'quest', ['Lumbridge', 'Draynor Village', 'Port Sarim', ], [], [], 0),
|
||||
LocationRow('Quest: Below Ice Mountain', 'quest', ['Dwarven Mines', 'Dwarven Mountain Pass', 'Ice Mountain', 'Barbarian Village', 'Falador', 'Central Varrock', 'Edgeville', ], [], [], 16),
|
||||
LocationRow('Quest: Dragon Slayer', 'goal', ['Crandor', 'South of Varrock', 'Edgeville', 'Lumbridge', 'Rimmington', 'Monastery', 'Dwarven Mines', 'Port Sarim', 'Draynor Village', ], [], [], 32),
|
||||
LocationRow('Bury Some Big Bones', 'prayer', ['Big Bones', ], [SkillRequirement('Prayer', 1), ], [], 0),
|
||||
LocationRow('Activate the "Sharp Eye" Prayer', 'prayer', [], [SkillRequirement('Prayer', 8), ], [], 0),
|
||||
LocationRow('Activate the "Rock Skin" Prayer', 'prayer', [], [SkillRequirement('Prayer', 10), ], [], 0),
|
||||
LocationRow('Activate the "Protect Item" Prayer', 'prayer', [], [SkillRequirement('Prayer', 25), ], [], 2),
|
||||
LocationRow('Pray at the Edgeville Monastery', 'prayer', ['Monastery', ], [SkillRequirement('Prayer', 31), ], [], 6),
|
||||
LocationRow('Cast Bones To Bananas', 'magic', ['Nature Runes', ], [SkillRequirement('Magic', 15), ], [], 0),
|
||||
LocationRow('Cast Earth Strike', 'magic', [], [SkillRequirement('Magic', 9), ], [], 0),
|
||||
LocationRow('Cast Curse', 'magic', [], [SkillRequirement('Magic', 19), ], [], 0),
|
||||
LocationRow('Teleport to Varrock', 'magic', ['Central Varrock', 'Law Runes', ], [SkillRequirement('Magic', 25), ], [], 0),
|
||||
LocationRow('Teleport to Lumbridge', 'magic', ['Lumbridge', 'Law Runes', ], [SkillRequirement('Magic', 31), ], [], 2),
|
||||
LocationRow('Teleport to Lumbridge', 'magic', ['Lumbridge', 'Law Runes', ], [SkillRequirement('Magic', 31), ], [], 0),
|
||||
LocationRow('Telegrab a Gold Bar from the Varrock Bank', 'magic', ['Law Runes', 'West Varrock', ], [SkillRequirement('Magic', 33), ], [], 0),
|
||||
LocationRow('Teleport to Falador', 'magic', ['Falador', 'Law Runes', ], [SkillRequirement('Magic', 37), ], [], 6),
|
||||
LocationRow('Craft an Air Rune', 'runecraft', ['Rune Essence', 'Falador Farms', ], [SkillRequirement('Runecraft', 1), ], [], 0),
|
||||
LocationRow('Craft a Mind Rune', 'runecraft', ['Rune Essence', 'Goblin Village', ], [SkillRequirement('Runecraft', 2), ], [], 0),
|
||||
LocationRow('Craft a Water Rune', 'runecraft', ['Rune Essence', 'Lumbridge Swamp', ], [SkillRequirement('Runecraft', 5), ], [], 0),
|
||||
LocationRow('Craft an Earth Rune', 'runecraft', ['Rune Essence', 'Lumberyard', ], [SkillRequirement('Runecraft', 9), ], [], 0),
|
||||
LocationRow('Craft a Fire Rune', 'runecraft', ['Rune Essence', 'Al Kharid', ], [SkillRequirement('Runecraft', 14), ], [], 0),
|
||||
LocationRow('Craft a Body Rune', 'runecraft', ['Rune Essence', 'Dwarven Mountain Pass', ], [SkillRequirement('Runecraft', 20), ], [], 0),
|
||||
LocationRow('Craft runes with a Mind Core', 'runecraft', ['Camdozaal', 'Goblin Village', ], [SkillRequirement('Runecraft', 2), ], [], 0),
|
||||
LocationRow('Craft runes with a Body Core', 'runecraft', ['Camdozaal', 'Dwarven Mountain Pass', ], [SkillRequirement('Runecraft', 20), ], [], 0),
|
||||
LocationRow('Craft a Pot', 'crafting', ['Clay Ore', 'Barbarian Village', ], [SkillRequirement('Crafting', 1), ], [], 0),
|
||||
LocationRow('Craft a pair of Leather Boots', 'crafting', ['Milk', 'Al Kharid', ], [SkillRequirement('Crafting', 7), ], [], 0),
|
||||
LocationRow('Make an Unblessed Symbol', 'crafting', ['Silver Ore', 'Furnace', 'Al Kharid', 'Sheep', 'Spinning Wheel', ], [SkillRequirement('Crafting', 16), ], [], 0),
|
||||
LocationRow('Cut a Sapphire', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 20), ], [], 0),
|
||||
LocationRow('Cut an Emerald', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 27), ], [], 0),
|
||||
LocationRow('Cut a Ruby', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 34), ], [], 4),
|
||||
LocationRow('Enter the Crafting Guild', 'crafting', ['Crafting Guild', ], [SkillRequirement('Crafting', 40), ], [], 0),
|
||||
LocationRow('Cut a Diamond', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 43), ], [], 8),
|
||||
LocationRow('Mine Copper', 'crafting', ['Bronze Ores', ], [SkillRequirement('Mining', 1), ], [], 0),
|
||||
LocationRow('Mine Tin', 'crafting', ['Bronze Ores', ], [SkillRequirement('Mining', 1), ], [], 0),
|
||||
LocationRow('Mine Clay', 'crafting', ['Clay Ore', ], [SkillRequirement('Mining', 1), ], [], 0),
|
||||
LocationRow('Mine Iron', 'mining', ['Iron Ore', ], [SkillRequirement('Mining', 1), ], [], 0),
|
||||
LocationRow('Mine a Blurite Ore', 'mining', ['Mudskipper Point', 'Port Sarim', ], [SkillRequirement('Mining', 10), ], [], 0),
|
||||
LocationRow('Crush a Barronite Deposit', 'mining', ['Camdozaal', ], [SkillRequirement('Mining', 14), ], [], 0),
|
||||
LocationRow('Mine Silver', 'mining', ['Silver Ore', ], [SkillRequirement('Mining', 20), ], [], 0),
|
||||
LocationRow('Mine Coal', 'mining', ['Coal Ore', ], [SkillRequirement('Mining', 30), ], [], 2),
|
||||
LocationRow('Mine Gold', 'mining', ['Gold Ore', ], [SkillRequirement('Mining', 40), ], [], 6),
|
||||
LocationRow('Smelt a Bronze Bar', 'smithing', ['Bronze Ores', 'Furnace', ], [SkillRequirement('Smithing', 1), SkillRequirement('Mining', 1), ], [], 0),
|
||||
LocationRow('Smelt an Iron Bar', 'smithing', ['Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 15), SkillRequirement('Mining', 15), ], [], 0),
|
||||
LocationRow('Smelt a Silver Bar', 'smithing', ['Silver Ore', 'Furnace', ], [SkillRequirement('Smithing', 20), SkillRequirement('Mining', 20), ], [], 0),
|
||||
LocationRow('Smelt a Steel Bar', 'smithing', ['Coal Ore', 'Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 30), SkillRequirement('Mining', 30), ], [], 2),
|
||||
LocationRow('Smelt a Gold Bar', 'smithing', ['Gold Ore', 'Furnace', ], [SkillRequirement('Smithing', 40), SkillRequirement('Mining', 40), ], [], 6),
|
||||
LocationRow('Catch a Sardine', 'fishing', ['Shrimp Spot', ], [SkillRequirement('Fishing', 5), ], [], 0),
|
||||
LocationRow('Catch some Anchovies', 'fishing', ['Shrimp Spot', ], [SkillRequirement('Fishing', 15), ], [], 0),
|
||||
LocationRow('Catch a Trout', 'fishing', ['Fly Fishing Spot', ], [SkillRequirement('Fishing', 20), ], [], 0),
|
||||
LocationRow('Prepare a Tetra', 'fishing', ['Camdozaal', ], [SkillRequirement('Fishing', 33), SkillRequirement('Cooking', 33), ], [], 2),
|
||||
@@ -58,13 +77,16 @@ location_rows = [
|
||||
LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0),
|
||||
LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0),
|
||||
LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2),
|
||||
LocationRow('Enter the Cook\'s Guild', 'cooking', ['Cook\'s Guild', ], [], [], 0),
|
||||
LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6),
|
||||
LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8),
|
||||
LocationRow('Burn a Log', 'firemaking', [], [SkillRequirement('Firemaking', 1), SkillRequirement('Woodcutting', 1), ], [], 0),
|
||||
LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), SkillRequirement('Woodcutting', 15), ], [], 0),
|
||||
LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), SkillRequirement('Woodcutting', 30), ], [], 0),
|
||||
LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0),
|
||||
LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0),
|
||||
LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0),
|
||||
LocationRow('Kill a Duck', 'combat', ['Duck', ], [SkillRequirement('Combat', 1), ], [], 0),
|
||||
LocationRow('Kill Jeff', 'combat', ['Dwarven Mountain Pass', ], [SkillRequirement('Combat', 2), ], [], 0),
|
||||
LocationRow('Kill a Goblin', 'combat', ['Goblin', ], [SkillRequirement('Combat', 2), ], [], 0),
|
||||
LocationRow('Kill a Monkey', 'combat', ['Karamja', ], [SkillRequirement('Combat', 3), ], [], 0),
|
||||
@@ -81,19 +103,24 @@ location_rows = [
|
||||
LocationRow('Kill an Ogress Shaman', 'combat', ['Corsair Cove', ], [SkillRequirement('Combat', 82), ], [], 8),
|
||||
LocationRow('Kill Obor', 'combat', ['Edgeville', ], [SkillRequirement('Combat', 106), ], [], 28),
|
||||
LocationRow('Kill Bryophyta', 'combat', ['Central Varrock', ], [SkillRequirement('Combat', 128), ], [], 28),
|
||||
LocationRow('Die', 'general', [], [], [], 0),
|
||||
LocationRow('Reach a Level 10', 'general', [], [], [], 0),
|
||||
LocationRow('Total XP 5,000', 'general', [], [], [], 0),
|
||||
LocationRow('Combat Level 5', 'general', [], [], [], 0),
|
||||
LocationRow('Total XP 10,000', 'general', [], [], [], 0),
|
||||
LocationRow('Total Level 50', 'general', [], [], [], 0),
|
||||
LocationRow('Reach a Level 20', 'general', [], [], [], 0),
|
||||
LocationRow('Total XP 25,000', 'general', [], [], [], 0),
|
||||
LocationRow('Total Level 100', 'general', [], [], [], 0),
|
||||
LocationRow('Total XP 50,000', 'general', [], [], [], 0),
|
||||
LocationRow('Combat Level 15', 'general', [], [], [], 0),
|
||||
LocationRow('Total Level 150', 'general', [], [], [], 2),
|
||||
LocationRow('Reach a Level 30', 'general', [], [], [], 2),
|
||||
LocationRow('Total XP 75,000', 'general', [], [], [], 2),
|
||||
LocationRow('Combat Level 25', 'general', [], [], [], 2),
|
||||
LocationRow('Total XP 100,000', 'general', [], [], [], 6),
|
||||
LocationRow('Total Level 200', 'general', [], [], [], 6),
|
||||
LocationRow('Reach a Level 40', 'general', [], [], [], 6),
|
||||
LocationRow('Total XP 125,000', 'general', [], [], [], 6),
|
||||
LocationRow('Combat Level 30', 'general', [], [], [], 10),
|
||||
LocationRow('Total Level 250', 'general', [], [], [], 10),
|
||||
@@ -103,6 +130,28 @@ location_rows = [
|
||||
LocationRow('Open a Simple Lockbox', 'general', ['Camdozaal', ], [], [], 0),
|
||||
LocationRow('Open an Elaborate Lockbox', 'general', ['Camdozaal', ], [], [], 0),
|
||||
LocationRow('Open an Ornate Lockbox', 'general', ['Camdozaal', ], [], [], 0),
|
||||
LocationRow('Trans your Gender', 'general', ['Makeover', ], [], [], 0),
|
||||
LocationRow('Read a Flyer from Ali the Leaflet Dropper', 'general', ['Al Kharid', 'South of Varrock', ], [], [], 0),
|
||||
LocationRow('Cry by the Members Gate to Taverley', 'general', ['Dwarven Mountain Pass', ], [], [], 0),
|
||||
LocationRow('Get Prompted to Buy Membership', 'general', [], [], [], 0),
|
||||
LocationRow('Pet the Stray Dog in Varrock', 'general', ['Central Varrock', 'West Varrock', 'South of Varrock', ], [], [], 0),
|
||||
LocationRow('Get Sent to Jail in Shantay Pass', 'general', ['Al Kharid', 'Port Sarim', ], [], [], 0),
|
||||
LocationRow('Have the Apothecary Make a Strength Potion', 'general', ['Central Varrock', 'Red Spider Eggs', 'Limpwurt Root', ], [], [], 0),
|
||||
LocationRow('Put a Whole Banana into a Bottle of Karamjan Rum', 'general', ['Karamja', ], [], [], 0),
|
||||
LocationRow('Attempt to Shear "The Thing"', 'general', ['Lumbridge Farms West', ], [], [], 0),
|
||||
LocationRow('Eat a Kebab', 'general', ['Al Kharid', ], [], [], 0),
|
||||
LocationRow('Return a Beer Glass to a Bar', 'general', ['Falador', ], [], [], 0),
|
||||
LocationRow('Enter the Varrock Bear Cage', 'general', ['Varrock Palace', ], [], [], 0),
|
||||
LocationRow('Equip a Cabbage Cape', 'general', ['Draynor Village', ], [], [], 0),
|
||||
LocationRow('Equip a Pride Scarf', 'general', ['Draynor Village', ], [], [], 0),
|
||||
LocationRow('Visit the Black Hole', 'general', ['Draynor Village', 'Dwarven Mines', ], [], [], 0),
|
||||
LocationRow('Try to Equip Goblin Mail', 'general', ['Goblin', ], [], [], 0),
|
||||
LocationRow('Equip an Orange Cape', 'general', ['Draynor Village', ], [], [], 0),
|
||||
LocationRow('Find a Needle in a Haystack', 'general', ['Haystack', ], [], [], 0),
|
||||
LocationRow('Insult the Homeless (but not Charlie he\'s cool)', 'general', ['Central Varrock', 'South of Varrock', ], [], [], 0),
|
||||
LocationRow('Dance with Party Pete', 'general', ['Falador', ], [], [], 0),
|
||||
LocationRow('Read a Newspaper', 'general', ['Central Varrock', ], [], [], 0),
|
||||
LocationRow('Add a Card to the Chronicle', 'general', ['Draynor Village', ], [], [], 0),
|
||||
LocationRow('Points: Cook\'s Assistant', 'points', [], [], [], 0),
|
||||
LocationRow('Points: Demon Slayer', 'points', [], [], [], 0),
|
||||
LocationRow('Points: The Restless Ghost', 'points', [], [], [], 0),
|
||||
|
||||
@@ -4,19 +4,19 @@ This file was auto generated by LogicCSVToPython.py
|
||||
from ..Regions import RegionRow
|
||||
|
||||
region_rows = [
|
||||
RegionRow('Lumbridge', 'Area: Lumbridge', ['Lumbridge Farms East', 'Lumbridge Farms West', 'Al Kharid', 'Lumbridge Swamp', 'HAM Hideout', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Mind Runes', 'Spinning Wheel', 'Furnace', 'Chisel', 'Bronze Anvil', 'Fly Fishing Spot', 'Bowl', 'Cake Tin', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Goblin', 'Imps', ]),
|
||||
RegionRow('Lumbridge Swamp', 'Area: Lumbridge Swamp', ['Lumbridge', 'HAM Hideout', ], ['Bronze Ores', 'Coal Ore', 'Shrimp Spot', 'Meat', 'Goblin', 'Imps', ]),
|
||||
RegionRow('Lumbridge', 'Area: Lumbridge', ['Lumbridge Farms East', 'Lumbridge Farms West', 'Al Kharid', 'Lumbridge Swamp', 'HAM Hideout', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Mind Runes', 'Spinning Wheel', 'Furnace', 'Chisel', 'Bronze Anvil', 'Fly Fishing Spot', 'Bowl', 'Cake Tin', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Goblin', 'Imps', 'Duck', 'Bar', ]),
|
||||
RegionRow('Lumbridge Swamp', 'Area: Lumbridge Swamp', ['Lumbridge', 'HAM Hideout', ], ['Bronze Ores', 'Coal Ore', 'Shrimp Spot', 'Meat', 'Goblin', 'Imps', 'Big Bones', 'Duck', ]),
|
||||
RegionRow('HAM Hideout', 'Area: HAM Hideout', ['Lumbridge Farms West', 'Lumbridge', 'Lumbridge Swamp', 'Draynor Village', ], ['Goblin', ]),
|
||||
RegionRow('Lumbridge Farms West', 'Area: Lumbridge Farms', ['Sourhog\'s Lair', 'HAM Hideout', 'Draynor Village', ], ['Sheep', 'Meat', 'Wheat', 'Windmill', 'Egg', 'Milk', 'Willow Tree', 'Imps', 'Potato', ]),
|
||||
RegionRow('Lumbridge Farms West', 'Area: Lumbridge Farms', ['Sourhog\'s Lair', 'HAM Hideout', 'Draynor Village', ], ['Sheep', 'Meat', 'Wheat', 'Windmill', 'Egg', 'Milk', 'Willow Tree', 'Imps', 'Potato', 'Haystack', ]),
|
||||
RegionRow('Lumbridge Farms East', 'Area: Lumbridge Farms', ['South of Varrock', 'Lumbridge', ], ['Meat', 'Egg', 'Milk', 'Willow Tree', 'Goblin', 'Imps', 'Potato', ]),
|
||||
RegionRow('Sourhog\'s Lair', 'Area: South of Varrock', ['Lumbridge Farms West', 'Draynor Manor Outskirts', ], ['', ]),
|
||||
RegionRow('South of Varrock', 'Area: South of Varrock', ['Al Kharid', 'West Varrock', 'Central Varrock', 'East Varrock', 'Lumbridge Farms East', 'Lumbridge', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Sheep', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Redberry Bush', 'Meat', 'Wheat', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Guard', 'Imps', 'Clay Ore', ]),
|
||||
RegionRow('East Varrock', 'Area: East Varrock', ['Wilderness', 'South of Varrock', 'Central Varrock', 'Varrock Palace', ], ['Guard', ]),
|
||||
RegionRow('Central Varrock', 'Area: Central Varrock', ['Varrock Palace', 'East Varrock', 'South of Varrock', 'West Varrock', ], ['Mind Runes', 'Chisel', 'Anvil', 'Bowl', 'Cake Tin', 'Oak Tree', 'Barbarian', 'Guard', 'Rune Essence', 'Imps', ]),
|
||||
RegionRow('Varrock Palace', 'Area: Varrock Palace', ['Wilderness', 'East Varrock', 'Central Varrock', 'West Varrock', ], ['Pie Dish', 'Oak Tree', 'Zombie', 'Guard', 'Deadly Red Spider', 'Moss Giant', 'Nature Runes', 'Law Runes', ]),
|
||||
RegionRow('South of Varrock', 'Area: South of Varrock', ['Al Kharid', 'West Varrock', 'Central Varrock', 'Lumberyard', 'Lumbridge Farms East', 'Lumbridge', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Sheep', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Redberry Bush', 'Meat', 'Wheat', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Guard', 'Imps', 'Clay Ore', 'Duck', ]),
|
||||
RegionRow('Lumberyard', 'Area: Lumberyard', ['Wilderness', 'South of Varrock', 'Central Varrock', 'Varrock Palace', ], ['Guard', 'Bar', ]),
|
||||
RegionRow('Central Varrock', 'Area: Central Varrock', ['Varrock Palace', 'Lumberyard', 'South of Varrock', 'West Varrock', ], ['Mind Runes', 'Chisel', 'Anvil', 'Bowl', 'Cake Tin', 'Oak Tree', 'Barbarian', 'Guard', 'Rune Essence', 'Imps', 'Makeover', 'Bar', ]),
|
||||
RegionRow('Varrock Palace', 'Area: Varrock Palace', ['Wilderness', 'Lumberyard', 'Central Varrock', 'West Varrock', ], ['Pie Dish', 'Oak Tree', 'Zombie', 'Guard', 'Deadly Red Spider', 'Moss Giant', 'Nature Runes', 'Law Runes', 'Big Bones', 'Makeover', 'Red Spider Eggs', ]),
|
||||
RegionRow('West Varrock', 'Area: West Varrock', ['Wilderness', 'Varrock Palace', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Cook\'s Guild', ], ['Anvil', 'Wheat', 'Oak Tree', 'Goblin', 'Guard', 'Onion', ]),
|
||||
RegionRow('Cook\'s Guild', 'Area: West Varrock*', ['West Varrock', ], ['Bowl', 'Cooking Apple', 'Pie Dish', 'Cake Tin', 'Windmill', ]),
|
||||
RegionRow('Edgeville', 'Area: Edgeville', ['Wilderness', 'West Varrock', 'Barbarian Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Bowl', 'Meat', 'Cake Tin', 'Willow Tree', 'Canoe Tree', 'Zombie', 'Guard', 'Hill Giant', 'Nature Runes', 'Law Runes', 'Imps', ]),
|
||||
RegionRow('Edgeville', 'Area: Edgeville', ['Wilderness', 'West Varrock', 'Barbarian Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Bowl', 'Meat', 'Cake Tin', 'Willow Tree', 'Canoe Tree', 'Zombie', 'Guard', 'Hill Giant', 'Nature Runes', 'Law Runes', 'Imps', 'Big Bones', 'Limpwurt Root', 'Haystack', ]),
|
||||
RegionRow('Barbarian Village', 'Area: Barbarian Village', ['Edgeville', 'West Varrock', 'Draynor Manor Outskirts', 'Dwarven Mountain Pass', ], ['Spinning Wheel', 'Coal Ore', 'Anvil', 'Fly Fishing Spot', 'Meat', 'Canoe Tree', 'Barbarian', 'Zombie', 'Law Runes', ]),
|
||||
RegionRow('Draynor Manor Outskirts', 'Area: Draynor Manor', ['Barbarian Village', 'Sourhog\'s Lair', 'Draynor Village', 'Falador East Outskirts', ], ['Goblin', ]),
|
||||
RegionRow('Draynor Manor', 'Area: Draynor Manor', ['Draynor Village', ], ['', ]),
|
||||
@@ -27,21 +27,21 @@ region_rows = [
|
||||
RegionRow('Ice Mountain', 'Area: Ice Mountain', ['Wilderness', 'Monastery', 'Dwarven Mines', 'Camdozaal*', ], ['', ]),
|
||||
RegionRow('Camdozaal', 'Area: Ice Mountain', ['Ice Mountain', ], ['Clay Ore', ]),
|
||||
RegionRow('Monastery', 'Area: Monastery', ['Wilderness', 'Dwarven Mountain Pass', 'Dwarven Mines', 'Ice Mountain', ], ['Sheep', ]),
|
||||
RegionRow('Falador', 'Area: Falador', ['Dwarven Mountain Pass', 'Falador Farms', 'Dwarven Mines', ], ['Furnace', 'Chisel', 'Bowl', 'Cake Tin', 'Oak Tree', 'Guard', 'Imps', ]),
|
||||
RegionRow('Falador Farms', 'Area: Falador Farms', ['Falador', 'Falador East Outskirts', 'Draynor Village', 'Port Sarim', 'Rimmington', 'Crafting Guild Outskirts', ], ['Spinning Wheel', 'Meat', 'Egg', 'Milk', 'Oak Tree', 'Imps', ]),
|
||||
RegionRow('Falador', 'Area: Falador', ['Dwarven Mountain Pass', 'Falador Farms', 'Dwarven Mines', ], ['Furnace', 'Chisel', 'Bowl', 'Cake Tin', 'Oak Tree', 'Guard', 'Imps', 'Duck', 'Makeover', 'Bar', ]),
|
||||
RegionRow('Falador Farms', 'Area: Falador Farms', ['Falador', 'Falador East Outskirts', 'Draynor Village', 'Port Sarim', 'Rimmington', 'Crafting Guild Outskirts', ], ['Spinning Wheel', 'Meat', 'Egg', 'Milk', 'Oak Tree', 'Imps', 'Duck', ]),
|
||||
RegionRow('Port Sarim', 'Area: Port Sarim', ['Falador Farms', 'Mudskipper Point', 'Rimmington', 'Karamja Docks', 'Crandor', ], ['Mind Runes', 'Shrimp Spot', 'Meat', 'Cheese', 'Tomato', 'Oak Tree', 'Willow Tree', 'Goblin', 'Potato', ]),
|
||||
RegionRow('Karamja Docks', 'Area: Mudskipper Point', ['Port Sarim', 'Karamja', ], ['', ]),
|
||||
RegionRow('Mudskipper Point', 'Area: Mudskipper Point', ['Rimmington', 'Port Sarim', ], ['Anvil', 'Ice Giant', 'Nature Runes', 'Law Runes', ]),
|
||||
RegionRow('Karamja', 'Area: Karamja', ['Karamja Docks', 'Crandor', ], ['Gold Ore', 'Lobster Spot', 'Bowl', 'Cake Tin', 'Deadly Red Spider', 'Imps', ]),
|
||||
RegionRow('Crandor', 'Area: Crandor', ['Karamja', 'Port Sarim', ], ['Coal Ore', 'Gold Ore', 'Moss Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]),
|
||||
RegionRow('Mudskipper Point', 'Area: Mudskipper Point', ['Rimmington', 'Port Sarim', ], ['Anvil', 'Ice Giant', 'Nature Runes', 'Law Runes', 'Big Bones', 'Limpwurt Root', ]),
|
||||
RegionRow('Karamja', 'Area: Karamja', ['Karamja Docks', 'Crandor', ], ['Gold Ore', 'Lobster Spot', 'Bowl', 'Cake Tin', 'Deadly Red Spider', 'Imps', 'Red Spider Eggs', ]),
|
||||
RegionRow('Crandor', 'Area: Crandor', ['Karamja', 'Port Sarim', ], ['Coal Ore', 'Gold Ore', 'Moss Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', 'Big Bones', 'Limpwurt Root', ]),
|
||||
RegionRow('Rimmington', 'Area: Rimmington', ['Falador Farms', 'Port Sarim', 'Mudskipper Point', 'Crafting Guild Peninsula', 'Corsair Cove', ], ['Chisel', 'Bronze Ores', 'Iron Ore', 'Gold Ore', 'Bowl', 'Cake Tin', 'Wheat', 'Oak Tree', 'Willow Tree', 'Crafting Moulds', 'Imps', 'Clay Ore', 'Onion', ]),
|
||||
RegionRow('Crafting Guild Peninsula', 'Area: Crafting Guild', ['Falador Farms', 'Rimmington', ], ['', ]),
|
||||
RegionRow('Crafting Guild Outskirts', 'Area: Crafting Guild', ['Falador Farms', 'Crafting Guild', ], ['Sheep', 'Willow Tree', 'Oak Tree', ]),
|
||||
RegionRow('Crafting Guild Peninsula', 'Area: Crafting Guild', ['Falador Farms', 'Rimmington', ], ['Limpwurt Root', ]),
|
||||
RegionRow('Crafting Guild Outskirts', 'Area: Crafting Guild', ['Falador Farms', 'Crafting Guild', ], ['Sheep', 'Willow Tree', 'Oak Tree', 'Makeover', ]),
|
||||
RegionRow('Crafting Guild', 'Area: Crafting Guild*', ['Crafting Guild', ], ['Spinning Wheel', 'Chisel', 'Silver Ore', 'Gold Ore', 'Meat', 'Milk', 'Clay Ore', ]),
|
||||
RegionRow('Draynor Village', 'Area: Draynor Village', ['Draynor Manor', 'Lumbridge Farms West', 'HAM Hideout', 'Wizard Tower', ], ['Anvil', 'Shrimp Spot', 'Wheat', 'Cheese', 'Tomato', 'Willow Tree', 'Goblin', 'Zombie', 'Nature Runes', 'Law Runes', 'Imps', ]),
|
||||
RegionRow('Wizard Tower', 'Area: Wizard Tower', ['Draynor Village', ], ['Lesser Demon', 'Rune Essence', ]),
|
||||
RegionRow('Corsair Cove', 'Area: Corsair Cove*', ['Rimmington', ], ['Anvil', 'Meat', ]),
|
||||
RegionRow('Corsair Cove', 'Area: Corsair Cove*', ['Rimmington', ], ['Anvil', 'Meat', 'Limpwurt Root', ]),
|
||||
RegionRow('Al Kharid', 'Area: Al Kharid', ['South of Varrock', 'Citharede Abbey', 'Lumbridge', 'Port Sarim', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Coal Ore', 'Gold Ore', 'Shrimp Spot', 'Bowl', 'Cake Tin', 'Cheese', 'Crafting Moulds', 'Imps', ]),
|
||||
RegionRow('Citharede Abbey', 'Area: Citharede Abbey', ['Al Kharid', ], ['Iron Ore', 'Coal Ore', 'Anvil', 'Hill Giant', 'Nature Runes', 'Law Runes', ]),
|
||||
RegionRow('Wilderness', 'Area: Wilderness', ['East Varrock', 'Varrock Palace', 'West Varrock', 'Edgeville', 'Monastery', 'Ice Mountain', 'Goblin Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Iron Ore', 'Coal Ore', 'Anvil', 'Meat', 'Cake Tin', 'Cheese', 'Tomato', 'Oak Tree', 'Canoe Tree', 'Zombie', 'Hill Giant', 'Deadly Red Spider', 'Moss Giant', 'Ice Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]),
|
||||
RegionRow('Citharede Abbey', 'Area: Citharede Abbey', ['Al Kharid', ], ['Iron Ore', 'Coal Ore', 'Anvil', 'Hill Giant', 'Nature Runes', 'Law Runes', 'Big Bones', 'Limpwurt Root', ]),
|
||||
RegionRow('Wilderness', 'Area: Wilderness', ['Lumberyard', 'Varrock Palace', 'West Varrock', 'Edgeville', 'Monastery', 'Ice Mountain', 'Goblin Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Iron Ore', 'Coal Ore', 'Anvil', 'Meat', 'Cake Tin', 'Cheese', 'Tomato', 'Oak Tree', 'Canoe Tree', 'Zombie', 'Hill Giant', 'Deadly Red Spider', 'Moss Giant', 'Ice Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', 'Big Bones', 'Limpwurt Root', 'Bar', ]),
|
||||
]
|
||||
|
||||
@@ -51,4 +51,11 @@ resource_rows = [
|
||||
ResourceRow('Clay Ore'),
|
||||
ResourceRow('Onion'),
|
||||
ResourceRow('Potato'),
|
||||
ResourceRow('Big Bones'),
|
||||
ResourceRow('Duck'),
|
||||
ResourceRow('Makeover'),
|
||||
ResourceRow('Limpwurt Root'),
|
||||
ResourceRow('Bar'),
|
||||
ResourceRow('Haystack'),
|
||||
ResourceRow('Red Spider Eggs'),
|
||||
]
|
||||
|
||||
@@ -73,7 +73,7 @@ class ItemNames(str, Enum):
|
||||
South_Of_Varrock = "Area: South of Varrock"
|
||||
Central_Varrock = "Area: Central Varrock"
|
||||
Varrock_Palace = "Area: Varrock Palace"
|
||||
East_Of_Varrock = "Area: East Varrock"
|
||||
Lumberyard = "Area: Lumberyard"
|
||||
West_Varrock = "Area: West Varrock"
|
||||
Edgeville = "Area: Edgeville"
|
||||
Barbarian_Village = "Area: Barbarian Village"
|
||||
@@ -94,8 +94,8 @@ class ItemNames(str, Enum):
|
||||
Progressive_Weapons = "Progressive Weapons"
|
||||
Progressive_Tools = "Progressive Tools"
|
||||
Progressive_Range_Armor = "Progressive Ranged Armor"
|
||||
Progressive_Range_Weapon = "Progressive Ranged Weapons"
|
||||
Progressive_Magic = "Progressive Magic"
|
||||
Progressive_Range_Weapon = "Progressive Ranged Weapon"
|
||||
Progressive_Magic = "Progressive Magic Spell"
|
||||
Lobsters = "10 Lobsters"
|
||||
Swordfish = "5 Swordfish"
|
||||
Energy_Potions = "10 Energy Potions"
|
||||
|
||||
@@ -3,18 +3,19 @@ from dataclasses import dataclass
|
||||
from Options import Choice, Toggle, Range, PerGameCommonOptions
|
||||
|
||||
MAX_COMBAT_TASKS = 16
|
||||
MAX_PRAYER_TASKS = 3
|
||||
MAX_MAGIC_TASKS = 4
|
||||
MAX_RUNECRAFT_TASKS = 3
|
||||
MAX_CRAFTING_TASKS = 5
|
||||
MAX_MINING_TASKS = 5
|
||||
MAX_SMITHING_TASKS = 4
|
||||
MAX_FISHING_TASKS = 5
|
||||
MAX_COOKING_TASKS = 5
|
||||
MAX_FIREMAKING_TASKS = 2
|
||||
|
||||
MAX_PRAYER_TASKS = 5
|
||||
MAX_MAGIC_TASKS = 7
|
||||
MAX_RUNECRAFT_TASKS = 8
|
||||
MAX_CRAFTING_TASKS = 11
|
||||
MAX_MINING_TASKS = 6
|
||||
MAX_SMITHING_TASKS = 5
|
||||
MAX_FISHING_TASKS = 6
|
||||
MAX_COOKING_TASKS = 6
|
||||
MAX_FIREMAKING_TASKS = 3
|
||||
MAX_WOODCUTTING_TASKS = 3
|
||||
|
||||
NON_QUEST_LOCATION_COUNT = 22
|
||||
NON_QUEST_LOCATION_COUNT = 49
|
||||
|
||||
|
||||
class StartingArea(Choice):
|
||||
@@ -58,6 +59,31 @@ class ProgressiveTasks(Toggle):
|
||||
display_name = "Progressive Tasks"
|
||||
|
||||
|
||||
class EnableDuds(Toggle):
|
||||
"""
|
||||
Whether to include filler "Dud" items that serve no purpose but allow for more tasks in the pool.
|
||||
"""
|
||||
display_name = "Enable Duds"
|
||||
|
||||
|
||||
class DudCount(Range):
|
||||
"""
|
||||
How many "Dud" items to include in the pool. This setting is ignored if "Enable Duds" is not included
|
||||
"""
|
||||
display_name = "Dud Item Count"
|
||||
range_start = 0
|
||||
range_end = 30
|
||||
default = 10
|
||||
|
||||
|
||||
class EnableCarePacks(Toggle):
|
||||
"""
|
||||
Whether or not to include useful "Care Pack" items that allow you to trade over specific items.
|
||||
Note: Requires your account NOT to be an Ironman. Also, requires access to another account to trade over the items,
|
||||
or gold to purchase off of the grand exchange.
|
||||
"""
|
||||
display_name = "Enable Care Packs"
|
||||
|
||||
class MaxCombatLevel(Range):
|
||||
"""
|
||||
The highest combat level of monster to possibly be assigned as a task.
|
||||
@@ -472,6 +498,9 @@ class OSRSOptions(PerGameCommonOptions):
|
||||
starting_area: StartingArea
|
||||
brutal_grinds: BrutalGrinds
|
||||
progressive_tasks: ProgressiveTasks
|
||||
enable_duds: EnableDuds
|
||||
dud_count: DudCount
|
||||
enable_carepacks: EnableCarePacks
|
||||
max_combat_level: MaxCombatLevel
|
||||
max_combat_tasks: MaxCombatTasks
|
||||
combat_task_weight: CombatTaskWeight
|
||||
|
||||
@@ -212,11 +212,14 @@ def get_skill_rule(skill, level, player, options) -> CollectionRule:
|
||||
return lambda state: True
|
||||
|
||||
|
||||
def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options):
|
||||
def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options, world):
|
||||
if outbound_region_name == RegionNames.Cooks_Guild:
|
||||
add_rule(entrance, get_cooking_skill_rule(32, player, options))
|
||||
# Since there's goblins in this chunk, checking for hat access is superfluous, you'd always have it anyway
|
||||
elif outbound_region_name == RegionNames.Crafting_Guild:
|
||||
add_rule(entrance, get_crafting_skill_rule(40, player, options))
|
||||
# Literally the only brown apron access in the entirety of f2p is buying it in varrock
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Central_Varrock, player))
|
||||
elif outbound_region_name == RegionNames.Corsair_Cove:
|
||||
# Need to be able to start Corsair Curse in addition to having the item
|
||||
add_rule(entrance, lambda state: state.can_reach(RegionNames.Falador_Farm, "Region", player))
|
||||
@@ -224,6 +227,17 @@ def generate_special_rules_for(entrance, region_row, outbound_region_name, playe
|
||||
add_rule(entrance, lambda state: state.has(ItemNames.QP_Below_Ice_Mountain, player))
|
||||
elif region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*":
|
||||
add_rule(entrance, lambda state: state.has(ItemNames.QP_Dorics_Quest, player))
|
||||
elif outbound_region_name == RegionNames.Crandor:
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.South_Of_Varrock, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Edgeville, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Lumbridge, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Rimmington, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Monastery, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Dwarven_Mines, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Port_Sarim, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Draynor_Village, player))
|
||||
add_rule(entrance, lambda state: world.quest_points(state) >= 32)
|
||||
|
||||
|
||||
# Special logic for canoes
|
||||
canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village,
|
||||
|
||||
@@ -168,7 +168,7 @@ class OSRSWorld(World):
|
||||
|
||||
item_name = self.region_rows_by_name[parsed_outbound].itemReq
|
||||
entrance.access_rule = lambda state, item_name=item_name.replace("*",""): state.has(item_name, self.player)
|
||||
generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options)
|
||||
generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options, self)
|
||||
|
||||
for resource_region in region_row.resources:
|
||||
if not resource_region:
|
||||
@@ -179,7 +179,7 @@ class OSRSWorld(World):
|
||||
entrance.connect(self.region_name_to_data[resource_region])
|
||||
else:
|
||||
entrance.connect(self.region_name_to_data[resource_region.replace('*', '')])
|
||||
generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options)
|
||||
generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options, self)
|
||||
|
||||
self.roll_locations()
|
||||
|
||||
@@ -195,7 +195,16 @@ class OSRSWorld(World):
|
||||
generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override
|
||||
locations_required = 0
|
||||
for item_row in item_rows:
|
||||
# If it's a filler item, set it aside for later
|
||||
if item_row.progression == ItemClassification.filler:
|
||||
continue
|
||||
|
||||
# If it starts with "Care Pack", only add it if Care Packs are enabled
|
||||
if item_row.name.startswith("Care Pack"):
|
||||
if not self.options.enable_carepacks:
|
||||
continue
|
||||
locations_required += item_row.amount
|
||||
if self.options.enable_duds: locations_required += self.options.dud_count
|
||||
|
||||
locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0
|
||||
|
||||
@@ -232,6 +241,7 @@ class OSRSWorld(World):
|
||||
max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks")
|
||||
tasks_for_this_type = [task for task in self.locations_by_category[task_type]
|
||||
if self.task_within_skill_levels(task.skills)]
|
||||
max_amount_for_task_type = min(max_amount_for_task_type, len(tasks_for_this_type))
|
||||
if not self.options.progressive_tasks:
|
||||
rnd.shuffle(tasks_for_this_type)
|
||||
else:
|
||||
@@ -286,16 +296,36 @@ class OSRSWorld(World):
|
||||
self.create_and_add_location(index)
|
||||
|
||||
def create_items(self) -> None:
|
||||
filler_items = []
|
||||
for item_row in item_rows:
|
||||
if item_row.name != self.starting_area_item:
|
||||
# If it's a filler item, set it aside for later
|
||||
if item_row.progression == ItemClassification.filler:
|
||||
filler_items.append(item_row)
|
||||
continue
|
||||
|
||||
# If it starts with "Care Pack", only add it if Care Packs are enabled
|
||||
if item_row.name.startswith("Care Pack"):
|
||||
if not self.options.enable_carepacks:
|
||||
continue
|
||||
|
||||
for c in range(item_row.amount):
|
||||
item = self.create_item(item_row.name)
|
||||
self.multiworld.itempool.append(item)
|
||||
if self.options.enable_duds:
|
||||
self.random.shuffle(filler_items)
|
||||
filler_items = filler_items[0:self.options.dud_count]
|
||||
for item_row in filler_items:
|
||||
item = self.create_item(item_row.name)
|
||||
self.multiworld.itempool.append(item)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choice(
|
||||
[ItemNames.Progressive_Armor, ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic,
|
||||
ItemNames.Progressive_Tools, ItemNames.Progressive_Range_Armor, ItemNames.Progressive_Range_Weapon])
|
||||
if self.options.enable_duds:
|
||||
return self.random.choice([item for item in item_rows if item.progression == ItemClassification.filler])
|
||||
else:
|
||||
return self.random.choice([ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic,
|
||||
ItemNames.Progressive_Range_Weapon, ItemNames.Progressive_Armor,
|
||||
ItemNames.Progressive_Range_Armor, ItemNames.Progressive_Tools])
|
||||
|
||||
def create_and_add_location(self, row_index) -> None:
|
||||
location_row = location_rows[row_index]
|
||||
|
||||
@@ -151,7 +151,7 @@ Magma Stone;
|
||||
// Evil
|
||||
Smashing, Poppet!; Achievement;
|
||||
Arms Dealer; Npc;
|
||||
Leading Landlord; Achievement; Nurse & Arms Dealer; // The logic is way more complex, but that doesn't affect anything
|
||||
Leading Landlord; Achievement | Not Getfixedboi; Nurse & Arms Dealer; // The logic is way more complex, but that doesn't affect anything
|
||||
Completely Awesome; Achievement; Arms Dealer;
|
||||
Illegal Gun Parts; ; Arms Dealer | Flamethrower;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dataclasses import fields
|
||||
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set, TextIO
|
||||
from logging import warning
|
||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
|
||||
@@ -7,16 +8,16 @@ from .locations import location_table, location_name_groups, standard_location_n
|
||||
from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon
|
||||
from .er_rules import set_er_location_rules
|
||||
from .regions import tunic_regions
|
||||
from .er_scripts import create_er_regions
|
||||
from .er_scripts import create_er_regions, verify_plando_directions
|
||||
from .grass import grass_location_table, grass_location_name_to_id, grass_location_name_groups, excluded_grass_locations
|
||||
from .er_data import portal_mapping, RegionInfo, tunic_er_regions
|
||||
from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections,
|
||||
LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage, check_options,
|
||||
get_hexagons_in_pool, HexagonQuestAbilityUnlockType)
|
||||
get_hexagons_in_pool, HexagonQuestAbilityUnlockType, EntranceLayout)
|
||||
from .breakables import breakable_location_name_to_id, breakable_location_groups, breakable_location_table
|
||||
from .combat_logic import area_data, CombatState
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from Options import PlandoConnection, OptionError
|
||||
from Options import PlandoConnection, OptionError, PerGameCommonOptions, Removed, Range
|
||||
from settings import Group, Bool
|
||||
|
||||
|
||||
@@ -61,8 +62,9 @@ class SeedGroup(TypedDict):
|
||||
ice_grappling: int # ice_grappling value
|
||||
ladder_storage: int # ls value
|
||||
laurels_at_10_fairies: bool # laurels location value
|
||||
fixed_shop: bool # fixed shop value
|
||||
plando: TunicPlandoConnections # consolidated plando connections for the seed group
|
||||
entrance_layout: int # entrance layout value
|
||||
has_decoupled_enabled: bool # for checking that players don't have conflicting options
|
||||
plando: List[PlandoConnection] # consolidated plando connections for the seed group
|
||||
|
||||
|
||||
class TunicWorld(World):
|
||||
@@ -95,7 +97,7 @@ class TunicWorld(World):
|
||||
tunic_portal_pairs: Dict[str, str]
|
||||
er_portal_hints: Dict[int, str]
|
||||
seed_groups: Dict[str, SeedGroup] = {}
|
||||
shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected
|
||||
used_shop_numbers: Set[int]
|
||||
er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work
|
||||
|
||||
# for the local_fill option
|
||||
@@ -119,27 +121,52 @@ class TunicWorld(World):
|
||||
raise Exception("You have a TUNIC APWorld in your lib/worlds folder and custom_worlds folder.\n"
|
||||
"This would cause an error at the end of generation.\n"
|
||||
"Please remove one of them, most likely the one in lib/worlds.")
|
||||
|
||||
if self.options.all_random:
|
||||
for option_name in (attr.name for attr in fields(TunicOptions)
|
||||
if attr not in fields(PerGameCommonOptions)):
|
||||
option = getattr(self.options, option_name)
|
||||
if option_name == "all_random":
|
||||
continue
|
||||
if isinstance(option, Removed):
|
||||
continue
|
||||
if option.supports_weighting:
|
||||
if isinstance(option, Range):
|
||||
option.value = self.random.randint(option.range_start, option.range_end)
|
||||
else:
|
||||
option.value = self.random.choice(list(option.name_lookup))
|
||||
|
||||
check_options(self)
|
||||
|
||||
if self.options.logic_rules >= LogicRules.option_no_major_glitches:
|
||||
self.options.laurels_zips.value = LaurelsZips.option_true
|
||||
self.options.ice_grappling.value = IceGrappling.option_medium
|
||||
if self.options.logic_rules.value == LogicRules.option_unrestricted:
|
||||
self.options.ladder_storage.value = LadderStorage.option_medium
|
||||
|
||||
self.er_regions = tunic_er_regions.copy()
|
||||
if self.options.plando_connections and not self.options.entrance_rando:
|
||||
self.options.plando_connections.value = ()
|
||||
if self.options.plando_connections:
|
||||
def replace_connection(old_cxn: PlandoConnection, new_cxn: PlandoConnection, index: int) -> None:
|
||||
self.options.plando_connections.value.remove(old_cxn)
|
||||
self.options.plando_connections.value.insert(index, new_cxn)
|
||||
|
||||
for index, cxn in enumerate(self.options.plando_connections):
|
||||
# making shops second to simplify other things later
|
||||
if cxn.entrance.startswith("Shop"):
|
||||
replacement = PlandoConnection(cxn.exit, "Shop Portal", "both")
|
||||
self.options.plando_connections.value.remove(cxn)
|
||||
self.options.plando_connections.value.insert(index, replacement)
|
||||
elif cxn.exit.startswith("Shop"):
|
||||
replacement = PlandoConnection(cxn.entrance, "Shop Portal", "both")
|
||||
self.options.plando_connections.value.remove(cxn)
|
||||
self.options.plando_connections.value.insert(index, replacement)
|
||||
replacement = None
|
||||
if self.options.decoupled:
|
||||
# flip any that are pointing to exit to point to entrance so that I don't have to deal with it
|
||||
if cxn.direction == "exit":
|
||||
replacement = PlandoConnection(cxn.exit, cxn.entrance, "entrance", cxn.percentage)
|
||||
# if decoupled is on and you plando'd an entrance to itself but left the direction as both
|
||||
if cxn.direction == "both" and cxn.entrance == cxn.exit:
|
||||
replacement = PlandoConnection(cxn.entrance, cxn.exit, "entrance")
|
||||
# if decoupled is off, just convert these to both
|
||||
elif cxn.direction != "both":
|
||||
replacement = PlandoConnection(cxn.entrance, cxn.exit, "both", cxn.percentage)
|
||||
|
||||
if replacement:
|
||||
replace_connection(cxn, replacement, index)
|
||||
|
||||
if (self.options.entrance_layout == EntranceLayout.option_direction_pairs
|
||||
and not verify_plando_directions(cxn)):
|
||||
raise OptionError(f"TUNIC: Player {self.player_name} has invalid plando connections. "
|
||||
f"They have Direction Pairs enabled and the connection "
|
||||
f"{cxn.entrance} --> {cxn.exit} does not abide by this option.")
|
||||
|
||||
# Universal tracker stuff, shouldn't do anything in standard gen
|
||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||
@@ -160,16 +187,16 @@ class TunicWorld(World):
|
||||
self.options.hexagon_quest_ability_type.value = self.passthrough.get("hexagon_quest_ability_type", 0)
|
||||
self.options.entrance_rando.value = self.passthrough["entrance_rando"]
|
||||
self.options.shuffle_ladders.value = self.passthrough["shuffle_ladders"]
|
||||
self.options.entrance_layout.value = EntranceLayout.option_standard
|
||||
if ("ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].keys()
|
||||
or "ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].values()):
|
||||
self.options.entrance_layout.value = EntranceLayout.option_fixed_shop
|
||||
self.options.decoupled = self.passthrough.get("decoupled", 0)
|
||||
self.options.laurels_location.value = LaurelsLocation.option_anywhere
|
||||
self.options.grass_randomizer.value = self.passthrough.get("grass_randomizer", 0)
|
||||
self.options.breakable_shuffle.value = self.passthrough.get("breakable_shuffle", 0)
|
||||
self.options.laurels_location.value = self.options.laurels_location.option_anywhere
|
||||
self.options.combat_logic.value = self.passthrough["combat_logic"]
|
||||
|
||||
self.options.fixed_shop.value = self.options.fixed_shop.option_false
|
||||
if ("ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].keys()
|
||||
or "ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].values()):
|
||||
self.options.fixed_shop.value = self.options.fixed_shop.option_true
|
||||
|
||||
self.options.combat_logic.value = self.passthrough.get("combat_logic", 0)
|
||||
else:
|
||||
self.using_ut = False
|
||||
else:
|
||||
@@ -227,10 +254,14 @@ class TunicWorld(World):
|
||||
ice_grappling=tunic.options.ice_grappling.value,
|
||||
ladder_storage=tunic.options.ladder_storage.value,
|
||||
laurels_at_10_fairies=tunic.options.laurels_location == LaurelsLocation.option_10_fairies,
|
||||
fixed_shop=bool(tunic.options.fixed_shop),
|
||||
plando=tunic.options.plando_connections)
|
||||
entrance_layout=tunic.options.entrance_layout.value,
|
||||
has_decoupled_enabled=bool(tunic.options.decoupled),
|
||||
plando=tunic.options.plando_connections.value.copy())
|
||||
continue
|
||||
|
||||
# I feel that syncing this one is worse than erroring out
|
||||
if bool(tunic.options.decoupled) != cls.seed_groups[group]["has_decoupled_enabled"]:
|
||||
raise OptionError(f"TUNIC: All players in the seed group {group} must "
|
||||
f"have Decoupled either enabled or disabled.")
|
||||
# off is more restrictive
|
||||
if not tunic.options.laurels_zips:
|
||||
cls.seed_groups[group]["laurels_zips"] = False
|
||||
@@ -243,34 +274,52 @@ class TunicWorld(World):
|
||||
# laurels at 10 fairies changes logic for secret gathering place placement
|
||||
if tunic.options.laurels_location == 3:
|
||||
cls.seed_groups[group]["laurels_at_10_fairies"] = True
|
||||
# more restrictive, overrides the option for others in the same group, which is better than failing imo
|
||||
if tunic.options.fixed_shop:
|
||||
cls.seed_groups[group]["fixed_shop"] = True
|
||||
|
||||
# fixed shop and direction pairs override standard, but conflict with each other
|
||||
if tunic.options.entrance_layout:
|
||||
if cls.seed_groups[group]["entrance_layout"] == EntranceLayout.option_standard:
|
||||
cls.seed_groups[group]["entrance_layout"] = tunic.options.entrance_layout.value
|
||||
elif cls.seed_groups[group]["entrance_layout"] != tunic.options.entrance_layout.value:
|
||||
raise OptionError(f"TUNIC: Conflict between seed group {group}'s Entrance Layout options. "
|
||||
f"Seed group cannot have both Fixed Shop and Direction Pairs enabled.")
|
||||
if tunic.options.plando_connections:
|
||||
# loop through the connections in the player's yaml
|
||||
for cxn in tunic.options.plando_connections:
|
||||
for index, player_cxn in enumerate(tunic.options.plando_connections):
|
||||
new_cxn = True
|
||||
for group_cxn in cls.seed_groups[group]["plando"]:
|
||||
# if neither entrance nor exit match anything in the group, add to group
|
||||
if ((cxn.entrance == group_cxn.entrance and cxn.exit == group_cxn.exit)
|
||||
or (cxn.exit == group_cxn.entrance and cxn.entrance == group_cxn.exit)):
|
||||
new_cxn = False
|
||||
break
|
||||
|
||||
# verify that it abides by direction pairs if enabled
|
||||
if (cls.seed_groups[group]["entrance_layout"] == EntranceLayout.option_direction_pairs
|
||||
and not verify_plando_directions(player_cxn)):
|
||||
player_dir = "<->" if player_cxn.direction == "both" else "-->"
|
||||
raise Exception(f"TUNIC: Conflict between Entrance Layout option and Plando Connection: "
|
||||
f"{player_cxn.entrance} {player_dir} {player_cxn.exit}")
|
||||
# check if this pair is the same as a pair in the group already
|
||||
if ((player_cxn.entrance == group_cxn.entrance and player_cxn.exit == group_cxn.exit)
|
||||
or (player_cxn.entrance == group_cxn.exit and player_cxn.exit == group_cxn.entrance
|
||||
and "both" in [player_cxn.direction, group_cxn.direction])):
|
||||
new_cxn = False
|
||||
# if the group's was one-way and the player's was two-way, we replace the group's now
|
||||
if player_cxn.direction == "both" and group_cxn.direction == "entrance":
|
||||
cls.seed_groups[group]["plando"].remove(group_cxn)
|
||||
cls.seed_groups[group]["plando"].insert(index, player_cxn)
|
||||
break
|
||||
is_mismatched = (
|
||||
cxn.entrance == group_cxn.entrance and cxn.exit != group_cxn.exit
|
||||
or cxn.entrance == group_cxn.exit and cxn.exit != group_cxn.entrance
|
||||
or cxn.exit == group_cxn.entrance and cxn.entrance != group_cxn.exit
|
||||
or cxn.exit == group_cxn.exit and cxn.entrance != group_cxn.entrance
|
||||
player_cxn.entrance == group_cxn.entrance and player_cxn.exit != group_cxn.exit
|
||||
or player_cxn.exit == group_cxn.exit and player_cxn.entrance != group_cxn.entrance
|
||||
)
|
||||
if not tunic.options.decoupled:
|
||||
is_mismatched = is_mismatched or (
|
||||
player_cxn.entrance == group_cxn.exit and player_cxn.exit != group_cxn.entrance
|
||||
or player_cxn.exit == group_cxn.entrance and player_cxn.entrance != group_cxn.exit
|
||||
)
|
||||
if is_mismatched:
|
||||
raise Exception(f"TUNIC: Conflict between seed group {group}'s plando "
|
||||
f"connection {group_cxn.entrance} <-> {group_cxn.exit} and "
|
||||
f"{tunic.player_name}'s plando connection {cxn.entrance} <-> {cxn.exit}")
|
||||
group_dir = "<->" if group_cxn.direction == "both" else "-->"
|
||||
player_dir = "<->" if player_cxn.direction == "both" else "-->"
|
||||
raise OptionError(f"TUNIC: Conflict between seed group {group}'s plando "
|
||||
f"connection {group_cxn.entrance} {group_dir} {group_cxn.exit} and "
|
||||
f"{tunic.player_name}'s plando connection "
|
||||
f"{player_cxn.entrance} {player_dir} {player_cxn.exit}")
|
||||
if new_cxn:
|
||||
cls.seed_groups[group]["plando"].value.append(cxn)
|
||||
cls.seed_groups[group]["plando"].append(player_cxn)
|
||||
|
||||
def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem:
|
||||
item_data = item_table[name]
|
||||
@@ -571,7 +620,7 @@ class TunicWorld(World):
|
||||
all_state = self.multiworld.get_all_state(True)
|
||||
all_state.update_reachable_regions(self.player)
|
||||
paths = all_state.path
|
||||
portal_names = [portal.name for portal in portal_mapping]
|
||||
portal_names = {portal.name for portal in portal_mapping}.union({f"Shop Portal {i + 1}" for i in range(500)})
|
||||
for location in self.multiworld.get_locations(self.player):
|
||||
# skipping event locations
|
||||
if not location.address:
|
||||
@@ -630,6 +679,7 @@ class TunicWorld(World):
|
||||
"lanternless": self.options.lanternless.value,
|
||||
"maskless": self.options.maskless.value,
|
||||
"entrance_rando": int(bool(self.options.entrance_rando.value)),
|
||||
"decoupled": self.options.decoupled.value if self.options.entrance_rando else 0,
|
||||
"shuffle_ladders": self.options.shuffle_ladders.value,
|
||||
"grass_randomizer": self.options.grass_randomizer.value,
|
||||
"combat_logic": self.options.combat_logic.value,
|
||||
|
||||
@@ -22,6 +22,7 @@ class AreaStats(NamedTuple):
|
||||
|
||||
# the vanilla upgrades/equipment you would have
|
||||
area_data: Dict[str, AreaStats] = {
|
||||
# The upgrade page is right by the Well entrance. Upper Overworld by the chest in the top right might need something
|
||||
"Overworld": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Stick"]),
|
||||
"East Forest": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Sword"]),
|
||||
"Before Well": AreaStats(1, 1, 1, 1, 1, 1, 3, ["Sword", "Shield"]),
|
||||
|
||||
@@ -83,7 +83,7 @@ Notes:
|
||||
- The Entrance Randomizer option must be enabled for it to work.
|
||||
- The `direction` field is not supported. Connections are always coupled.
|
||||
- For a list of entrance names, check `er_data.py` in the TUNIC world folder or generate a game with the Entrance Randomizer option enabled and check the spoiler log.
|
||||
- There is no limit to the number of Shops you can plando.
|
||||
- You can plando up to 500 additional shops in Decoupled. You should not do this.
|
||||
|
||||
See the [Archipelago Plando Guide](../../../tutorial/Archipelago/plando/en) for more information on Plando and Connection Plando.
|
||||
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
from typing import Dict, NamedTuple, List, TYPE_CHECKING, Optional
|
||||
from typing import Dict, NamedTuple, List, Optional, TYPE_CHECKING
|
||||
from enum import IntEnum
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TunicWorld
|
||||
|
||||
|
||||
# the direction you go to enter a portal
|
||||
class Direction(IntEnum):
|
||||
none = 0 # for when the direction isn't relevant
|
||||
north = 1
|
||||
south = 2
|
||||
east = 3
|
||||
west = 4
|
||||
floor = 5
|
||||
ladder_up = 6
|
||||
ladder_down = 7
|
||||
|
||||
|
||||
class Portal(NamedTuple):
|
||||
name: str # human-readable name
|
||||
region: str # AP region
|
||||
destination: str # vanilla destination scene
|
||||
tag: str # vanilla tag
|
||||
direction: int # the direction you go to enter a portal
|
||||
|
||||
def scene(self) -> str: # the actual scene name in Tunic
|
||||
if self.region.startswith("Shop"):
|
||||
@@ -25,497 +38,497 @@ class Portal(NamedTuple):
|
||||
|
||||
portal_mapping: List[Portal] = [
|
||||
Portal(name="Stick House Entrance", region="Overworld",
|
||||
destination="Sword Cave", tag="_"),
|
||||
destination="Sword Cave", tag="_", direction=Direction.north),
|
||||
Portal(name="Windmill Entrance", region="Overworld",
|
||||
destination="Windmill", tag="_"),
|
||||
destination="Windmill", tag="_", direction=Direction.north),
|
||||
Portal(name="Well Ladder Entrance", region="Overworld Well Ladder",
|
||||
destination="Sewer", tag="_entrance"),
|
||||
destination="Sewer", tag="_entrance", direction=Direction.ladder_down),
|
||||
Portal(name="Entrance to Well from Well Rail", region="Overworld Well to Furnace Rail",
|
||||
destination="Sewer", tag="_west_aqueduct"),
|
||||
destination="Sewer", tag="_west_aqueduct", direction=Direction.north),
|
||||
Portal(name="Old House Door Entrance", region="Overworld Old House Door",
|
||||
destination="Overworld Interiors", tag="_house"),
|
||||
destination="Overworld Interiors", tag="_house", direction=Direction.east),
|
||||
Portal(name="Old House Waterfall Entrance", region="Overworld",
|
||||
destination="Overworld Interiors", tag="_under_checkpoint"),
|
||||
destination="Overworld Interiors", tag="_under_checkpoint", direction=Direction.east),
|
||||
Portal(name="Entrance to Furnace from Well Rail", region="Overworld Well to Furnace Rail",
|
||||
destination="Furnace", tag="_gyro_upper_north"),
|
||||
destination="Furnace", tag="_gyro_upper_north", direction=Direction.south),
|
||||
Portal(name="Entrance to Furnace under Windmill", region="Overworld",
|
||||
destination="Furnace", tag="_gyro_upper_east"),
|
||||
destination="Furnace", tag="_gyro_upper_east", direction=Direction.west),
|
||||
Portal(name="Entrance to Furnace near West Garden", region="Overworld to West Garden from Furnace",
|
||||
destination="Furnace", tag="_gyro_west"),
|
||||
destination="Furnace", tag="_gyro_west", direction=Direction.east),
|
||||
Portal(name="Entrance to Furnace from Beach", region="Overworld Tunnel Turret",
|
||||
destination="Furnace", tag="_gyro_lower"),
|
||||
destination="Furnace", tag="_gyro_lower", direction=Direction.north),
|
||||
Portal(name="Caustic Light Cave Entrance", region="Overworld Swamp Lower Entry",
|
||||
destination="Overworld Cave", tag="_"),
|
||||
destination="Overworld Cave", tag="_", direction=Direction.north),
|
||||
Portal(name="Swamp Upper Entrance", region="Overworld Swamp Upper Entry",
|
||||
destination="Swamp Redux 2", tag="_wall"),
|
||||
destination="Swamp Redux 2", tag="_wall", direction=Direction.south),
|
||||
Portal(name="Swamp Lower Entrance", region="Overworld Swamp Lower Entry",
|
||||
destination="Swamp Redux 2", tag="_conduit"),
|
||||
destination="Swamp Redux 2", tag="_conduit", direction=Direction.south),
|
||||
Portal(name="Ruined Passage Not-Door Entrance", region="After Ruined Passage",
|
||||
destination="Ruins Passage", tag="_east"),
|
||||
destination="Ruins Passage", tag="_east", direction=Direction.north),
|
||||
Portal(name="Ruined Passage Door Entrance", region="Overworld Ruined Passage Door",
|
||||
destination="Ruins Passage", tag="_west"),
|
||||
destination="Ruins Passage", tag="_west", direction=Direction.east),
|
||||
Portal(name="Atoll Upper Entrance", region="Overworld to Atoll Upper",
|
||||
destination="Atoll Redux", tag="_upper"),
|
||||
destination="Atoll Redux", tag="_upper", direction=Direction.south),
|
||||
Portal(name="Atoll Lower Entrance", region="Overworld Beach",
|
||||
destination="Atoll Redux", tag="_lower"),
|
||||
destination="Atoll Redux", tag="_lower", direction=Direction.south),
|
||||
Portal(name="Special Shop Entrance", region="Overworld Special Shop Entry",
|
||||
destination="ShopSpecial", tag="_"),
|
||||
destination="ShopSpecial", tag="_", direction=Direction.east),
|
||||
Portal(name="Maze Cave Entrance", region="Overworld Beach",
|
||||
destination="Maze Room", tag="_"),
|
||||
destination="Maze Room", tag="_", direction=Direction.north),
|
||||
Portal(name="West Garden Entrance near Belltower", region="Overworld to West Garden Upper",
|
||||
destination="Archipelagos Redux", tag="_upper"),
|
||||
destination="Archipelagos Redux", tag="_upper", direction=Direction.west),
|
||||
Portal(name="West Garden Entrance from Furnace", region="Overworld to West Garden from Furnace",
|
||||
destination="Archipelagos Redux", tag="_lower"),
|
||||
destination="Archipelagos Redux", tag="_lower", direction=Direction.west),
|
||||
Portal(name="West Garden Laurels Entrance", region="Overworld West Garden Laurels Entry",
|
||||
destination="Archipelagos Redux", tag="_lowest"),
|
||||
destination="Archipelagos Redux", tag="_lowest", direction=Direction.west),
|
||||
Portal(name="Temple Door Entrance", region="Overworld Temple Door",
|
||||
destination="Temple", tag="_main"),
|
||||
destination="Temple", tag="_main", direction=Direction.north),
|
||||
Portal(name="Temple Rafters Entrance", region="Overworld after Temple Rafters",
|
||||
destination="Temple", tag="_rafters"),
|
||||
destination="Temple", tag="_rafters", direction=Direction.east),
|
||||
Portal(name="Ruined Shop Entrance", region="Overworld",
|
||||
destination="Ruined Shop", tag="_"),
|
||||
destination="Ruined Shop", tag="_", direction=Direction.east),
|
||||
Portal(name="Patrol Cave Entrance", region="Overworld at Patrol Cave",
|
||||
destination="PatrolCave", tag="_"),
|
||||
destination="PatrolCave", tag="_", direction=Direction.north),
|
||||
Portal(name="Hourglass Cave Entrance", region="Overworld Beach",
|
||||
destination="Town Basement", tag="_beach"),
|
||||
destination="Town Basement", tag="_beach", direction=Direction.north),
|
||||
Portal(name="Changing Room Entrance", region="Overworld",
|
||||
destination="Changing Room", tag="_"),
|
||||
destination="Changing Room", tag="_", direction=Direction.south),
|
||||
Portal(name="Cube Cave Entrance", region="Cube Cave Entrance Region",
|
||||
destination="CubeRoom", tag="_"),
|
||||
destination="CubeRoom", tag="_", direction=Direction.north),
|
||||
Portal(name="Stairs from Overworld to Mountain", region="Upper Overworld",
|
||||
destination="Mountain", tag="_"),
|
||||
destination="Mountain", tag="_", direction=Direction.north),
|
||||
Portal(name="Overworld to Fortress", region="East Overworld",
|
||||
destination="Fortress Courtyard", tag="_"),
|
||||
destination="Fortress Courtyard", tag="_", direction=Direction.east),
|
||||
Portal(name="Fountain HC Door Entrance", region="Overworld Fountain Cross Door",
|
||||
destination="Town_FiligreeRoom", tag="_"),
|
||||
destination="Town_FiligreeRoom", tag="_", direction=Direction.north),
|
||||
Portal(name="Southeast HC Door Entrance", region="Overworld Southeast Cross Door",
|
||||
destination="EastFiligreeCache", tag="_"),
|
||||
destination="EastFiligreeCache", tag="_", direction=Direction.north),
|
||||
Portal(name="Overworld to Quarry Connector", region="Overworld Quarry Entry",
|
||||
destination="Darkwoods Tunnel", tag="_"),
|
||||
destination="Darkwoods Tunnel", tag="_", direction=Direction.north),
|
||||
Portal(name="Dark Tomb Main Entrance", region="Overworld",
|
||||
destination="Crypt Redux", tag="_"),
|
||||
destination="Crypt Redux", tag="_", direction=Direction.north),
|
||||
Portal(name="Overworld to Forest Belltower", region="East Overworld",
|
||||
destination="Forest Belltower", tag="_"),
|
||||
destination="Forest Belltower", tag="_", direction=Direction.east),
|
||||
Portal(name="Town to Far Shore", region="Overworld Town Portal",
|
||||
destination="Transit", tag="_teleporter_town"),
|
||||
destination="Transit", tag="_teleporter_town", direction=Direction.floor),
|
||||
Portal(name="Spawn to Far Shore", region="Overworld Spawn Portal",
|
||||
destination="Transit", tag="_teleporter_starting island"),
|
||||
destination="Transit", tag="_teleporter_starting island", direction=Direction.floor),
|
||||
Portal(name="Secret Gathering Place Entrance", region="Overworld",
|
||||
destination="Waterfall", tag="_"),
|
||||
|
||||
destination="Waterfall", tag="_", direction=Direction.north),
|
||||
|
||||
Portal(name="Secret Gathering Place Exit", region="Secret Gathering Place",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
|
||||
destination="Overworld Redux", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Windmill Exit", region="Windmill",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
destination="Overworld Redux", tag="_", direction=Direction.south),
|
||||
Portal(name="Windmill Shop", region="Windmill",
|
||||
destination="Shop", tag="_"),
|
||||
|
||||
destination="Shop", tag="_", direction=Direction.north),
|
||||
|
||||
Portal(name="Old House Door Exit", region="Old House Front",
|
||||
destination="Overworld Redux", tag="_house"),
|
||||
destination="Overworld Redux", tag="_house", direction=Direction.west),
|
||||
Portal(name="Old House to Glyph Tower", region="Old House Front",
|
||||
destination="g_elements", tag="_"),
|
||||
destination="g_elements", tag="_", direction=Direction.south), # portal drops you on north side
|
||||
Portal(name="Old House Waterfall Exit", region="Old House Back",
|
||||
destination="Overworld Redux", tag="_under_checkpoint"),
|
||||
|
||||
destination="Overworld Redux", tag="_under_checkpoint", direction=Direction.west),
|
||||
|
||||
Portal(name="Glyph Tower Exit", region="Relic Tower",
|
||||
destination="Overworld Interiors", tag="_"),
|
||||
|
||||
destination="Overworld Interiors", tag="_", direction=Direction.north),
|
||||
|
||||
Portal(name="Changing Room Exit", region="Changing Room",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
|
||||
destination="Overworld Redux", tag="_", direction=Direction.north),
|
||||
|
||||
Portal(name="Fountain HC Room Exit", region="Fountain Cross Room",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
|
||||
destination="Overworld Redux", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Cube Cave Exit", region="Cube Cave",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
|
||||
destination="Overworld Redux", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Guard Patrol Cave Exit", region="Patrol Cave",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
|
||||
destination="Overworld Redux", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Ruined Shop Exit", region="Ruined Shop",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
|
||||
destination="Overworld Redux", tag="_", direction=Direction.west),
|
||||
|
||||
Portal(name="Furnace Exit towards Well", region="Furnace Fuse",
|
||||
destination="Overworld Redux", tag="_gyro_upper_north"),
|
||||
destination="Overworld Redux", tag="_gyro_upper_north", direction=Direction.north),
|
||||
Portal(name="Furnace Exit to Dark Tomb", region="Furnace Walking Path",
|
||||
destination="Crypt Redux", tag="_"),
|
||||
destination="Crypt Redux", tag="_", direction=Direction.east),
|
||||
Portal(name="Furnace Exit towards West Garden", region="Furnace Walking Path",
|
||||
destination="Overworld Redux", tag="_gyro_west"),
|
||||
destination="Overworld Redux", tag="_gyro_west", direction=Direction.west),
|
||||
Portal(name="Furnace Exit to Beach", region="Furnace Ladder Area",
|
||||
destination="Overworld Redux", tag="_gyro_lower"),
|
||||
destination="Overworld Redux", tag="_gyro_lower", direction=Direction.south),
|
||||
Portal(name="Furnace Exit under Windmill", region="Furnace Ladder Area",
|
||||
destination="Overworld Redux", tag="_gyro_upper_east"),
|
||||
|
||||
destination="Overworld Redux", tag="_gyro_upper_east", direction=Direction.east),
|
||||
|
||||
Portal(name="Stick House Exit", region="Stick House",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
|
||||
destination="Overworld Redux", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Ruined Passage Not-Door Exit", region="Ruined Passage",
|
||||
destination="Overworld Redux", tag="_east"),
|
||||
destination="Overworld Redux", tag="_east", direction=Direction.south),
|
||||
Portal(name="Ruined Passage Door Exit", region="Ruined Passage",
|
||||
destination="Overworld Redux", tag="_west"),
|
||||
|
||||
destination="Overworld Redux", tag="_west", direction=Direction.west),
|
||||
|
||||
Portal(name="Southeast HC Room Exit", region="Southeast Cross Room",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
|
||||
destination="Overworld Redux", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Caustic Light Cave Exit", region="Caustic Light Cave",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
|
||||
destination="Overworld Redux", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Maze Cave Exit", region="Maze Cave",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
|
||||
destination="Overworld Redux", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Hourglass Cave Exit", region="Hourglass Cave",
|
||||
destination="Overworld Redux", tag="_beach"),
|
||||
|
||||
destination="Overworld Redux", tag="_beach", direction=Direction.south),
|
||||
|
||||
Portal(name="Special Shop Exit", region="Special Shop",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
|
||||
destination="Overworld Redux", tag="_", direction=Direction.west),
|
||||
|
||||
Portal(name="Temple Rafters Exit", region="Sealed Temple Rafters",
|
||||
destination="Overworld Redux", tag="_rafters"),
|
||||
destination="Overworld Redux", tag="_rafters", direction=Direction.west),
|
||||
Portal(name="Temple Door Exit", region="Sealed Temple",
|
||||
destination="Overworld Redux", tag="_main"),
|
||||
destination="Overworld Redux", tag="_main", direction=Direction.south),
|
||||
|
||||
Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main behind bushes",
|
||||
destination="Fortress Courtyard", tag="_"),
|
||||
destination="Fortress Courtyard", tag="_", direction=Direction.north),
|
||||
Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower",
|
||||
destination="East Forest Redux", tag="_"),
|
||||
destination="East Forest Redux", tag="_", direction=Direction.south),
|
||||
Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
destination="Overworld Redux", tag="_", direction=Direction.west),
|
||||
Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper",
|
||||
destination="Forest Boss Room", tag="_"),
|
||||
destination="Forest Boss Room", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Forest to Belltower", region="East Forest",
|
||||
destination="Forest Belltower", tag="_"),
|
||||
destination="Forest Belltower", tag="_", direction=Direction.north),
|
||||
Portal(name="Forest Guard House 1 Lower Entrance", region="East Forest",
|
||||
destination="East Forest Redux Laddercave", tag="_lower"),
|
||||
destination="East Forest Redux Laddercave", tag="_lower", direction=Direction.north),
|
||||
Portal(name="Forest Guard House 1 Gate Entrance", region="East Forest",
|
||||
destination="East Forest Redux Laddercave", tag="_gate"),
|
||||
destination="East Forest Redux Laddercave", tag="_gate", direction=Direction.north),
|
||||
Portal(name="Forest Dance Fox Outside Doorway", region="East Forest Dance Fox Spot",
|
||||
destination="East Forest Redux Laddercave", tag="_upper"),
|
||||
destination="East Forest Redux Laddercave", tag="_upper", direction=Direction.east),
|
||||
Portal(name="Forest to Far Shore", region="East Forest Portal",
|
||||
destination="Transit", tag="_teleporter_forest teleporter"),
|
||||
destination="Transit", tag="_teleporter_forest teleporter", direction=Direction.floor),
|
||||
Portal(name="Forest Guard House 2 Lower Entrance", region="Lower Forest",
|
||||
destination="East Forest Redux Interior", tag="_lower"),
|
||||
destination="East Forest Redux Interior", tag="_lower", direction=Direction.north),
|
||||
Portal(name="Forest Guard House 2 Upper Entrance", region="East Forest",
|
||||
destination="East Forest Redux Interior", tag="_upper"),
|
||||
destination="East Forest Redux Interior", tag="_upper", direction=Direction.east),
|
||||
Portal(name="Forest Grave Path Lower Entrance", region="East Forest",
|
||||
destination="Sword Access", tag="_lower"),
|
||||
destination="Sword Access", tag="_lower", direction=Direction.east),
|
||||
Portal(name="Forest Grave Path Upper Entrance", region="East Forest",
|
||||
destination="Sword Access", tag="_upper"),
|
||||
destination="Sword Access", tag="_upper", direction=Direction.east),
|
||||
|
||||
Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper",
|
||||
destination="East Forest Redux", tag="_upper"),
|
||||
destination="East Forest Redux", tag="_upper", direction=Direction.west),
|
||||
Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main",
|
||||
destination="East Forest Redux", tag="_lower"),
|
||||
destination="East Forest Redux", tag="_lower", direction=Direction.west),
|
||||
Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave",
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth"),
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor),
|
||||
|
||||
Portal(name="Guard House 1 Dance Fox Exit", region="Guard House 1 West",
|
||||
destination="East Forest Redux", tag="_upper"),
|
||||
destination="East Forest Redux", tag="_upper", direction=Direction.west),
|
||||
Portal(name="Guard House 1 Lower Exit", region="Guard House 1 West",
|
||||
destination="East Forest Redux", tag="_lower"),
|
||||
destination="East Forest Redux", tag="_lower", direction=Direction.south),
|
||||
Portal(name="Guard House 1 Upper Forest Exit", region="Guard House 1 East",
|
||||
destination="East Forest Redux", tag="_gate"),
|
||||
destination="East Forest Redux", tag="_gate", direction=Direction.south),
|
||||
Portal(name="Guard House 1 to Guard Captain Room", region="Guard House 1 East",
|
||||
destination="Forest Boss Room", tag="_"),
|
||||
destination="Forest Boss Room", tag="_", direction=Direction.north),
|
||||
|
||||
Portal(name="Guard House 2 Lower Exit", region="Guard House 2 Lower",
|
||||
destination="East Forest Redux", tag="_lower"),
|
||||
destination="East Forest Redux", tag="_lower", direction=Direction.south),
|
||||
Portal(name="Guard House 2 Upper Exit", region="Guard House 2 Upper before bushes",
|
||||
destination="East Forest Redux", tag="_upper"),
|
||||
destination="East Forest Redux", tag="_upper", direction=Direction.west),
|
||||
|
||||
Portal(name="Guard Captain Room Non-Gate Exit", region="Forest Boss Room",
|
||||
destination="East Forest Redux Laddercave", tag="_"),
|
||||
destination="East Forest Redux Laddercave", tag="_", direction=Direction.south),
|
||||
Portal(name="Guard Captain Room Gate Exit", region="Forest Boss Room",
|
||||
destination="Forest Belltower", tag="_"),
|
||||
destination="Forest Belltower", tag="_", direction=Direction.north),
|
||||
|
||||
Portal(name="Well Ladder Exit", region="Beneath the Well Ladder Exit",
|
||||
destination="Overworld Redux", tag="_entrance"),
|
||||
destination="Overworld Redux", tag="_entrance", direction=Direction.ladder_up),
|
||||
Portal(name="Well to Well Boss", region="Beneath the Well Back",
|
||||
destination="Sewer_Boss", tag="_"),
|
||||
destination="Sewer_Boss", tag="_", direction=Direction.east),
|
||||
Portal(name="Well Exit towards Furnace", region="Beneath the Well Back",
|
||||
destination="Overworld Redux", tag="_west_aqueduct"),
|
||||
destination="Overworld Redux", tag="_west_aqueduct", direction=Direction.south),
|
||||
|
||||
Portal(name="Well Boss to Well", region="Well Boss",
|
||||
destination="Sewer", tag="_"),
|
||||
destination="Sewer", tag="_", direction=Direction.west),
|
||||
Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint",
|
||||
destination="Crypt Redux", tag="_"),
|
||||
destination="Crypt Redux", tag="_", direction=Direction.ladder_up),
|
||||
|
||||
Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
destination="Overworld Redux", tag="_", direction=Direction.south),
|
||||
Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit",
|
||||
destination="Furnace", tag="_"),
|
||||
destination="Furnace", tag="_", direction=Direction.west),
|
||||
Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point",
|
||||
destination="Sewer_Boss", tag="_"),
|
||||
|
||||
destination="Sewer_Boss", tag="_", direction=Direction.ladder_down),
|
||||
|
||||
Portal(name="West Garden Exit near Hero's Grave", region="West Garden before Terry",
|
||||
destination="Overworld Redux", tag="_lower"),
|
||||
destination="Overworld Redux", tag="_lower", direction=Direction.east),
|
||||
Portal(name="West Garden to Magic Dagger House", region="West Garden at Dagger House",
|
||||
destination="archipelagos_house", tag="_"),
|
||||
destination="archipelagos_house", tag="_", direction=Direction.east),
|
||||
Portal(name="West Garden Exit after Boss", region="West Garden after Boss",
|
||||
destination="Overworld Redux", tag="_upper"),
|
||||
destination="Overworld Redux", tag="_upper", direction=Direction.east),
|
||||
Portal(name="West Garden Shop", region="West Garden before Terry",
|
||||
destination="Shop", tag="_"),
|
||||
destination="Shop", tag="_", direction=Direction.east),
|
||||
Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit Region",
|
||||
destination="Overworld Redux", tag="_lowest"),
|
||||
destination="Overworld Redux", tag="_lowest", direction=Direction.east),
|
||||
Portal(name="West Garden Hero's Grave", region="West Garden Hero's Grave Region",
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth"),
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor),
|
||||
Portal(name="West Garden to Far Shore", region="West Garden Portal",
|
||||
destination="Transit", tag="_teleporter_archipelagos_teleporter"),
|
||||
destination="Transit", tag="_teleporter_archipelagos_teleporter", direction=Direction.floor),
|
||||
|
||||
Portal(name="Magic Dagger House Exit", region="Magic Dagger House",
|
||||
destination="Archipelagos Redux", tag="_"),
|
||||
destination="Archipelagos Redux", tag="_", direction=Direction.west),
|
||||
|
||||
Portal(name="Fortress Courtyard to Fortress Grave Path Lower", region="Fortress Courtyard",
|
||||
destination="Fortress Reliquary", tag="_Lower"),
|
||||
destination="Fortress Reliquary", tag="_Lower", direction=Direction.east),
|
||||
Portal(name="Fortress Courtyard to Fortress Grave Path Upper", region="Fortress Courtyard Upper",
|
||||
destination="Fortress Reliquary", tag="_Upper"),
|
||||
destination="Fortress Reliquary", tag="_Upper", direction=Direction.east),
|
||||
Portal(name="Fortress Courtyard to Fortress Interior", region="Fortress Courtyard",
|
||||
destination="Fortress Main", tag="_Big Door"),
|
||||
destination="Fortress Main", tag="_Big Door", direction=Direction.north),
|
||||
Portal(name="Fortress Courtyard to East Fortress", region="Fortress Courtyard Upper",
|
||||
destination="Fortress East", tag="_"),
|
||||
destination="Fortress East", tag="_", direction=Direction.north),
|
||||
Portal(name="Fortress Courtyard to Beneath the Vault", region="Beneath the Vault Entry",
|
||||
destination="Fortress Basement", tag="_"),
|
||||
destination="Fortress Basement", tag="_", direction=Direction.ladder_down),
|
||||
Portal(name="Fortress Courtyard to Forest Belltower", region="Fortress Exterior from East Forest",
|
||||
destination="Forest Belltower", tag="_"),
|
||||
destination="Forest Belltower", tag="_", direction=Direction.south),
|
||||
Portal(name="Fortress Courtyard to Overworld", region="Fortress Exterior from Overworld",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
destination="Overworld Redux", tag="_", direction=Direction.west),
|
||||
Portal(name="Fortress Courtyard Shop", region="Fortress Exterior near cave",
|
||||
destination="Shop", tag="_"),
|
||||
destination="Shop", tag="_", direction=Direction.north),
|
||||
|
||||
Portal(name="Beneath the Vault to Fortress Interior", region="Beneath the Vault Back",
|
||||
destination="Fortress Main", tag="_"),
|
||||
destination="Fortress Main", tag="_", direction=Direction.east),
|
||||
Portal(name="Beneath the Vault to Fortress Courtyard", region="Beneath the Vault Ladder Exit",
|
||||
destination="Fortress Courtyard", tag="_"),
|
||||
destination="Fortress Courtyard", tag="_", direction=Direction.ladder_up),
|
||||
|
||||
Portal(name="Fortress Interior Main Exit", region="Eastern Vault Fortress",
|
||||
destination="Fortress Courtyard", tag="_Big Door"),
|
||||
destination="Fortress Courtyard", tag="_Big Door", direction=Direction.south),
|
||||
Portal(name="Fortress Interior to Beneath the Earth", region="Eastern Vault Fortress",
|
||||
destination="Fortress Basement", tag="_"),
|
||||
destination="Fortress Basement", tag="_", direction=Direction.west),
|
||||
Portal(name="Fortress Interior to Siege Engine Arena", region="Eastern Vault Fortress Gold Door",
|
||||
destination="Fortress Arena", tag="_"),
|
||||
destination="Fortress Arena", tag="_", direction=Direction.north),
|
||||
Portal(name="Fortress Interior Shop", region="Eastern Vault Fortress",
|
||||
destination="Shop", tag="_"),
|
||||
destination="Shop", tag="_", direction=Direction.north),
|
||||
Portal(name="Fortress Interior to East Fortress Upper", region="Eastern Vault Fortress",
|
||||
destination="Fortress East", tag="_upper"),
|
||||
destination="Fortress East", tag="_upper", direction=Direction.east),
|
||||
Portal(name="Fortress Interior to East Fortress Lower", region="Eastern Vault Fortress",
|
||||
destination="Fortress East", tag="_lower"),
|
||||
destination="Fortress East", tag="_lower", direction=Direction.east),
|
||||
|
||||
Portal(name="East Fortress to Interior Lower", region="Fortress East Shortcut Lower",
|
||||
destination="Fortress Main", tag="_lower"),
|
||||
destination="Fortress Main", tag="_lower", direction=Direction.west),
|
||||
Portal(name="East Fortress to Courtyard", region="Fortress East Shortcut Upper",
|
||||
destination="Fortress Courtyard", tag="_"),
|
||||
destination="Fortress Courtyard", tag="_", direction=Direction.south),
|
||||
Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper",
|
||||
destination="Fortress Main", tag="_upper"),
|
||||
destination="Fortress Main", tag="_upper", direction=Direction.west),
|
||||
|
||||
Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path Entry",
|
||||
destination="Fortress Courtyard", tag="_Lower"),
|
||||
destination="Fortress Courtyard", tag="_Lower", direction=Direction.west),
|
||||
Portal(name="Fortress Hero's Grave", region="Fortress Hero's Grave Region",
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth"),
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor),
|
||||
Portal(name="Fortress Grave Path Upper Exit", region="Fortress Grave Path Upper",
|
||||
destination="Fortress Courtyard", tag="_Upper"),
|
||||
destination="Fortress Courtyard", tag="_Upper", direction=Direction.west),
|
||||
Portal(name="Fortress Grave Path Dusty Entrance", region="Fortress Grave Path Dusty Entrance Region",
|
||||
destination="Dusty", tag="_"),
|
||||
destination="Dusty", tag="_", direction=Direction.north),
|
||||
|
||||
Portal(name="Dusty Exit", region="Fortress Leaf Piles",
|
||||
destination="Fortress Reliquary", tag="_"),
|
||||
destination="Fortress Reliquary", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Siege Engine Arena to Fortress", region="Fortress Arena",
|
||||
destination="Fortress Main", tag="_"),
|
||||
destination="Fortress Main", tag="_", direction=Direction.south),
|
||||
Portal(name="Fortress to Far Shore", region="Fortress Arena Portal",
|
||||
destination="Transit", tag="_teleporter_spidertank"),
|
||||
destination="Transit", tag="_teleporter_spidertank", direction=Direction.floor),
|
||||
|
||||
Portal(name="Atoll Upper Exit", region="Ruined Atoll",
|
||||
destination="Overworld Redux", tag="_upper"),
|
||||
destination="Overworld Redux", tag="_upper", direction=Direction.north),
|
||||
Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area",
|
||||
destination="Overworld Redux", tag="_lower"),
|
||||
destination="Overworld Redux", tag="_lower", direction=Direction.north),
|
||||
Portal(name="Atoll Shop", region="Ruined Atoll",
|
||||
destination="Shop", tag="_"),
|
||||
destination="Shop", tag="_", direction=Direction.north),
|
||||
Portal(name="Atoll to Far Shore", region="Ruined Atoll Portal",
|
||||
destination="Transit", tag="_teleporter_atoll"),
|
||||
destination="Transit", tag="_teleporter_atoll", direction=Direction.floor),
|
||||
Portal(name="Atoll Statue Teleporter", region="Ruined Atoll Statue",
|
||||
destination="Library Exterior", tag="_"),
|
||||
destination="Library Exterior", tag="_", direction=Direction.floor),
|
||||
Portal(name="Frog Stairs Eye Entrance", region="Ruined Atoll Frog Eye",
|
||||
destination="Frog Stairs", tag="_eye"),
|
||||
destination="Frog Stairs", tag="_eye", direction=Direction.south), # camera rotates, it's fine
|
||||
Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth",
|
||||
destination="Frog Stairs", tag="_mouth"),
|
||||
destination="Frog Stairs", tag="_mouth", direction=Direction.east),
|
||||
|
||||
Portal(name="Frog Stairs Eye Exit", region="Frog Stairs Eye Exit",
|
||||
destination="Atoll Redux", tag="_eye"),
|
||||
destination="Atoll Redux", tag="_eye", direction=Direction.north),
|
||||
Portal(name="Frog Stairs Mouth Exit", region="Frog Stairs Upper",
|
||||
destination="Atoll Redux", tag="_mouth"),
|
||||
destination="Atoll Redux", tag="_mouth", direction=Direction.west),
|
||||
Portal(name="Frog Stairs to Frog's Domain's Entrance", region="Frog Stairs to Frog's Domain",
|
||||
destination="frog cave main", tag="_Entrance"),
|
||||
destination="frog cave main", tag="_Entrance", direction=Direction.ladder_down),
|
||||
Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog Stairs Lower",
|
||||
destination="frog cave main", tag="_Exit"),
|
||||
destination="frog cave main", tag="_Exit", direction=Direction.east),
|
||||
|
||||
Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain Entry",
|
||||
destination="Frog Stairs", tag="_Entrance"),
|
||||
destination="Frog Stairs", tag="_Entrance", direction=Direction.ladder_up),
|
||||
Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back",
|
||||
destination="Frog Stairs", tag="_Exit"),
|
||||
destination="Frog Stairs", tag="_Exit", direction=Direction.west),
|
||||
|
||||
Portal(name="Library Exterior Tree", region="Library Exterior Tree Region",
|
||||
destination="Atoll Redux", tag="_"),
|
||||
destination="Atoll Redux", tag="_", direction=Direction.floor),
|
||||
Portal(name="Library Exterior Ladder", region="Library Exterior Ladder Region",
|
||||
destination="Library Hall", tag="_"),
|
||||
destination="Library Hall", tag="_", direction=Direction.west), # camera rotates
|
||||
|
||||
Portal(name="Library Hall Bookshelf Exit", region="Library Hall Bookshelf",
|
||||
destination="Library Exterior", tag="_"),
|
||||
destination="Library Exterior", tag="_", direction=Direction.east),
|
||||
Portal(name="Library Hero's Grave", region="Library Hero's Grave Region",
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth"),
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor),
|
||||
Portal(name="Library Hall to Rotunda", region="Library Hall to Rotunda",
|
||||
destination="Library Rotunda", tag="_"),
|
||||
destination="Library Rotunda", tag="_", direction=Direction.ladder_up),
|
||||
|
||||
Portal(name="Library Rotunda Lower Exit", region="Library Rotunda to Hall",
|
||||
destination="Library Hall", tag="_"),
|
||||
destination="Library Hall", tag="_", direction=Direction.ladder_down),
|
||||
Portal(name="Library Rotunda Upper Exit", region="Library Rotunda to Lab",
|
||||
destination="Library Lab", tag="_"),
|
||||
destination="Library Lab", tag="_", direction=Direction.ladder_up),
|
||||
|
||||
Portal(name="Library Lab to Rotunda", region="Library Lab Lower",
|
||||
destination="Library Rotunda", tag="_"),
|
||||
destination="Library Rotunda", tag="_", direction=Direction.ladder_down),
|
||||
Portal(name="Library to Far Shore", region="Library Portal",
|
||||
destination="Transit", tag="_teleporter_library teleporter"),
|
||||
destination="Transit", tag="_teleporter_library teleporter", direction=Direction.floor),
|
||||
Portal(name="Library Lab to Librarian Arena", region="Library Lab to Librarian",
|
||||
destination="Library Arena", tag="_"),
|
||||
destination="Library Arena", tag="_", direction=Direction.ladder_up),
|
||||
|
||||
Portal(name="Librarian Arena Exit", region="Library Arena",
|
||||
destination="Library Lab", tag="_"),
|
||||
destination="Library Lab", tag="_", direction=Direction.ladder_down),
|
||||
|
||||
Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs",
|
||||
destination="Mountaintop", tag="_"),
|
||||
destination="Mountaintop", tag="_", direction=Direction.north),
|
||||
Portal(name="Mountain to Quarry", region="Lower Mountain",
|
||||
destination="Quarry Redux", tag="_"),
|
||||
destination="Quarry Redux", tag="_", direction=Direction.south), # connecting is north
|
||||
Portal(name="Mountain to Overworld", region="Lower Mountain",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
|
||||
destination="Overworld Redux", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Top of the Mountain Exit", region="Top of the Mountain",
|
||||
destination="Mountain", tag="_"),
|
||||
|
||||
destination="Mountain", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Quarry Connector to Overworld", region="Quarry Connector",
|
||||
destination="Overworld Redux", tag="_"),
|
||||
destination="Overworld Redux", tag="_", direction=Direction.south),
|
||||
Portal(name="Quarry Connector to Quarry", region="Quarry Connector",
|
||||
destination="Quarry Redux", tag="_"),
|
||||
|
||||
destination="Quarry Redux", tag="_", direction=Direction.north), # rotates, it's fine
|
||||
|
||||
Portal(name="Quarry to Overworld Exit", region="Quarry Entry",
|
||||
destination="Darkwoods Tunnel", tag="_"),
|
||||
destination="Darkwoods Tunnel", tag="_", direction=Direction.south), # rotates, it's fine
|
||||
Portal(name="Quarry Shop", region="Quarry Entry",
|
||||
destination="Shop", tag="_"),
|
||||
destination="Shop", tag="_", direction=Direction.north),
|
||||
Portal(name="Quarry to Monastery Front", region="Quarry Monastery Entry",
|
||||
destination="Monastery", tag="_front"),
|
||||
destination="Monastery", tag="_front", direction=Direction.north),
|
||||
Portal(name="Quarry to Monastery Back", region="Monastery Rope",
|
||||
destination="Monastery", tag="_back"),
|
||||
destination="Monastery", tag="_back", direction=Direction.east),
|
||||
Portal(name="Quarry to Mountain", region="Quarry Back",
|
||||
destination="Mountain", tag="_"),
|
||||
destination="Mountain", tag="_", direction=Direction.north),
|
||||
Portal(name="Quarry to Ziggurat", region="Lower Quarry Zig Door",
|
||||
destination="ziggurat2020_0", tag="_"),
|
||||
destination="ziggurat2020_0", tag="_", direction=Direction.north),
|
||||
Portal(name="Quarry to Far Shore", region="Quarry Portal",
|
||||
destination="Transit", tag="_teleporter_quarry teleporter"),
|
||||
|
||||
destination="Transit", tag="_teleporter_quarry teleporter", direction=Direction.floor),
|
||||
|
||||
Portal(name="Monastery Rear Exit", region="Monastery Back",
|
||||
destination="Quarry Redux", tag="_back"),
|
||||
destination="Quarry Redux", tag="_back", direction=Direction.west),
|
||||
Portal(name="Monastery Front Exit", region="Monastery Front",
|
||||
destination="Quarry Redux", tag="_front"),
|
||||
destination="Quarry Redux", tag="_front", direction=Direction.south),
|
||||
Portal(name="Monastery Hero's Grave", region="Monastery Hero's Grave Region",
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth"),
|
||||
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor),
|
||||
|
||||
Portal(name="Ziggurat Entry Hallway to Ziggurat Upper", region="Rooted Ziggurat Entry",
|
||||
destination="ziggurat2020_1", tag="_"),
|
||||
destination="ziggurat2020_1", tag="_", direction=Direction.north),
|
||||
Portal(name="Ziggurat Entry Hallway to Quarry", region="Rooted Ziggurat Entry",
|
||||
destination="Quarry Redux", tag="_"),
|
||||
|
||||
destination="Quarry Redux", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Ziggurat Upper to Ziggurat Entry Hallway", region="Rooted Ziggurat Upper Entry",
|
||||
destination="ziggurat2020_0", tag="_"),
|
||||
destination="ziggurat2020_0", tag="_", direction=Direction.south),
|
||||
Portal(name="Ziggurat Upper to Ziggurat Tower", region="Rooted Ziggurat Upper Back",
|
||||
destination="ziggurat2020_2", tag="_"),
|
||||
|
||||
destination="ziggurat2020_2", tag="_", direction=Direction.north), # connecting is south
|
||||
|
||||
Portal(name="Ziggurat Tower to Ziggurat Upper", region="Rooted Ziggurat Middle Top",
|
||||
destination="ziggurat2020_1", tag="_"),
|
||||
destination="ziggurat2020_1", tag="_", direction=Direction.south),
|
||||
Portal(name="Ziggurat Tower to Ziggurat Lower", region="Rooted Ziggurat Middle Bottom",
|
||||
destination="ziggurat2020_3", tag="_"),
|
||||
|
||||
destination="ziggurat2020_3", tag="_", direction=Direction.south),
|
||||
|
||||
Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Entry",
|
||||
destination="ziggurat2020_2", tag="_"),
|
||||
destination="ziggurat2020_2", tag="_", direction=Direction.north),
|
||||
Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance",
|
||||
destination="ziggurat2020_FTRoom", tag="_"),
|
||||
destination="ziggurat2020_FTRoom", tag="_", direction=Direction.north),
|
||||
# only if fixed shop is on, removed otherwise
|
||||
Portal(name="Ziggurat Lower Falling Entrance", region="Zig Skip Exit",
|
||||
destination="ziggurat2020_1", tag="_zig2_skip"),
|
||||
|
||||
Portal(name="Ziggurat Lower Falling Entrance", region="Zig Skip Exit", # not a real region
|
||||
destination="ziggurat2020_1", tag="_zig2_skip", direction=Direction.none),
|
||||
|
||||
Portal(name="Ziggurat Portal Room Exit", region="Rooted Ziggurat Portal Room Exit",
|
||||
destination="ziggurat2020_3", tag="_"),
|
||||
destination="ziggurat2020_3", tag="_", direction=Direction.south),
|
||||
Portal(name="Ziggurat to Far Shore", region="Rooted Ziggurat Portal",
|
||||
destination="Transit", tag="_teleporter_ziggurat teleporter"),
|
||||
|
||||
destination="Transit", tag="_teleporter_ziggurat teleporter", direction=Direction.floor),
|
||||
|
||||
Portal(name="Swamp Lower Exit", region="Swamp Front",
|
||||
destination="Overworld Redux", tag="_conduit"),
|
||||
destination="Overworld Redux", tag="_conduit", direction=Direction.north),
|
||||
Portal(name="Swamp to Cathedral Main Entrance", region="Swamp to Cathedral Main Entrance Region",
|
||||
destination="Cathedral Redux", tag="_main"),
|
||||
destination="Cathedral Redux", tag="_main", direction=Direction.north),
|
||||
Portal(name="Swamp to Cathedral Secret Legend Room Entrance", region="Swamp to Cathedral Treasure Room",
|
||||
destination="Cathedral Redux", tag="_secret"),
|
||||
destination="Cathedral Redux", tag="_secret", direction=Direction.south), # feels a little weird
|
||||
Portal(name="Swamp to Gauntlet", region="Back of Swamp",
|
||||
destination="Cathedral Arena", tag="_"),
|
||||
destination="Cathedral Arena", tag="_", direction=Direction.north),
|
||||
Portal(name="Swamp Shop", region="Swamp Front",
|
||||
destination="Shop", tag="_"),
|
||||
destination="Shop", tag="_", direction=Direction.north),
|
||||
Portal(name="Swamp Upper Exit", region="Back of Swamp Laurels Area",
|
||||
destination="Overworld Redux", tag="_wall"),
|
||||
destination="Overworld Redux", tag="_wall", direction=Direction.north),
|
||||
Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave Region",
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth"),
|
||||
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor),
|
||||
|
||||
Portal(name="Cathedral Main Exit", region="Cathedral Entry",
|
||||
destination="Swamp Redux 2", tag="_main"),
|
||||
destination="Swamp Redux 2", tag="_main", direction=Direction.south),
|
||||
Portal(name="Cathedral Elevator", region="Cathedral to Gauntlet",
|
||||
destination="Cathedral Arena", tag="_"),
|
||||
destination="Cathedral Arena", tag="_", direction=Direction.ladder_down), # elevators are ladders, right?
|
||||
Portal(name="Cathedral Secret Legend Room Exit", region="Cathedral Secret Legend Room",
|
||||
destination="Swamp Redux 2", tag="_secret"),
|
||||
|
||||
destination="Swamp Redux 2", tag="_secret", direction=Direction.north),
|
||||
|
||||
Portal(name="Gauntlet to Swamp", region="Cathedral Gauntlet Exit",
|
||||
destination="Swamp Redux 2", tag="_"),
|
||||
destination="Swamp Redux 2", tag="_", direction=Direction.south),
|
||||
Portal(name="Gauntlet Elevator", region="Cathedral Gauntlet Checkpoint",
|
||||
destination="Cathedral Redux", tag="_"),
|
||||
destination="Cathedral Redux", tag="_", direction=Direction.ladder_up),
|
||||
Portal(name="Gauntlet Shop", region="Cathedral Gauntlet Checkpoint",
|
||||
destination="Shop", tag="_"),
|
||||
|
||||
destination="Shop", tag="_", direction=Direction.east),
|
||||
|
||||
Portal(name="Hero's Grave to Fortress", region="Hero Relic - Fortress",
|
||||
destination="Fortress Reliquary", tag="_teleporter_relic plinth"),
|
||||
destination="Fortress Reliquary", tag="_teleporter_relic plinth", direction=Direction.floor),
|
||||
Portal(name="Hero's Grave to Monastery", region="Hero Relic - Quarry",
|
||||
destination="Monastery", tag="_teleporter_relic plinth"),
|
||||
destination="Monastery", tag="_teleporter_relic plinth", direction=Direction.floor),
|
||||
Portal(name="Hero's Grave to West Garden", region="Hero Relic - West Garden",
|
||||
destination="Archipelagos Redux", tag="_teleporter_relic plinth"),
|
||||
destination="Archipelagos Redux", tag="_teleporter_relic plinth", direction=Direction.floor),
|
||||
Portal(name="Hero's Grave to East Forest", region="Hero Relic - East Forest",
|
||||
destination="Sword Access", tag="_teleporter_relic plinth"),
|
||||
destination="Sword Access", tag="_teleporter_relic plinth", direction=Direction.floor),
|
||||
Portal(name="Hero's Grave to Library", region="Hero Relic - Library",
|
||||
destination="Library Hall", tag="_teleporter_relic plinth"),
|
||||
destination="Library Hall", tag="_teleporter_relic plinth", direction=Direction.floor),
|
||||
Portal(name="Hero's Grave to Swamp", region="Hero Relic - Swamp",
|
||||
destination="Swamp Redux 2", tag="_teleporter_relic plinth"),
|
||||
|
||||
destination="Swamp Redux 2", tag="_teleporter_relic plinth", direction=Direction.floor),
|
||||
|
||||
Portal(name="Far Shore to West Garden", region="Far Shore to West Garden Region",
|
||||
destination="Archipelagos Redux", tag="_teleporter_archipelagos_teleporter"),
|
||||
destination="Archipelagos Redux", tag="_teleporter_archipelagos_teleporter", direction=Direction.floor),
|
||||
Portal(name="Far Shore to Library", region="Far Shore to Library Region",
|
||||
destination="Library Lab", tag="_teleporter_library teleporter"),
|
||||
destination="Library Lab", tag="_teleporter_library teleporter", direction=Direction.floor),
|
||||
Portal(name="Far Shore to Quarry", region="Far Shore to Quarry Region",
|
||||
destination="Quarry Redux", tag="_teleporter_quarry teleporter"),
|
||||
destination="Quarry Redux", tag="_teleporter_quarry teleporter", direction=Direction.floor),
|
||||
Portal(name="Far Shore to East Forest", region="Far Shore to East Forest Region",
|
||||
destination="East Forest Redux", tag="_teleporter_forest teleporter"),
|
||||
destination="East Forest Redux", tag="_teleporter_forest teleporter", direction=Direction.floor),
|
||||
Portal(name="Far Shore to Fortress", region="Far Shore to Fortress Region",
|
||||
destination="Fortress Arena", tag="_teleporter_spidertank"),
|
||||
destination="Fortress Arena", tag="_teleporter_spidertank", direction=Direction.floor),
|
||||
Portal(name="Far Shore to Atoll", region="Far Shore",
|
||||
destination="Atoll Redux", tag="_teleporter_atoll"),
|
||||
destination="Atoll Redux", tag="_teleporter_atoll", direction=Direction.floor),
|
||||
Portal(name="Far Shore to Ziggurat", region="Far Shore",
|
||||
destination="ziggurat2020_FTRoom", tag="_teleporter_ziggurat teleporter"),
|
||||
destination="ziggurat2020_FTRoom", tag="_teleporter_ziggurat teleporter", direction=Direction.floor),
|
||||
Portal(name="Far Shore to Heir", region="Far Shore",
|
||||
destination="Spirit Arena", tag="_teleporter_spirit arena"),
|
||||
destination="Spirit Arena", tag="_teleporter_spirit arena", direction=Direction.floor),
|
||||
Portal(name="Far Shore to Town", region="Far Shore",
|
||||
destination="Overworld Redux", tag="_teleporter_town"),
|
||||
destination="Overworld Redux", tag="_teleporter_town", direction=Direction.floor),
|
||||
Portal(name="Far Shore to Spawn", region="Far Shore to Spawn Region",
|
||||
destination="Overworld Redux", tag="_teleporter_starting island"),
|
||||
|
||||
destination="Overworld Redux", tag="_teleporter_starting island", direction=Direction.floor),
|
||||
|
||||
Portal(name="Heir Arena Exit", region="Spirit Arena",
|
||||
destination="Transit", tag="_teleporter_spirit arena"),
|
||||
|
||||
destination="Transit", tag="_teleporter_spirit arena", direction=Direction.floor),
|
||||
|
||||
Portal(name="Purgatory Bottom Exit", region="Purgatory",
|
||||
destination="Purgatory", tag="_bottom"),
|
||||
destination="Purgatory", tag="_bottom", direction=Direction.south),
|
||||
Portal(name="Purgatory Top Exit", region="Purgatory",
|
||||
destination="Purgatory", tag="_top"),
|
||||
destination="Purgatory", tag="_top", direction=Direction.north),
|
||||
]
|
||||
|
||||
|
||||
@@ -523,6 +536,7 @@ class RegionInfo(NamedTuple):
|
||||
game_scene: str # the name of the scene in the actual game
|
||||
dead_end: int = 0 # if a region has only one exit
|
||||
outlet_region: Optional[str] = None
|
||||
is_fake_region: bool = False
|
||||
|
||||
|
||||
# gets the outlet region name if it exists, the region if it doesn't
|
||||
@@ -540,9 +554,9 @@ class DeadEnd(IntEnum):
|
||||
|
||||
# key is the AP region name. "Fake" in region info just means the mod won't receive that info at all
|
||||
tunic_er_regions: Dict[str, RegionInfo] = {
|
||||
"Menu": RegionInfo("Fake", dead_end=DeadEnd.all_cats),
|
||||
"Menu": RegionInfo("Fake", dead_end=DeadEnd.all_cats, is_fake_region=True),
|
||||
"Overworld": RegionInfo("Overworld Redux"), # main overworld, the central area
|
||||
"Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats), # main overworld holy cross checks
|
||||
"Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats, is_fake_region=True), # main overworld holy cross checks
|
||||
"Overworld Belltower": RegionInfo("Overworld Redux"), # the area with the belltower and chest
|
||||
"Overworld Belltower at Bell": RegionInfo("Overworld Redux"), # being able to ring the belltower, basically
|
||||
"Overworld Swamp Upper Entry": RegionInfo("Overworld Redux"), # upper swamp entry spot
|
||||
@@ -722,7 +736,7 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
||||
"Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the front for combat logic
|
||||
"Rooted Ziggurat Lower Mid Checkpoint": RegionInfo("ziggurat2020_3"), # the mid-checkpoint before double admin
|
||||
"Rooted Ziggurat Lower Back": RegionInfo("ziggurat2020_3"), # the boss side
|
||||
"Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Entry"), # for use with fixed shop on
|
||||
"Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Entry", is_fake_region=True), # for use with fixed shop on
|
||||
"Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3", outlet_region="Rooted Ziggurat Lower Back"), # the door itself on the zig 3 side
|
||||
"Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom", outlet_region="Rooted Ziggurat Portal Room"),
|
||||
"Rooted Ziggurat Portal Room": RegionInfo("ziggurat2020_FTRoom"),
|
||||
@@ -758,7 +772,7 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
||||
"Purgatory": RegionInfo("Purgatory"),
|
||||
"Shop": RegionInfo("Shop", dead_end=DeadEnd.all_cats),
|
||||
"Spirit Arena": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats),
|
||||
"Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats),
|
||||
"Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats, is_fake_region=True),
|
||||
}
|
||||
|
||||
|
||||
@@ -813,7 +827,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
"Overworld Southeast Cross Door":
|
||||
[],
|
||||
"Overworld Fountain Cross Door":
|
||||
[],
|
||||
[],
|
||||
"Overworld Town Portal":
|
||||
[],
|
||||
"Overworld Spawn Portal":
|
||||
@@ -1301,7 +1315,6 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
[],
|
||||
},
|
||||
|
||||
# cannot get from frogs back to front
|
||||
"Library Exterior Ladder Region": {
|
||||
"Library Exterior by Tree":
|
||||
[],
|
||||
@@ -1634,10 +1647,6 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
"Rooted Ziggurat Portal Room Entrance":
|
||||
[],
|
||||
},
|
||||
"Zig Skip Exit": {
|
||||
"Rooted Ziggurat Lower Front":
|
||||
[],
|
||||
},
|
||||
"Rooted Ziggurat Portal Room Entrance": {
|
||||
"Rooted Ziggurat Lower Back":
|
||||
[],
|
||||
|
||||
@@ -381,9 +381,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Tunnel Turret"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
regions["Overworld Tunnel Turret"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: state.has_any({grapple, laurels}, player))
|
||||
|
||||
# always have access to Overworld, so connecting back isn't needed
|
||||
# regions["Overworld Tunnel Turret"].connect(
|
||||
# connecting_region=regions["Overworld"],
|
||||
# rule=lambda state: state.has_any({grapple, laurels}, player))
|
||||
|
||||
cube_entrance = regions["Overworld"].connect(
|
||||
connecting_region=regions["Cube Cave Entrance Region"],
|
||||
@@ -1053,11 +1055,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
regions["Rooted Ziggurat Portal Room Entrance"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Back"])
|
||||
|
||||
# zig skip region only gets made if entrance rando and fewer shops are on
|
||||
if options.entrance_rando and options.fixed_shop:
|
||||
regions["Zig Skip Exit"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Front"])
|
||||
|
||||
regions["Rooted Ziggurat Portal"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Portal Room"])
|
||||
regions["Rooted Ziggurat Portal Room"].connect(
|
||||
@@ -1226,14 +1223,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
and has_sword(state, player))))
|
||||
|
||||
if options.ladder_storage:
|
||||
def get_portal_info(portal_sd: str) -> Tuple[str, str]:
|
||||
for portal1, portal2 in portal_pairs.items():
|
||||
if portal1.scene_destination() == portal_sd:
|
||||
return portal1.name, get_portal_outlet_region(portal2, world)
|
||||
if portal2.scene_destination() == portal_sd:
|
||||
return portal2.name, get_portal_outlet_region(portal1, world)
|
||||
raise Exception("no matches found in get_paired_region")
|
||||
|
||||
# connect ls elevation regions to their destinations
|
||||
def ls_connect(origin_name: str, portal_sdt: str) -> None:
|
||||
p_name, paired_region_name = get_portal_info(portal_sdt)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from typing import Dict, List, Set, Tuple, TYPE_CHECKING
|
||||
from BaseClasses import Region, ItemClassification, Item, Location
|
||||
from .locations import all_locations
|
||||
from .er_data import Portal, portal_mapping, traversal_requirements, DeadEnd, RegionInfo
|
||||
from .er_data import (Portal, portal_mapping, traversal_requirements, DeadEnd, Direction, RegionInfo,
|
||||
get_portal_outlet_region)
|
||||
from .er_rules import set_er_region_rules
|
||||
from .breakables import create_breakable_exclusive_regions, set_breakable_location_rules
|
||||
from Options import PlandoConnection
|
||||
from .options import EntranceRando
|
||||
from .options import EntranceRando, EntranceLayout
|
||||
from random import Random
|
||||
from copy import deepcopy
|
||||
|
||||
@@ -23,17 +24,18 @@ class TunicERLocation(Location):
|
||||
|
||||
def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||
regions: Dict[str, Region] = {}
|
||||
world.used_shop_numbers = set()
|
||||
|
||||
for region_name, region_data in world.er_regions.items():
|
||||
if world.options.entrance_rando and region_name == "Zig Skip Exit":
|
||||
# need to check if there's a seed group for this first
|
||||
if world.options.entrance_rando.value not in EntranceRando.options.values():
|
||||
if not world.seed_groups[world.options.entrance_rando.value]["fixed_shop"]:
|
||||
if world.seed_groups[world.options.entrance_rando.value]["entrance_layout"] != EntranceLayout.option_fixed_shop:
|
||||
continue
|
||||
elif not world.options.fixed_shop:
|
||||
elif world.options.entrance_layout != EntranceLayout.option_fixed_shop:
|
||||
continue
|
||||
if not world.options.entrance_rando and region_name in ("Zig Skip Exit", "Purgatory"):
|
||||
continue
|
||||
|
||||
region = Region(region_name, world.player, world.multiworld)
|
||||
regions[region_name] = region
|
||||
world.multiworld.regions.append(region)
|
||||
@@ -46,13 +48,18 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||
portal_pairs = pair_portals(world, regions)
|
||||
|
||||
# output the entrances to the spoiler log here for convenience
|
||||
sorted_portal_pairs = sort_portals(portal_pairs)
|
||||
for portal1, portal2 in sorted_portal_pairs.items():
|
||||
world.multiworld.spoiler.set_entrance(portal1, portal2, "both", world.player)
|
||||
sorted_portal_pairs = sort_portals(portal_pairs, world)
|
||||
if not world.options.decoupled:
|
||||
for portal1, portal2 in sorted_portal_pairs.items():
|
||||
world.multiworld.spoiler.set_entrance(portal1, portal2, "both", world.player)
|
||||
else:
|
||||
for portal1, portal2 in sorted_portal_pairs.items():
|
||||
world.multiworld.spoiler.set_entrance(portal1, portal2, "entrance", world.player)
|
||||
|
||||
else:
|
||||
portal_pairs = vanilla_portals(world, regions)
|
||||
|
||||
create_randomized_entrances(portal_pairs, regions)
|
||||
create_randomized_entrances(world, portal_pairs, regions)
|
||||
|
||||
set_er_region_rules(world, regions, portal_pairs)
|
||||
|
||||
@@ -75,6 +82,7 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||
return portal_pairs
|
||||
|
||||
|
||||
# keys are event names, values are event regions
|
||||
tunic_events: Dict[str, str] = {
|
||||
"Eastern Bell": "Forest Belltower Upper",
|
||||
"Western Bell": "Overworld Belltower at Bell",
|
||||
@@ -111,17 +119,31 @@ def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None:
|
||||
region.locations.append(location)
|
||||
|
||||
|
||||
# keeping track of which shop numbers have been used already to avoid duplicates
|
||||
# due to plando, shops can be added out of order, so a set is the best way to make this work smoothly
|
||||
def get_shop_num(world: "TunicWorld") -> int:
|
||||
portal_num = -1
|
||||
for i in range(500):
|
||||
if i + 1 not in world.used_shop_numbers:
|
||||
portal_num = i + 1
|
||||
world.used_shop_numbers.add(portal_num)
|
||||
break
|
||||
if portal_num == -1:
|
||||
raise Exception(f"TUNIC: {world.player_name} has plando'd too many shops.")
|
||||
return portal_num
|
||||
|
||||
|
||||
# all shops are the same shop. however, you cannot get to all shops from the same shop entrance.
|
||||
# so, we need a bunch of shop regions that connect to the actual shop, but the actual shop cannot connect back
|
||||
def create_shop_region(world: "TunicWorld", regions: Dict[str, Region]) -> None:
|
||||
new_shop_name = f"Shop {world.shop_num}"
|
||||
def create_shop_region(world: "TunicWorld", regions: Dict[str, Region], portal_num) -> None:
|
||||
new_shop_name = f"Shop {portal_num}"
|
||||
world.er_regions[new_shop_name] = RegionInfo("Shop", dead_end=DeadEnd.all_cats)
|
||||
new_shop_region = Region(new_shop_name, world.player, world.multiworld)
|
||||
new_shop_region.connect(regions["Shop"])
|
||||
regions[new_shop_name] = new_shop_region
|
||||
world.shop_num += 1
|
||||
|
||||
|
||||
# for non-ER that uses the ER rules, we create a vanilla set of portal pairs
|
||||
def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]:
|
||||
portal_pairs: Dict[Portal, Portal] = {}
|
||||
# we don't want the zig skip exit for vanilla portals, since it shouldn't be considered for logic here
|
||||
@@ -135,9 +157,10 @@ def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Por
|
||||
portal2_sdt = portal1.destination_scene()
|
||||
|
||||
if portal2_sdt.startswith("Shop,"):
|
||||
portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}",
|
||||
destination="Previous Region", tag="_")
|
||||
create_shop_region(world, regions)
|
||||
portal_num = get_shop_num(world)
|
||||
portal2 = Portal(name=f"Shop Portal {portal_num}", region=f"Shop {portal_num}",
|
||||
destination=str(portal_num), tag="_", direction=Direction.none)
|
||||
create_shop_region(world, regions, portal_num)
|
||||
|
||||
for portal in portal_map:
|
||||
if portal.scene_destination() == portal2_sdt:
|
||||
@@ -152,7 +175,13 @@ def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Por
|
||||
return portal_pairs
|
||||
|
||||
|
||||
# pairing off portals, starting with dead ends
|
||||
# the really long function that gives us our portal pairs
|
||||
# before we start pairing, we separate the portals into dead ends and non-dead ends (two_plus)
|
||||
# then, we do a few other important tasks to accommodate options and seed gropus
|
||||
# first phase: pick a two_plus in a reachable region and non-reachable region and pair them
|
||||
# repeat this phase until all regions are reachable
|
||||
# second phase: randomly pair dead ends to random two_plus
|
||||
# third phase: randomly pair the remaining two_plus to each other
|
||||
def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]:
|
||||
portal_pairs: Dict[Portal, Portal] = {}
|
||||
dead_ends: List[Portal] = []
|
||||
@@ -162,8 +191,9 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
laurels_zips = world.options.laurels_zips.value
|
||||
ice_grappling = world.options.ice_grappling.value
|
||||
ladder_storage = world.options.ladder_storage.value
|
||||
fixed_shop = world.options.fixed_shop
|
||||
entrance_layout = world.options.entrance_layout
|
||||
laurels_location = world.options.laurels_location
|
||||
decoupled = world.options.decoupled
|
||||
traversal_reqs = deepcopy(traversal_requirements)
|
||||
has_laurels = True
|
||||
waterfall_plando = False
|
||||
@@ -174,7 +204,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
laurels_zips = seed_group["laurels_zips"]
|
||||
ice_grappling = seed_group["ice_grappling"]
|
||||
ladder_storage = seed_group["ladder_storage"]
|
||||
fixed_shop = seed_group["fixed_shop"]
|
||||
entrance_layout = seed_group["entrance_layout"]
|
||||
laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False
|
||||
|
||||
logic_tricks: Tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage)
|
||||
@@ -183,15 +213,18 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
if laurels_location == "10_fairies" and not world.using_ut:
|
||||
has_laurels = False
|
||||
|
||||
shop_count = 6
|
||||
if fixed_shop:
|
||||
shop_count = 0
|
||||
else:
|
||||
# if fixed shop is off, remove this portal
|
||||
for portal in portal_map:
|
||||
if portal.region == "Zig Skip Exit":
|
||||
portal_map.remove(portal)
|
||||
break
|
||||
# for the direction pairs option with decoupled off
|
||||
# tracks how many portals are in each direction in each list
|
||||
two_plus_direction_tracker: Dict[int, int] = {direction: 0 for direction in range(8)}
|
||||
dead_end_direction_tracker: Dict[int, int] = {direction: 0 for direction in range(8)}
|
||||
|
||||
# for ensuring we have enough entrances in directions left that we don't leave dead ends without any
|
||||
def too_few_portals_for_direction_pairs(direction: int, offset: int) -> bool:
|
||||
if two_plus_direction_tracker[direction] <= (dead_end_direction_tracker[direction_pairs[direction]] + offset):
|
||||
return False
|
||||
if two_plus_direction_tracker[direction_pairs[direction]] <= dead_end_direction_tracker[direction] + offset:
|
||||
return False
|
||||
return True
|
||||
|
||||
# If using Universal Tracker, restore portal_map. Could be cleaner, but it does not matter for UT even a little bit
|
||||
if world.using_ut:
|
||||
@@ -202,25 +235,59 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
dead_end_status = world.er_regions[portal.region].dead_end
|
||||
if dead_end_status == DeadEnd.free:
|
||||
two_plus.append(portal)
|
||||
two_plus_direction_tracker[portal.direction] += 1
|
||||
elif dead_end_status == DeadEnd.all_cats:
|
||||
dead_ends.append(portal)
|
||||
dead_end_direction_tracker[portal.direction] += 1
|
||||
elif dead_end_status == DeadEnd.restricted:
|
||||
if ice_grappling:
|
||||
two_plus.append(portal)
|
||||
two_plus_direction_tracker[portal.direction] += 1
|
||||
else:
|
||||
dead_ends.append(portal)
|
||||
dead_end_direction_tracker[portal.direction] += 1
|
||||
# these two get special handling
|
||||
elif dead_end_status == DeadEnd.special:
|
||||
if portal.region == "Secret Gathering Place":
|
||||
if laurels_location == "10_fairies":
|
||||
two_plus.append(portal)
|
||||
two_plus_direction_tracker[portal.direction] += 1
|
||||
else:
|
||||
dead_ends.append(portal)
|
||||
dead_end_direction_tracker[portal.direction] += 1
|
||||
if portal.region == "Zig Skip Exit" and entrance_layout == EntranceLayout.option_fixed_shop:
|
||||
# direction isn't meaningful here since zig skip cannot be in direction pairs mode
|
||||
two_plus.append(portal)
|
||||
|
||||
# now we generate the shops and add them to the dead ends list
|
||||
shop_count = 6
|
||||
if entrance_layout == EntranceLayout.option_fixed_shop:
|
||||
shop_count = 0
|
||||
else:
|
||||
# if fixed shop is off, remove this portal
|
||||
for portal in portal_map:
|
||||
if portal.region == "Zig Skip Exit":
|
||||
if fixed_shop:
|
||||
two_plus.append(portal)
|
||||
else:
|
||||
dead_ends.append(portal)
|
||||
portal_map.remove(portal)
|
||||
break
|
||||
# need 8 shops with direction pairs or there won't be a valid set of pairs
|
||||
if entrance_layout == EntranceLayout.option_direction_pairs:
|
||||
shop_count = 8
|
||||
|
||||
# for universal tracker, we want to skip shop gen since it's essentially full plando
|
||||
if world.using_ut:
|
||||
shop_count = 0
|
||||
|
||||
for _ in range(shop_count):
|
||||
# 6 of the shops have south exits, 2 of them have west exits
|
||||
portal_num = get_shop_num(world)
|
||||
shop_dir = Direction.south
|
||||
if portal_num > 6:
|
||||
shop_dir = Direction.west
|
||||
shop_portal = Portal(name=f"Shop Portal {portal_num}", region=f"Shop {portal_num}",
|
||||
destination=str(portal_num), tag="_", direction=shop_dir)
|
||||
create_shop_region(world, regions, portal_num)
|
||||
dead_ends.append(shop_portal)
|
||||
dead_end_direction_tracker[shop_portal.direction] += 1
|
||||
|
||||
connected_regions: Set[str] = set()
|
||||
# make better start region stuff when/if implementing random start
|
||||
@@ -249,29 +316,68 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
portal_name2 = portal.name
|
||||
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
|
||||
# shops have special handling
|
||||
if not portal_name2 and portal2 == "Shop, Previous Region_":
|
||||
portal_name2 = "Shop Portal"
|
||||
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both"))
|
||||
if not portal_name1 and portal1.startswith("Shop"):
|
||||
# it should show up as "Shop, 1_" for shop 1
|
||||
portal_name1 = "Shop Portal " + str(portal1).split(", ")[1].split("_")[0]
|
||||
if not portal_name2 and portal2.startswith("Shop"):
|
||||
portal_name2 = "Shop Portal " + str(portal2).split(", ")[1].split("_")[0]
|
||||
if world.options.decoupled:
|
||||
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "entrance"))
|
||||
else:
|
||||
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both"))
|
||||
|
||||
# put together the list of non-deadend regions
|
||||
non_dead_end_regions = set()
|
||||
for region_name, region_info in world.er_regions.items():
|
||||
if not region_info.dead_end:
|
||||
# these are not real regions, they are just here to be descriptive
|
||||
if region_info.is_fake_region or region_name == "Shop":
|
||||
continue
|
||||
# dead ends aren't real in decoupled
|
||||
if decoupled:
|
||||
non_dead_end_regions.add(region_name)
|
||||
elif not region_info.dead_end:
|
||||
non_dead_end_regions.add(region_name)
|
||||
# if ice grappling to places is in logic, both places stop being dead ends
|
||||
elif region_info.dead_end == DeadEnd.restricted and ice_grappling:
|
||||
non_dead_end_regions.add(region_name)
|
||||
# secret gathering place and zig skip get weird, special handling
|
||||
# secret gathering place is treated as a non-dead end if 10 fairies is on to assure non-laurels access to it
|
||||
elif region_info.dead_end == DeadEnd.special:
|
||||
if (region_name == "Secret Gathering Place" and laurels_location == "10_fairies") \
|
||||
or (region_name == "Zig Skip Exit" and fixed_shop):
|
||||
if region_name == "Secret Gathering Place" and laurels_location == "10_fairies":
|
||||
non_dead_end_regions.add(region_name)
|
||||
|
||||
if decoupled:
|
||||
# add the dead ends to the two plus list, since dead ends aren't real in decoupled
|
||||
two_plus.extend(dead_ends)
|
||||
dead_ends.clear()
|
||||
# if decoupled is on, we make a second two_plus list, where the first is entrances and the second is exits
|
||||
two_plus2 = two_plus.copy()
|
||||
else:
|
||||
# if decoupled is off, the two lists are the same list, since entrances and exits are intertwined
|
||||
two_plus2 = two_plus
|
||||
|
||||
if plando_connections:
|
||||
for connection in plando_connections:
|
||||
if decoupled:
|
||||
modified_plando_connections = plando_connections.copy()
|
||||
for index, cxn in enumerate(modified_plando_connections):
|
||||
# it's much easier if we split both-direction portals into two one-ways in decoupled
|
||||
if cxn.direction == "both":
|
||||
replacement1 = PlandoConnection(cxn.entrance, cxn.exit, "entrance")
|
||||
replacement2 = PlandoConnection(cxn.exit, cxn.entrance, "entrance")
|
||||
modified_plando_connections.remove(cxn)
|
||||
modified_plando_connections.insert(index, replacement1)
|
||||
modified_plando_connections.append(replacement2)
|
||||
else:
|
||||
modified_plando_connections = plando_connections
|
||||
|
||||
connected_shop_portal1s: Set[int] = set()
|
||||
connected_shop_portal2s: Set[int] = set()
|
||||
for connection in modified_plando_connections:
|
||||
p_entrance = connection.entrance
|
||||
p_exit = connection.exit
|
||||
# if you plando secret gathering place, need to know that during portal pairing
|
||||
if "Secret Gathering Place Exit" in [p_entrance, p_exit]:
|
||||
if p_exit == "Secret Gathering Place Exit":
|
||||
waterfall_plando = True
|
||||
if p_entrance == "Secret Gathering Place Exit" and not decoupled:
|
||||
waterfall_plando = True
|
||||
portal1_dead_end = True
|
||||
portal2_dead_end = True
|
||||
@@ -279,118 +385,186 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
portal1 = None
|
||||
portal2 = None
|
||||
|
||||
# search two_plus for both at once
|
||||
# search the two_plus lists (or list) for the portals
|
||||
for portal in two_plus:
|
||||
if p_entrance == portal.name:
|
||||
portal1 = portal
|
||||
portal1_dead_end = False
|
||||
break
|
||||
for portal in two_plus2:
|
||||
if p_exit == portal.name:
|
||||
portal2 = portal
|
||||
portal2_dead_end = False
|
||||
break
|
||||
|
||||
# search dead_ends individually since we can't really remove items from two_plus during the loop
|
||||
if portal1:
|
||||
two_plus.remove(portal1)
|
||||
else:
|
||||
# if not both, they're both dead ends
|
||||
if not portal2:
|
||||
if not portal2 and not decoupled:
|
||||
if world.options.entrance_rando.value not in EntranceRando.options.values():
|
||||
raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead "
|
||||
"end to a dead end in their plando connections.")
|
||||
else:
|
||||
raise Exception(f"{player_name} paired a dead end to a dead end in their "
|
||||
"plando connections.")
|
||||
f"plando connections -- {connection.entrance} to {connection.exit}")
|
||||
|
||||
for portal in dead_ends:
|
||||
if p_entrance == portal.name:
|
||||
portal1 = portal
|
||||
dead_ends.remove(portal1)
|
||||
break
|
||||
if not portal1:
|
||||
raise Exception(f"Could not find entrance named {p_entrance} for "
|
||||
f"plando connections in {player_name}'s YAML.")
|
||||
dead_ends.remove(portal1)
|
||||
else:
|
||||
if p_entrance.startswith("Shop Portal "):
|
||||
portal_num = int(p_entrance.split("Shop Portal ")[-1])
|
||||
# shops 1-6 are south, 7 and 8 are east, and after that it just breaks direction pairs
|
||||
if portal_num <= 6:
|
||||
pdir = Direction.south
|
||||
elif portal_num in [7, 8]:
|
||||
pdir = Direction.east
|
||||
else:
|
||||
pdir = Direction.none
|
||||
portal1 = Portal(name=f"Shop Portal {portal_num}", region=f"Shop {portal_num}",
|
||||
destination=str(portal_num), tag="_", direction=pdir)
|
||||
connected_shop_portal1s.add(portal_num)
|
||||
if portal_num not in world.used_shop_numbers:
|
||||
create_shop_region(world, regions, portal_num)
|
||||
world.used_shop_numbers.add(portal_num)
|
||||
if decoupled and portal_num not in connected_shop_portal2s:
|
||||
two_plus2.append(portal1)
|
||||
non_dead_end_regions.add(portal1.region)
|
||||
else:
|
||||
raise Exception(f"Could not find entrance named {p_entrance} for "
|
||||
f"plando connections in {player_name}'s YAML.")
|
||||
|
||||
if portal2:
|
||||
two_plus.remove(portal2)
|
||||
two_plus2.remove(portal2)
|
||||
else:
|
||||
for portal in dead_ends:
|
||||
if p_exit == portal.name:
|
||||
portal2 = portal
|
||||
dead_ends.remove(portal2)
|
||||
break
|
||||
# if it's not a dead end, it might be a shop
|
||||
if p_exit == "Shop Portal":
|
||||
portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}",
|
||||
destination="Previous Region", tag="_")
|
||||
create_shop_region(world, regions)
|
||||
shop_count -= 1
|
||||
# need to maintain an even number of portals total
|
||||
if shop_count < 0:
|
||||
shop_count += 2
|
||||
# and if it's neither shop nor dead end, it just isn't correct
|
||||
# if it's not a dead end, maybe it's a plando'd shop portal that doesn't normally exist
|
||||
else:
|
||||
if not portal2:
|
||||
raise Exception(f"Could not find entrance named {p_exit} for "
|
||||
f"plando connections in {player_name}'s YAML.\n"
|
||||
f"If you are using Universal Tracker, the most likely reason for this error "
|
||||
f"is that the host generated with a newer version of the APWorld.\n"
|
||||
f"Please check the TUNIC Randomizer Github and place the newest APWorld in your "
|
||||
f"custom_worlds folder, and remove the one in lib/worlds if there is one there.")
|
||||
dead_ends.remove(portal2)
|
||||
if p_exit.startswith("Shop Portal "):
|
||||
portal_num = int(p_exit.split("Shop Portal ")[-1])
|
||||
if portal_num <= 6:
|
||||
pdir = Direction.south
|
||||
elif portal_num in [7, 8]:
|
||||
pdir = Direction.east
|
||||
else:
|
||||
pdir = Direction.none
|
||||
portal2 = Portal(name=f"Shop Portal {portal_num}", region=f"Shop {portal_num}",
|
||||
destination=str(portal_num), tag="_", direction=pdir)
|
||||
connected_shop_portal2s.add(portal_num)
|
||||
if portal_num not in world.used_shop_numbers:
|
||||
create_shop_region(world, regions, portal_num)
|
||||
world.used_shop_numbers.add(portal_num)
|
||||
if decoupled and portal_num not in connected_shop_portal1s:
|
||||
two_plus.append(portal2)
|
||||
non_dead_end_regions.add(portal2.region)
|
||||
else:
|
||||
raise Exception(f"Could not find entrance named {p_exit} for "
|
||||
f"plando connections in {player_name}'s YAML.")
|
||||
|
||||
# update the traversal chart to say you can get from portal1's region to portal2's and vice versa
|
||||
if not portal1_dead_end and not portal2_dead_end:
|
||||
traversal_reqs.setdefault(portal1.region, dict())[portal2.region] = []
|
||||
traversal_reqs.setdefault(portal2.region, dict())[portal1.region] = []
|
||||
# if we're doing decoupled, we don't need to do complex checks
|
||||
if decoupled:
|
||||
# we turn any plando that uses "exit" to use "entrance" instead
|
||||
traversal_reqs.setdefault(portal1.region, dict())[get_portal_outlet_region(portal2, world)] = []
|
||||
# outside decoupled, we want to use what we were doing before decoupled got added
|
||||
else:
|
||||
# update the traversal chart to say you can get from portal1's region to portal2's and vice versa
|
||||
if not portal1_dead_end and not portal2_dead_end:
|
||||
traversal_reqs.setdefault(portal1.region, dict())[get_portal_outlet_region(portal2, world)] = []
|
||||
traversal_reqs.setdefault(portal2.region, dict())[get_portal_outlet_region(portal1, world)] = []
|
||||
|
||||
if (portal1.region == "Zig Skip Exit" and (portal2_dead_end or portal2.region == "Secret Gathering Place")
|
||||
or portal2.region == "Zig Skip Exit" and (portal1_dead_end or portal1.region == "Secret Gathering Place")):
|
||||
if world.options.entrance_rando.value not in EntranceRando.options.values():
|
||||
raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead "
|
||||
"end to a dead end in their plando connections.")
|
||||
else:
|
||||
raise Exception(f"{player_name} paired a dead end to a dead end in their "
|
||||
"plando connections.")
|
||||
|
||||
if (portal1.region == "Secret Gathering Place" and (portal2_dead_end or portal2.region == "Zig Skip Exit")
|
||||
or portal2.region == "Secret Gathering Place" and (portal1_dead_end or portal1.region == "Zig Skip Exit")):
|
||||
# need to make sure you didn't pair this to a dead end or zig skip
|
||||
if portal1_dead_end or portal2_dead_end or \
|
||||
portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit":
|
||||
if (portal1.region == "Zig Skip Exit" and (portal2_dead_end or portal2.region == "Secret Gathering Place")
|
||||
or portal2.region == "Zig Skip Exit" and (portal1_dead_end or portal1.region == "Secret Gathering Place")):
|
||||
if world.options.entrance_rando.value not in EntranceRando.options.values():
|
||||
raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead "
|
||||
"end to a dead end in their plando connections.")
|
||||
else:
|
||||
raise Exception(f"{player_name} paired a dead end to a dead end in their "
|
||||
"plando connections.")
|
||||
|
||||
if (portal1.region == "Secret Gathering Place" and (portal2_dead_end or portal2.region == "Zig Skip Exit")
|
||||
or portal2.region == "Secret Gathering Place" and (portal1_dead_end or portal1.region == "Zig Skip Exit")):
|
||||
# need to make sure you didn't pair this to a dead end or zig skip
|
||||
if portal1_dead_end or portal2_dead_end or \
|
||||
portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit":
|
||||
if world.options.entrance_rando.value not in EntranceRando.options.values():
|
||||
raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead "
|
||||
"end to a dead end in their plando connections.")
|
||||
else:
|
||||
raise Exception(f"{player_name} paired a dead end to a dead end in their "
|
||||
"plando connections.")
|
||||
# okay now that we're done with all of that nonsense, we can finally make the portal pair
|
||||
portal_pairs[portal1] = portal2
|
||||
|
||||
if portal1_dead_end:
|
||||
dead_end_direction_tracker[portal1.direction] -= 1
|
||||
else:
|
||||
two_plus_direction_tracker[portal1.direction] -= 1
|
||||
if portal2_dead_end:
|
||||
dead_end_direction_tracker[portal2.direction] -= 1
|
||||
else:
|
||||
two_plus_direction_tracker[portal2.direction] -= 1
|
||||
|
||||
# if we have plando connections, our connected regions may change somewhat
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks)
|
||||
|
||||
if fixed_shop and not world.using_ut:
|
||||
portal1 = None
|
||||
# if there are an odd number of shops after plando, add another one, except in decoupled where it doesn't matter
|
||||
if not decoupled and len(world.used_shop_numbers) % 2 == 1:
|
||||
if entrance_layout == EntranceLayout.option_direction_pairs:
|
||||
raise Exception(f"TUNIC: {world.player_name} plando'd too many shops for the Direction Pairs option.")
|
||||
portal_num = get_shop_num(world)
|
||||
shop_portal = Portal(name=f"Shop Portal {portal_num}", region=f"Shop {portal_num}",
|
||||
destination=str(portal_num), tag="_", direction=Direction.none)
|
||||
create_shop_region(world, regions, portal_num)
|
||||
dead_ends.append(shop_portal)
|
||||
|
||||
if entrance_layout == EntranceLayout.option_fixed_shop and not world.using_ut:
|
||||
windmill = None
|
||||
for portal in two_plus:
|
||||
if portal.scene_destination() == "Overworld Redux, Windmill_":
|
||||
portal1 = portal
|
||||
windmill = portal
|
||||
break
|
||||
if not portal1:
|
||||
raise Exception(f"Failed to do Fixed Shop option. "
|
||||
f"Did {player_name} plando connection the Windmill Shop entrance?")
|
||||
if not windmill:
|
||||
raise Exception(f"Failed to do Fixed Shop option for Entrance Layout. "
|
||||
f"Did {player_name} plando the Windmill Shop entrance?")
|
||||
|
||||
portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}",
|
||||
destination="Previous Region", tag="_")
|
||||
create_shop_region(world, regions)
|
||||
portal_num = get_shop_num(world)
|
||||
shop = Portal(name=f"Shop Portal {portal_num}", region=f"Shop {portal_num}",
|
||||
destination=str(portal_num), tag="_", direction=Direction.south)
|
||||
create_shop_region(world, regions, portal_num)
|
||||
|
||||
portal_pairs[portal1] = portal2
|
||||
two_plus.remove(portal1)
|
||||
portal_pairs[windmill] = shop
|
||||
two_plus.remove(windmill)
|
||||
if decoupled:
|
||||
two_plus.append(shop)
|
||||
non_dead_end_regions.add(shop.region)
|
||||
connected_regions.add(shop.region)
|
||||
|
||||
random_object: Random = world.random
|
||||
# use the seed given in the options to shuffle the portals
|
||||
if isinstance(world.options.entrance_rando.value, str):
|
||||
random_object = Random(world.options.entrance_rando.value)
|
||||
else:
|
||||
random_object: Random = world.random
|
||||
|
||||
# we want to start by making sure every region is accessible
|
||||
random_object.shuffle(two_plus)
|
||||
check_success = 0
|
||||
|
||||
# this is a backup in case we run into that rare direction pairing failure
|
||||
# so that we don't have to redo the plando bit basically
|
||||
backup_connected_regions = connected_regions.copy()
|
||||
backup_portal_pairs = portal_pairs.copy()
|
||||
backup_two_plus = two_plus.copy()
|
||||
backup_two_plus_direction_tracker = two_plus_direction_tracker.copy()
|
||||
rare_failure_count = 0
|
||||
|
||||
portal1 = None
|
||||
portal2 = None
|
||||
previous_conn_num = 0
|
||||
@@ -403,96 +577,182 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
# should, hopefully, only ever occur if someone plandos connections poorly
|
||||
if previous_conn_num == len(connected_regions):
|
||||
fail_count += 1
|
||||
if fail_count >= 500:
|
||||
if fail_count > 500:
|
||||
raise Exception(f"Failed to pair regions. Check plando connections for {player_name} for errors. "
|
||||
"Unconnected regions:", non_dead_end_regions - connected_regions)
|
||||
f"Unconnected regions: {non_dead_end_regions - connected_regions}.\n"
|
||||
f"Unconnected portals: {[portal.name for portal in two_plus]}")
|
||||
if (fail_count > 100 and not decoupled
|
||||
and (world.options.entrance_layout == EntranceLayout.option_direction_pairs or waterfall_plando)):
|
||||
# in direction pairs, we may run into a case where we run out of pairable directions
|
||||
# since we need to ensure the dead ends will have something to connect to
|
||||
# or if fairy cave is plando'd, it may run into an issue where it is trying to get access to 2 separate
|
||||
# areas at once to give access to laurels
|
||||
# so, this is basically just resetting entrance pairing
|
||||
# this should be very rare, so this fail-safe shouldn't be covering up for an actual solution
|
||||
# this should never happen in decoupled, since it's entirely too flexible for that
|
||||
portal_pairs = backup_portal_pairs.copy()
|
||||
two_plus = two_plus2 = backup_two_plus.copy()
|
||||
two_plus_direction_tracker = backup_two_plus_direction_tracker.copy()
|
||||
random_object.shuffle(two_plus)
|
||||
connected_regions = backup_connected_regions.copy()
|
||||
rare_failure_count += 1
|
||||
fail_count = 0
|
||||
|
||||
if rare_failure_count > 100:
|
||||
raise Exception(f"Failed to pair regions due to rare pairing issues for {player_name}. "
|
||||
f"Unconnected regions: {non_dead_end_regions - connected_regions}.\n"
|
||||
f"Unconnected portals: {[portal.name for portal in two_plus]}")
|
||||
else:
|
||||
fail_count = 0
|
||||
previous_conn_num = len(connected_regions)
|
||||
|
||||
# find a portal in a connected region
|
||||
if check_success == 0:
|
||||
for portal in two_plus:
|
||||
if portal.region in connected_regions:
|
||||
portal1 = portal
|
||||
two_plus.remove(portal)
|
||||
check_success = 1
|
||||
break
|
||||
for portal in two_plus:
|
||||
if portal.region in connected_regions:
|
||||
# if there's more dead ends of a direction than two plus of the opposite direction,
|
||||
# then we'll run out of viable connections for those dead ends later
|
||||
# decoupled does not have this issue since dead ends aren't real in decoupled
|
||||
if not decoupled and entrance_layout == EntranceLayout.option_direction_pairs:
|
||||
if not too_few_portals_for_direction_pairs(portal.direction, 0):
|
||||
continue
|
||||
|
||||
# then we find a portal in an inaccessible region
|
||||
if check_success == 1:
|
||||
for portal in two_plus:
|
||||
if portal.region not in connected_regions:
|
||||
# if secret gathering place happens to get paired really late, you can end up running out
|
||||
if not has_laurels and len(two_plus) < 80:
|
||||
# if you plando'd secret gathering place with laurels at 10 fairies, you're the reason for this
|
||||
if waterfall_plando:
|
||||
cr = connected_regions.copy()
|
||||
cr.add(portal.region)
|
||||
if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_tricks):
|
||||
continue
|
||||
# if not waterfall_plando, then we just want to pair secret gathering place now
|
||||
elif portal.region != "Secret Gathering Place":
|
||||
portal1 = portal
|
||||
two_plus.remove(portal)
|
||||
break
|
||||
if not portal1:
|
||||
raise Exception("TUNIC: Failed to pair portals at first part of first phase.")
|
||||
|
||||
# then we find a portal in an unconnected region
|
||||
for portal in two_plus2:
|
||||
if portal.region not in connected_regions:
|
||||
# if secret gathering place happens to get paired really late, you can end up running out
|
||||
if not has_laurels and len(two_plus2) < 80:
|
||||
# if you plando'd secret gathering place with laurels at 10 fairies, you're the reason for this
|
||||
if waterfall_plando:
|
||||
cr = connected_regions.copy()
|
||||
cr.add(portal.region)
|
||||
if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_tricks):
|
||||
continue
|
||||
portal2 = portal
|
||||
connected_regions.add(portal.region)
|
||||
two_plus.remove(portal)
|
||||
check_success = 2
|
||||
break
|
||||
# if not waterfall_plando, then we just want to pair secret gathering place now
|
||||
elif portal.region != "Secret Gathering Place":
|
||||
continue
|
||||
|
||||
# if they're not facing opposite directions, just continue
|
||||
if entrance_layout == EntranceLayout.option_direction_pairs and not verify_direction_pair(portal, portal1):
|
||||
continue
|
||||
|
||||
# if you have direction pairs, we need to make sure we don't run out of spots for problem portals
|
||||
# this cuts down on using the failsafe significantly
|
||||
if not decoupled and entrance_layout == EntranceLayout.option_direction_pairs:
|
||||
should_continue = False
|
||||
# these portals are weird since they're one-ways essentially
|
||||
# we need to make sure they are connected in this first phase
|
||||
south_problems = ["Ziggurat Upper to Ziggurat Entry Hallway",
|
||||
"Ziggurat Tower to Ziggurat Upper", "Forest Belltower to Guard Captain Room"]
|
||||
if (portal.direction == Direction.south and portal.name not in south_problems
|
||||
and not too_few_portals_for_direction_pairs(portal.direction, 3)):
|
||||
for test_portal in two_plus:
|
||||
if test_portal.name in south_problems:
|
||||
should_continue = True
|
||||
# at risk of connecting frog's domain entry ladder to librarian exit
|
||||
if (portal.direction == Direction.ladder_down
|
||||
or portal.direction == Direction.ladder_up and portal.name != "Frog's Domain Ladder Exit"
|
||||
and not too_few_portals_for_direction_pairs(portal.direction, 1)):
|
||||
for test_portal in two_plus:
|
||||
if test_portal.name == "Frog's Domain Ladder Exit":
|
||||
should_continue = True
|
||||
if should_continue:
|
||||
continue
|
||||
|
||||
portal2 = portal
|
||||
connected_regions.add(get_portal_outlet_region(portal, world))
|
||||
two_plus2.remove(portal)
|
||||
break
|
||||
|
||||
if not portal2:
|
||||
if entrance_layout == EntranceLayout.option_direction_pairs or waterfall_plando:
|
||||
# portal1 doesn't have a valid direction pair yet, throw it back and start over
|
||||
two_plus.append(portal1)
|
||||
continue
|
||||
else:
|
||||
raise Exception(f"TUNIC: Failed to pair portals at second part of first phase for {world.player_name}.")
|
||||
|
||||
# once we have both portals, connect them and add the new region(s) to connected_regions
|
||||
if check_success == 2:
|
||||
if "Secret Gathering Place" in connected_regions:
|
||||
has_laurels = True
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks)
|
||||
portal_pairs[portal1] = portal2
|
||||
check_success = 0
|
||||
random_object.shuffle(two_plus)
|
||||
if not has_laurels and "Secret Gathering Place" in connected_regions:
|
||||
has_laurels = True
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks)
|
||||
|
||||
# for universal tracker, we want to skip shop gen
|
||||
if world.using_ut:
|
||||
shop_count = 0
|
||||
|
||||
for i in range(shop_count):
|
||||
portal1 = two_plus.pop()
|
||||
if portal1 is None:
|
||||
raise Exception("TUNIC: Too many shops in the pool, or something else went wrong.")
|
||||
portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}",
|
||||
destination="Previous Region", tag="_")
|
||||
create_shop_region(world, regions)
|
||||
|
||||
portal_pairs[portal1] = portal2
|
||||
two_plus_direction_tracker[portal1.direction] -= 1
|
||||
two_plus_direction_tracker[portal2.direction] -= 1
|
||||
portal1 = None
|
||||
portal2 = None
|
||||
random_object.shuffle(two_plus)
|
||||
if two_plus != two_plus2:
|
||||
random_object.shuffle(two_plus2)
|
||||
|
||||
# connect dead ends to random non-dead ends
|
||||
# none of the key events are in dead ends, so we don't need to do gate_before_switch
|
||||
# there are no dead ends in decoupled
|
||||
while len(dead_ends) > 0:
|
||||
if world.using_ut:
|
||||
break
|
||||
portal1 = two_plus.pop()
|
||||
portal2 = dead_ends.pop()
|
||||
portal_pairs[portal1] = portal2
|
||||
portal2 = dead_ends[0]
|
||||
for portal in two_plus:
|
||||
if entrance_layout == EntranceLayout.option_direction_pairs and not verify_direction_pair(portal, portal2):
|
||||
continue
|
||||
if entrance_layout == EntranceLayout.option_fixed_shop and portal.region == "Zig Skip Exit":
|
||||
continue
|
||||
portal1 = portal
|
||||
portal_pairs[portal1] = portal2
|
||||
two_plus.remove(portal1)
|
||||
dead_ends.remove(portal2)
|
||||
break
|
||||
else:
|
||||
raise Exception(f"Failed to pair {portal2.name} with anything in two_plus for player {world.player_name}.")
|
||||
|
||||
# then randomly connect the remaining portals to each other
|
||||
# every region is accessible, so gate_before_switch is not necessary
|
||||
while len(two_plus) > 1:
|
||||
final_pair_number = 0
|
||||
while len(two_plus) > 0:
|
||||
if world.using_ut:
|
||||
break
|
||||
portal1 = two_plus.pop()
|
||||
portal2 = two_plus.pop()
|
||||
final_pair_number += 1
|
||||
if final_pair_number > 10000:
|
||||
raise Exception(f"Failed to pair portals while pairing the final entrances off to each other. "
|
||||
f"Remaining portals in two_plus: {[portal.name for portal in two_plus]}. "
|
||||
f"Remaining portals in two_plus2: {[portal.name for portal in two_plus2]}.")
|
||||
portal1 = two_plus[0]
|
||||
two_plus.remove(portal1)
|
||||
portal2 = None
|
||||
if entrance_layout != EntranceLayout.option_direction_pairs:
|
||||
portal2 = two_plus2.pop()
|
||||
else:
|
||||
for portal in two_plus2:
|
||||
if verify_direction_pair(portal1, portal):
|
||||
portal2 = portal
|
||||
two_plus2.remove(portal2)
|
||||
break
|
||||
if portal2 is None:
|
||||
raise Exception("Something went wrong with the remaining two plus portals. Contact the TUNIC rando devs.")
|
||||
portal_pairs[portal1] = portal2
|
||||
|
||||
if len(two_plus) == 1:
|
||||
raise Exception("two plus had an odd number of portals, investigate this. last portal is " + two_plus[0].name)
|
||||
if len(two_plus2) > 0:
|
||||
raise Exception(f"TUNIC: Something went horribly wrong in ER for {world.player_name}. "
|
||||
f"Please contact the TUNIC rando devs.")
|
||||
|
||||
return portal_pairs
|
||||
|
||||
|
||||
# loop through our list of paired portals and make two-way connections
|
||||
def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dict[str, Region]) -> None:
|
||||
def create_randomized_entrances(world: "TunicWorld", portal_pairs: Dict[Portal, Portal], regions: Dict[str, Region]) -> None:
|
||||
for portal1, portal2 in portal_pairs.items():
|
||||
region1 = regions[portal1.region]
|
||||
region2 = regions[portal2.region]
|
||||
region1.connect(connecting_region=region2, name=portal1.name)
|
||||
region2.connect(connecting_region=region1, name=portal2.name)
|
||||
# connect to the outlet region if there is one, if not connect to the actual region
|
||||
regions[portal1.region].connect(
|
||||
connecting_region=regions[get_portal_outlet_region(portal2, world)],
|
||||
name=portal1.name)
|
||||
if not world.options.decoupled or not world.options.entrance_rando:
|
||||
regions[portal2.region].connect(
|
||||
connecting_region=regions[get_portal_outlet_region(portal1, world)],
|
||||
name=portal2.name)
|
||||
|
||||
|
||||
def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[str, Dict[str, List[List[str]]]],
|
||||
@@ -541,22 +801,58 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s
|
||||
return connected_regions
|
||||
|
||||
|
||||
# which directions are opposites
|
||||
direction_pairs: Dict[int, int] = {
|
||||
Direction.north: Direction.south,
|
||||
Direction.south: Direction.north,
|
||||
Direction.east: Direction.west,
|
||||
Direction.west: Direction.east,
|
||||
Direction.ladder_up: Direction.ladder_down,
|
||||
Direction.ladder_down: Direction.ladder_up,
|
||||
Direction.floor: Direction.floor,
|
||||
}
|
||||
|
||||
|
||||
# verify that two portals are in compatible directions
|
||||
def verify_direction_pair(portal1: Portal, portal2: Portal) -> bool:
|
||||
return portal1.direction == direction_pairs[portal2.direction]
|
||||
|
||||
|
||||
# verify that two plando'd portals are in compatible directions
|
||||
def verify_plando_directions(connection: PlandoConnection) -> bool:
|
||||
entrance_portal = None
|
||||
exit_portal = None
|
||||
for portal in portal_mapping:
|
||||
if connection.entrance == portal.name:
|
||||
entrance_portal = portal
|
||||
if connection.exit == portal.name:
|
||||
exit_portal = portal
|
||||
if entrance_portal and exit_portal:
|
||||
break
|
||||
# neither of these are shops, so verify the pair
|
||||
if entrance_portal and exit_portal:
|
||||
return verify_direction_pair(entrance_portal, exit_portal)
|
||||
# this is two shop portals, they can never pair directions
|
||||
elif not entrance_portal and not exit_portal:
|
||||
return False
|
||||
# if one of them is none, it's a shop, which has two possible directions
|
||||
elif not entrance_portal:
|
||||
return exit_portal.direction in [Direction.north, Direction.east]
|
||||
elif not exit_portal:
|
||||
return entrance_portal.direction in [Direction.north, Direction.east]
|
||||
else:
|
||||
# shouldn't be reachable, more of a just in case
|
||||
raise Exception("Something went very wrong with verify_plando_directions")
|
||||
|
||||
|
||||
# sort the portal dict by the name of the first portal, referring to the portal order in the master portal list
|
||||
def sort_portals(portal_pairs: Dict[Portal, Portal]) -> Dict[str, str]:
|
||||
def sort_portals(portal_pairs: Dict[Portal, Portal], world: "TunicWorld") -> Dict[str, str]:
|
||||
sorted_pairs: Dict[str, str] = {}
|
||||
reference_list: List[str] = [portal.name for portal in portal_mapping]
|
||||
reference_list.append("Shop Portal")
|
||||
|
||||
# note: this is not necessary yet since the shop portals aren't numbered yet -- they will be when decoupled happens
|
||||
# due to plando, there can be a variable number of shops
|
||||
# I could either do it like this, or just go up to like 200, this seemed better
|
||||
# shop_count = 0
|
||||
# for portal1, portal2 in portal_pairs.items():
|
||||
# if portal1.name.startswith("Shop"):
|
||||
# shop_count += 1
|
||||
# if portal2.name.startswith("Shop"):
|
||||
# shop_count += 1
|
||||
# reference_list.extend([f"Shop Portal {i + 1}" for i in range(shop_count)])
|
||||
largest_shop_number = max(world.used_shop_numbers)
|
||||
reference_list.extend([f"Shop Portal {i + 1}" for i in range(largest_shop_number)])
|
||||
|
||||
for name in reference_list:
|
||||
for portal1, portal2 in portal_pairs.items():
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Dict, Any, TYPE_CHECKING
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections,
|
||||
PerGameCommonOptions, OptionGroup, Visibility, NamedRange)
|
||||
PerGameCommonOptions, OptionGroup, Removed, Visibility, NamedRange)
|
||||
from .er_data import portal_mapping
|
||||
if TYPE_CHECKING:
|
||||
from . import TunicWorld
|
||||
@@ -147,14 +147,42 @@ class EntranceRando(TextChoice):
|
||||
|
||||
class FixedShop(Toggle):
|
||||
"""
|
||||
Forces the Windmill entrance to lead to a shop, and removes the remaining shops from the pool.
|
||||
Adds another entrance in Rooted Ziggurat Lower to keep an even number of entrances.
|
||||
Has no effect if Entrance Rando is not enabled.
|
||||
This option has been superseded by the Entrance Layout option.
|
||||
If enabled, it will override the Entrance Layout option.
|
||||
This is kept to keep older yamls working, and will be removed at a later date.
|
||||
"""
|
||||
visibility = Visibility.none
|
||||
internal_name = "fixed_shop"
|
||||
display_name = "Fewer Shops in Entrance Rando"
|
||||
|
||||
|
||||
class EntranceLayout(Choice):
|
||||
"""
|
||||
Decide how the Entrance Randomizer chooses how to pair the entrances.
|
||||
Standard: Entrances are randomly connected. There are 6 shops in the pool with this option.
|
||||
Fixed Shop: Forces the Windmill entrance to lead to a shop, and removes the other shops from the pool.
|
||||
Adds another entrance in Rooted Ziggurat Lower to keep an even number of entrances.
|
||||
Direction Pairs: Entrances facing opposite directions are paired together. There are 8 shops in the pool with this option.
|
||||
Note: For seed groups, if one player in a group chooses Fixed Shop and another chooses Direction Pairs, it will error out.
|
||||
Either of these options will override Standard within a seed group.
|
||||
"""
|
||||
internal_name = "entrance_layout"
|
||||
display_name = "Entrance Layout"
|
||||
option_standard = 0
|
||||
option_fixed_shop = 1
|
||||
option_direction_pairs = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class Decoupled(Toggle):
|
||||
"""
|
||||
Decouple the entrances, so that when you go from one entrance to another, the return trip won't necessarily bring you back to the same place.
|
||||
Note: For seed groups, all players in the group must have this option enabled or disabled.
|
||||
"""
|
||||
internal_name = "decoupled"
|
||||
display_name = "Decoupled Entrances"
|
||||
|
||||
|
||||
class LaurelsLocation(Choice):
|
||||
"""
|
||||
Force the Hero's Laurels to be placed at a location in your world.
|
||||
@@ -210,13 +238,22 @@ class LocalFill(NamedRange):
|
||||
class TunicPlandoConnections(PlandoConnections):
|
||||
"""
|
||||
Generic connection plando. Format is:
|
||||
- entrance: "Entrance Name"
|
||||
exit: "Exit Name"
|
||||
- entrance: Entrance Name
|
||||
exit: Exit Name
|
||||
direction: Direction
|
||||
percentage: 100
|
||||
Direction must be one of entrance, exit, or both, and defaults to both if omitted.
|
||||
Direction entrance means the entrance leads to the exit. Direction exit means the exit leads to the entrance.
|
||||
If you do not have Decoupled enabled, you do not need the direction line, as it will only use both.
|
||||
Percentage is an integer from 0 to 100 which determines whether that connection will be made. Defaults to 100 if omitted.
|
||||
If the Entrance Layout option is set to Standard or Fixed Shop, you can plando multiple shops.
|
||||
If the Entrance Layout option is set to Direction Pairs, your plando connections must be facing opposite directions.
|
||||
Shop Portal 1-6 are South portals, and Shop Portal 7-8 are West portals.
|
||||
This option does nothing if Entrance Rando is disabled.
|
||||
"""
|
||||
entrances = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"}
|
||||
exits = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"}
|
||||
shops = {f"Shop Portal {i + 1}" for i in range(500)}
|
||||
entrances = {portal.name for portal in portal_mapping}.union(shops)
|
||||
exits = {portal.name for portal in portal_mapping}.union(shops)
|
||||
|
||||
duplicate_exits = True
|
||||
|
||||
@@ -295,6 +332,16 @@ class LadderStorageWithoutItems(Toggle):
|
||||
display_name = "Ladder Storage without Items"
|
||||
|
||||
|
||||
class HiddenAllRandom(Toggle):
|
||||
"""
|
||||
Sets all options that can be random to random.
|
||||
For test gens.
|
||||
"""
|
||||
internal_name = "all_random"
|
||||
display_name = "All Random Debug"
|
||||
visibility = Visibility.none
|
||||
|
||||
|
||||
class LogicRules(Choice):
|
||||
"""
|
||||
This option has been superseded by the individual trick options.
|
||||
@@ -329,6 +376,7 @@ class TunicOptions(PerGameCommonOptions):
|
||||
start_with_sword: StartWithSword
|
||||
keys_behind_bosses: KeysBehindBosses
|
||||
ability_shuffling: AbilityShuffling
|
||||
|
||||
fool_traps: FoolTraps
|
||||
laurels_location: LaurelsLocation
|
||||
|
||||
@@ -343,7 +391,9 @@ class TunicOptions(PerGameCommonOptions):
|
||||
local_fill: LocalFill
|
||||
|
||||
entrance_rando: EntranceRando
|
||||
fixed_shop: FixedShop
|
||||
entrance_layout: EntranceLayout
|
||||
decoupled: Decoupled
|
||||
plando_connections: TunicPlandoConnections
|
||||
|
||||
combat_logic: CombatLogic
|
||||
lanternless: Lanternless
|
||||
@@ -352,10 +402,11 @@ class TunicOptions(PerGameCommonOptions):
|
||||
ice_grappling: IceGrappling
|
||||
ladder_storage: LadderStorage
|
||||
ladder_storage_without_items: LadderStorageWithoutItems
|
||||
|
||||
all_random: HiddenAllRandom
|
||||
|
||||
plando_connections: TunicPlandoConnections
|
||||
|
||||
logic_rules: LogicRules
|
||||
fixed_shop: FixedShop # will be removed at a later date
|
||||
logic_rules: Removed # fully removed in the direction pairs update
|
||||
|
||||
|
||||
tunic_option_groups = [
|
||||
@@ -372,8 +423,14 @@ tunic_option_groups = [
|
||||
LaurelsZips,
|
||||
IceGrappling,
|
||||
LadderStorage,
|
||||
LadderStorageWithoutItems
|
||||
])
|
||||
LadderStorageWithoutItems,
|
||||
]),
|
||||
OptionGroup("Entrance Randomizer", [
|
||||
EntranceRando,
|
||||
EntranceLayout,
|
||||
Decoupled,
|
||||
TunicPlandoConnections,
|
||||
]),
|
||||
]
|
||||
|
||||
tunic_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
from worlds.generic.Rules import set_rule, forbid_item, add_rule
|
||||
from BaseClasses import CollectionState
|
||||
@@ -157,8 +156,8 @@ def set_region_rules(world: "TunicWorld") -> None:
|
||||
|
||||
if options.ladder_storage >= LadderStorage.option_medium:
|
||||
# ls at any ladder in a safe spot in quarry to get to the monastery rope entrance
|
||||
world.get_region("Quarry Back").connect(world.get_region("Monastery"),
|
||||
rule=lambda state: can_ladder_storage(state, world))
|
||||
add_rule(world.get_entrance(entrance_name="Quarry Back -> Monastery"),
|
||||
rule=lambda state: can_ladder_storage(state, world))
|
||||
|
||||
|
||||
def set_location_rules(world: "TunicWorld") -> None:
|
||||
|
||||
@@ -78,7 +78,8 @@ class TestERSpecial(TunicTestBase):
|
||||
options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes,
|
||||
options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true,
|
||||
options.HexagonQuest.internal_name: options.HexagonQuest.option_false,
|
||||
options.FixedShop.internal_name: options.FixedShop.option_false,
|
||||
options.CombatLogic.internal_name: options.CombatLogic.option_off,
|
||||
options.EntranceLayout.internal_name: options.EntranceLayout.option_fixed_shop,
|
||||
options.IceGrappling.internal_name: options.IceGrappling.option_easy,
|
||||
"plando_connections": [
|
||||
{
|
||||
@@ -126,3 +127,262 @@ class TestLadderStorage(TunicTestBase):
|
||||
self.assertFalse(self.can_reach_location("Fortress Courtyard - Page Near Cave"))
|
||||
self.collect_by_name(["Pages 24-25 (Prayer)"])
|
||||
self.assertTrue(self.can_reach_location("Fortress Courtyard - Page Near Cave"))
|
||||
|
||||
|
||||
# check that it still functions if in decoupled and every single normal entrance leads to a shop
|
||||
class TestERDecoupledPlando(TunicTestBase):
|
||||
options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes,
|
||||
options.Decoupled.internal_name: options.Decoupled.option_true,
|
||||
"plando_connections": [
|
||||
{"entrance": "Stick House Entrance", "exit": "Shop Portal 1", "direction": "entrance"},
|
||||
{"entrance": "Windmill Entrance", "exit": "Shop Portal 2", "direction": "entrance"},
|
||||
{"entrance": "Well Ladder Entrance", "exit": "Shop Portal 3", "direction": "entrance"},
|
||||
{"entrance": "Entrance to Well from Well Rail", "exit": "Shop Portal 4", "direction": "entrance"},
|
||||
{"entrance": "Old House Door Entrance", "exit": "Shop Portal 5", "direction": "entrance"},
|
||||
{"entrance": "Old House Waterfall Entrance", "exit": "Shop Portal 6", "direction": "entrance"},
|
||||
{"entrance": "Entrance to Furnace from Well Rail", "exit": "Shop Portal 7", "direction": "entrance"},
|
||||
{"entrance": "Entrance to Furnace under Windmill", "exit": "Shop Portal 8", "direction": "entrance"},
|
||||
{"entrance": "Entrance to Furnace near West Garden", "exit": "Shop Portal 9",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Entrance to Furnace from Beach", "exit": "Shop Portal 10", "direction": "entrance"},
|
||||
{"entrance": "Caustic Light Cave Entrance", "exit": "Shop Portal 11", "direction": "entrance"},
|
||||
{"entrance": "Swamp Upper Entrance", "exit": "Shop Portal 12", "direction": "entrance"},
|
||||
{"entrance": "Swamp Lower Entrance", "exit": "Shop Portal 13", "direction": "entrance"},
|
||||
{"entrance": "Ruined Passage Not-Door Entrance", "exit": "Shop Portal 14", "direction": "entrance"},
|
||||
{"entrance": "Ruined Passage Door Entrance", "exit": "Shop Portal 15", "direction": "entrance"},
|
||||
{"entrance": "Atoll Upper Entrance", "exit": "Shop Portal 16", "direction": "entrance"},
|
||||
{"entrance": "Atoll Lower Entrance", "exit": "Shop Portal 17", "direction": "entrance"},
|
||||
{"entrance": "Special Shop Entrance", "exit": "Shop Portal 18", "direction": "entrance"},
|
||||
{"entrance": "Maze Cave Entrance", "exit": "Shop Portal 19", "direction": "entrance"},
|
||||
{"entrance": "West Garden Entrance near Belltower", "exit": "Shop Portal 20",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "West Garden Entrance from Furnace", "exit": "Shop Portal 21", "direction": "entrance"},
|
||||
{"entrance": "West Garden Laurels Entrance", "exit": "Shop Portal 22", "direction": "entrance"},
|
||||
{"entrance": "Temple Door Entrance", "exit": "Shop Portal 23", "direction": "entrance"},
|
||||
{"entrance": "Temple Rafters Entrance", "exit": "Shop Portal 24", "direction": "entrance"},
|
||||
{"entrance": "Ruined Shop Entrance", "exit": "Shop Portal 25", "direction": "entrance"},
|
||||
{"entrance": "Patrol Cave Entrance", "exit": "Shop Portal 26", "direction": "entrance"},
|
||||
{"entrance": "Hourglass Cave Entrance", "exit": "Shop Portal 27", "direction": "entrance"},
|
||||
{"entrance": "Changing Room Entrance", "exit": "Shop Portal 28", "direction": "entrance"},
|
||||
{"entrance": "Cube Cave Entrance", "exit": "Shop Portal 29", "direction": "entrance"},
|
||||
{"entrance": "Stairs from Overworld to Mountain", "exit": "Shop Portal 30", "direction": "entrance"},
|
||||
{"entrance": "Overworld to Fortress", "exit": "Shop Portal 31", "direction": "entrance"},
|
||||
{"entrance": "Fountain HC Door Entrance", "exit": "Shop Portal 32", "direction": "entrance"},
|
||||
{"entrance": "Southeast HC Door Entrance", "exit": "Shop Portal 33", "direction": "entrance"},
|
||||
{"entrance": "Overworld to Quarry Connector", "exit": "Shop Portal 34", "direction": "entrance"},
|
||||
{"entrance": "Dark Tomb Main Entrance", "exit": "Shop Portal 35", "direction": "entrance"},
|
||||
{"entrance": "Overworld to Forest Belltower", "exit": "Shop Portal 36", "direction": "entrance"},
|
||||
{"entrance": "Town to Far Shore", "exit": "Shop Portal 37", "direction": "entrance"},
|
||||
{"entrance": "Spawn to Far Shore", "exit": "Shop Portal 38", "direction": "entrance"},
|
||||
{"entrance": "Secret Gathering Place Entrance", "exit": "Shop Portal 39", "direction": "entrance"},
|
||||
{"entrance": "Secret Gathering Place Exit", "exit": "Shop Portal 40", "direction": "entrance"},
|
||||
{"entrance": "Windmill Exit", "exit": "Shop Portal 41", "direction": "entrance"},
|
||||
{"entrance": "Windmill Shop", "exit": "Shop Portal 42", "direction": "entrance"},
|
||||
{"entrance": "Old House Door Exit", "exit": "Shop Portal 43", "direction": "entrance"},
|
||||
{"entrance": "Old House to Glyph Tower", "exit": "Shop Portal 44", "direction": "entrance"},
|
||||
{"entrance": "Old House Waterfall Exit", "exit": "Shop Portal 45", "direction": "entrance"},
|
||||
{"entrance": "Glyph Tower Exit", "exit": "Shop Portal 46", "direction": "entrance"},
|
||||
{"entrance": "Changing Room Exit", "exit": "Shop Portal 47", "direction": "entrance"},
|
||||
{"entrance": "Fountain HC Room Exit", "exit": "Shop Portal 48", "direction": "entrance"},
|
||||
{"entrance": "Cube Cave Exit", "exit": "Shop Portal 49", "direction": "entrance"},
|
||||
{"entrance": "Guard Patrol Cave Exit", "exit": "Shop Portal 50", "direction": "entrance"},
|
||||
{"entrance": "Ruined Shop Exit", "exit": "Shop Portal 51", "direction": "entrance"},
|
||||
{"entrance": "Furnace Exit towards Well", "exit": "Shop Portal 52", "direction": "entrance"},
|
||||
{"entrance": "Furnace Exit to Dark Tomb", "exit": "Shop Portal 53", "direction": "entrance"},
|
||||
{"entrance": "Furnace Exit towards West Garden", "exit": "Shop Portal 54", "direction": "entrance"},
|
||||
{"entrance": "Furnace Exit to Beach", "exit": "Shop Portal 55", "direction": "entrance"},
|
||||
{"entrance": "Furnace Exit under Windmill", "exit": "Shop Portal 56", "direction": "entrance"},
|
||||
{"entrance": "Stick House Exit", "exit": "Shop Portal 57", "direction": "entrance"},
|
||||
{"entrance": "Ruined Passage Not-Door Exit", "exit": "Shop Portal 58", "direction": "entrance"},
|
||||
{"entrance": "Ruined Passage Door Exit", "exit": "Shop Portal 59", "direction": "entrance"},
|
||||
{"entrance": "Southeast HC Room Exit", "exit": "Shop Portal 60", "direction": "entrance"},
|
||||
{"entrance": "Caustic Light Cave Exit", "exit": "Shop Portal 61", "direction": "entrance"},
|
||||
{"entrance": "Maze Cave Exit", "exit": "Shop Portal 62", "direction": "entrance"},
|
||||
{"entrance": "Hourglass Cave Exit", "exit": "Shop Portal 63", "direction": "entrance"},
|
||||
{"entrance": "Special Shop Exit", "exit": "Shop Portal 64", "direction": "entrance"},
|
||||
{"entrance": "Temple Rafters Exit", "exit": "Shop Portal 65", "direction": "entrance"},
|
||||
{"entrance": "Temple Door Exit", "exit": "Shop Portal 66", "direction": "entrance"},
|
||||
{"entrance": "Forest Belltower to Fortress", "exit": "Shop Portal 67", "direction": "entrance"},
|
||||
{"entrance": "Forest Belltower to Forest", "exit": "Shop Portal 68", "direction": "entrance"},
|
||||
{"entrance": "Forest Belltower to Overworld", "exit": "Shop Portal 69", "direction": "entrance"},
|
||||
{"entrance": "Forest Belltower to Guard Captain Room", "exit": "Shop Portal 70",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Forest to Belltower", "exit": "Shop Portal 71", "direction": "entrance"},
|
||||
{"entrance": "Forest Guard House 1 Lower Entrance", "exit": "Shop Portal 72",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Forest Guard House 1 Gate Entrance", "exit": "Shop Portal 73",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Forest Dance Fox Outside Doorway", "exit": "Shop Portal 74", "direction": "entrance"},
|
||||
{"entrance": "Forest to Far Shore", "exit": "Shop Portal 75", "direction": "entrance"},
|
||||
{"entrance": "Forest Guard House 2 Lower Entrance", "exit": "Shop Portal 76",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Forest Guard House 2 Upper Entrance", "exit": "Shop Portal 77",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Forest Grave Path Lower Entrance", "exit": "Shop Portal 78", "direction": "entrance"},
|
||||
{"entrance": "Forest Grave Path Upper Entrance", "exit": "Shop Portal 79", "direction": "entrance"},
|
||||
{"entrance": "Forest Grave Path Upper Exit", "exit": "Shop Portal 80", "direction": "entrance"},
|
||||
{"entrance": "Forest Grave Path Lower Exit", "exit": "Shop Portal 81", "direction": "entrance"},
|
||||
{"entrance": "East Forest Hero's Grave", "exit": "Shop Portal 82", "direction": "entrance"},
|
||||
{"entrance": "Guard House 1 Dance Fox Exit", "exit": "Shop Portal 83", "direction": "entrance"},
|
||||
{"entrance": "Guard House 1 Lower Exit", "exit": "Shop Portal 84", "direction": "entrance"},
|
||||
{"entrance": "Guard House 1 Upper Forest Exit", "exit": "Shop Portal 85", "direction": "entrance"},
|
||||
{"entrance": "Guard House 1 to Guard Captain Room", "exit": "Shop Portal 86",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Guard House 2 Lower Exit", "exit": "Shop Portal 87", "direction": "entrance"},
|
||||
{"entrance": "Guard House 2 Upper Exit", "exit": "Shop Portal 88", "direction": "entrance"},
|
||||
{"entrance": "Guard Captain Room Non-Gate Exit", "exit": "Shop Portal 89", "direction": "entrance"},
|
||||
{"entrance": "Guard Captain Room Gate Exit", "exit": "Shop Portal 90", "direction": "entrance"},
|
||||
{"entrance": "Well Ladder Exit", "exit": "Shop Portal 91", "direction": "entrance"},
|
||||
{"entrance": "Well to Well Boss", "exit": "Shop Portal 92", "direction": "entrance"},
|
||||
{"entrance": "Well Exit towards Furnace", "exit": "Shop Portal 93", "direction": "entrance"},
|
||||
{"entrance": "Well Boss to Well", "exit": "Shop Portal 94", "direction": "entrance"},
|
||||
{"entrance": "Checkpoint to Dark Tomb", "exit": "Shop Portal 95", "direction": "entrance"},
|
||||
{"entrance": "Dark Tomb to Overworld", "exit": "Shop Portal 96", "direction": "entrance"},
|
||||
{"entrance": "Dark Tomb to Furnace", "exit": "Shop Portal 97", "direction": "entrance"},
|
||||
{"entrance": "Dark Tomb to Checkpoint", "exit": "Shop Portal 98", "direction": "entrance"},
|
||||
{"entrance": "West Garden Exit near Hero's Grave", "exit": "Shop Portal 99",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "West Garden to Magic Dagger House", "exit": "Shop Portal 100",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "West Garden Exit after Boss", "exit": "Shop Portal 101", "direction": "entrance"},
|
||||
{"entrance": "West Garden Shop", "exit": "Shop Portal 102", "direction": "entrance"},
|
||||
{"entrance": "West Garden Laurels Exit", "exit": "Shop Portal 103", "direction": "entrance"},
|
||||
{"entrance": "West Garden Hero's Grave", "exit": "Shop Portal 104", "direction": "entrance"},
|
||||
{"entrance": "West Garden to Far Shore", "exit": "Shop Portal 105", "direction": "entrance"},
|
||||
{"entrance": "Magic Dagger House Exit", "exit": "Shop Portal 106", "direction": "entrance"},
|
||||
{"entrance": "Fortress Courtyard to Fortress Grave Path Lower", "exit": "Shop Portal 107",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Fortress Courtyard to Fortress Grave Path Upper", "exit": "Shop Portal 108",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Fortress Courtyard to Fortress Interior", "exit": "Shop Portal 109",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Fortress Courtyard to East Fortress", "exit": "Shop Portal 110",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Fortress Courtyard to Beneath the Vault", "exit": "Shop Portal 111",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Fortress Courtyard to Forest Belltower", "exit": "Shop Portal 112",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Fortress Courtyard to Overworld", "exit": "Shop Portal 113", "direction": "entrance"},
|
||||
{"entrance": "Fortress Courtyard Shop", "exit": "Shop Portal 114", "direction": "entrance"},
|
||||
{"entrance": "Beneath the Vault to Fortress Interior", "exit": "Shop Portal 115",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Beneath the Vault to Fortress Courtyard", "exit": "Shop Portal 116",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Fortress Interior Main Exit", "exit": "Shop Portal 117", "direction": "entrance"},
|
||||
{"entrance": "Fortress Interior to Beneath the Earth", "exit": "Shop Portal 118",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Fortress Interior to Siege Engine Arena", "exit": "Shop Portal 119",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Fortress Interior Shop", "exit": "Shop Portal 120", "direction": "entrance"},
|
||||
{"entrance": "Fortress Interior to East Fortress Upper", "exit": "Shop Portal 121",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Fortress Interior to East Fortress Lower", "exit": "Shop Portal 122",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "East Fortress to Interior Lower", "exit": "Shop Portal 123", "direction": "entrance"},
|
||||
{"entrance": "East Fortress to Courtyard", "exit": "Shop Portal 124", "direction": "entrance"},
|
||||
{"entrance": "East Fortress to Interior Upper", "exit": "Shop Portal 125", "direction": "entrance"},
|
||||
{"entrance": "Fortress Grave Path Lower Exit", "exit": "Shop Portal 126", "direction": "entrance"},
|
||||
{"entrance": "Fortress Hero's Grave", "exit": "Shop Portal 127", "direction": "entrance"},
|
||||
{"entrance": "Fortress Grave Path Upper Exit", "exit": "Shop Portal 128", "direction": "entrance"},
|
||||
{"entrance": "Fortress Grave Path Dusty Entrance", "exit": "Shop Portal 129",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Dusty Exit", "exit": "Shop Portal 130", "direction": "entrance"},
|
||||
{"entrance": "Siege Engine Arena to Fortress", "exit": "Shop Portal 131", "direction": "entrance"},
|
||||
{"entrance": "Fortress to Far Shore", "exit": "Shop Portal 132", "direction": "entrance"},
|
||||
{"entrance": "Atoll Upper Exit", "exit": "Shop Portal 133", "direction": "entrance"},
|
||||
{"entrance": "Atoll Lower Exit", "exit": "Shop Portal 134", "direction": "entrance"},
|
||||
{"entrance": "Atoll Shop", "exit": "Shop Portal 135", "direction": "entrance"},
|
||||
{"entrance": "Atoll to Far Shore", "exit": "Shop Portal 136", "direction": "entrance"},
|
||||
{"entrance": "Atoll Statue Teleporter", "exit": "Shop Portal 137", "direction": "entrance"},
|
||||
{"entrance": "Frog Stairs Eye Entrance", "exit": "Shop Portal 138", "direction": "entrance"},
|
||||
{"entrance": "Frog Stairs Mouth Entrance", "exit": "Shop Portal 139", "direction": "entrance"},
|
||||
{"entrance": "Frog Stairs Eye Exit", "exit": "Shop Portal 140", "direction": "entrance"},
|
||||
{"entrance": "Frog Stairs Mouth Exit", "exit": "Shop Portal 141", "direction": "entrance"},
|
||||
{"entrance": "Frog Stairs to Frog's Domain's Entrance", "exit": "Shop Portal 142",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Frog Stairs to Frog's Domain's Exit", "exit": "Shop Portal 143",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Frog's Domain Ladder Exit", "exit": "Shop Portal 144", "direction": "entrance"},
|
||||
{"entrance": "Frog's Domain Orb Exit", "exit": "Shop Portal 145", "direction": "entrance"},
|
||||
{"entrance": "Library Exterior Tree", "exit": "Shop Portal 146", "direction": "entrance"},
|
||||
{"entrance": "Library Exterior Ladder", "exit": "Shop Portal 147", "direction": "entrance"},
|
||||
{"entrance": "Library Hall Bookshelf Exit", "exit": "Shop Portal 148", "direction": "entrance"},
|
||||
{"entrance": "Library Hero's Grave", "exit": "Shop Portal 149", "direction": "entrance"},
|
||||
{"entrance": "Library Hall to Rotunda", "exit": "Shop Portal 150", "direction": "entrance"},
|
||||
{"entrance": "Library Rotunda Lower Exit", "exit": "Shop Portal 151", "direction": "entrance"},
|
||||
{"entrance": "Library Rotunda Upper Exit", "exit": "Shop Portal 152", "direction": "entrance"},
|
||||
{"entrance": "Library Lab to Rotunda", "exit": "Shop Portal 153", "direction": "entrance"},
|
||||
{"entrance": "Library to Far Shore", "exit": "Shop Portal 154", "direction": "entrance"},
|
||||
{"entrance": "Library Lab to Librarian Arena", "exit": "Shop Portal 155", "direction": "entrance"},
|
||||
{"entrance": "Librarian Arena Exit", "exit": "Shop Portal 156", "direction": "entrance"},
|
||||
{"entrance": "Stairs to Top of the Mountain", "exit": "Shop Portal 157", "direction": "entrance"},
|
||||
{"entrance": "Mountain to Quarry", "exit": "Shop Portal 158", "direction": "entrance"},
|
||||
{"entrance": "Mountain to Overworld", "exit": "Shop Portal 159", "direction": "entrance"},
|
||||
{"entrance": "Top of the Mountain Exit", "exit": "Shop Portal 160", "direction": "entrance"},
|
||||
{"entrance": "Quarry Connector to Overworld", "exit": "Shop Portal 161", "direction": "entrance"},
|
||||
{"entrance": "Quarry Connector to Quarry", "exit": "Shop Portal 162", "direction": "entrance"},
|
||||
{"entrance": "Quarry to Overworld Exit", "exit": "Shop Portal 163", "direction": "entrance"},
|
||||
{"entrance": "Quarry Shop", "exit": "Shop Portal 164", "direction": "entrance"},
|
||||
{"entrance": "Quarry to Monastery Front", "exit": "Shop Portal 165", "direction": "entrance"},
|
||||
{"entrance": "Quarry to Monastery Back", "exit": "Shop Portal 166", "direction": "entrance"},
|
||||
{"entrance": "Quarry to Mountain", "exit": "Shop Portal 167", "direction": "entrance"},
|
||||
{"entrance": "Quarry to Ziggurat", "exit": "Shop Portal 168", "direction": "entrance"},
|
||||
{"entrance": "Quarry to Far Shore", "exit": "Shop Portal 169", "direction": "entrance"},
|
||||
{"entrance": "Monastery Rear Exit", "exit": "Shop Portal 170", "direction": "entrance"},
|
||||
{"entrance": "Monastery Front Exit", "exit": "Shop Portal 171", "direction": "entrance"},
|
||||
{"entrance": "Monastery Hero's Grave", "exit": "Shop Portal 172", "direction": "entrance"},
|
||||
{"entrance": "Ziggurat Entry Hallway to Ziggurat Upper", "exit": "Shop Portal 173",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Ziggurat Entry Hallway to Quarry", "exit": "Shop Portal 174", "direction": "entrance"},
|
||||
{"entrance": "Ziggurat Upper to Ziggurat Entry Hallway", "exit": "Shop Portal 175",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Ziggurat Upper to Ziggurat Tower", "exit": "Shop Portal 176", "direction": "entrance"},
|
||||
{"entrance": "Ziggurat Tower to Ziggurat Upper", "exit": "Shop Portal 177", "direction": "entrance"},
|
||||
{"entrance": "Ziggurat Tower to Ziggurat Lower", "exit": "Shop Portal 178", "direction": "entrance"},
|
||||
{"entrance": "Ziggurat Lower to Ziggurat Tower", "exit": "Shop Portal 179", "direction": "entrance"},
|
||||
{"entrance": "Ziggurat Portal Room Entrance", "exit": "Shop Portal 180", "direction": "entrance"},
|
||||
{"entrance": "Ziggurat Portal Room Exit", "exit": "Shop Portal 181", "direction": "entrance"},
|
||||
{"entrance": "Ziggurat to Far Shore", "exit": "Shop Portal 182", "direction": "entrance"},
|
||||
{"entrance": "Swamp Lower Exit", "exit": "Shop Portal 183", "direction": "entrance"},
|
||||
{"entrance": "Swamp to Cathedral Main Entrance", "exit": "Shop Portal 184", "direction": "entrance"},
|
||||
{"entrance": "Swamp to Cathedral Secret Legend Room Entrance", "exit": "Shop Portal 185",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Swamp to Gauntlet", "exit": "Shop Portal 186", "direction": "entrance"},
|
||||
{"entrance": "Swamp Shop", "exit": "Shop Portal 187", "direction": "entrance"},
|
||||
{"entrance": "Swamp Upper Exit", "exit": "Shop Portal 188", "direction": "entrance"},
|
||||
{"entrance": "Swamp Hero's Grave", "exit": "Shop Portal 189", "direction": "entrance"},
|
||||
{"entrance": "Cathedral Main Exit", "exit": "Shop Portal 190", "direction": "entrance"},
|
||||
{"entrance": "Cathedral Elevator", "exit": "Shop Portal 191", "direction": "entrance"},
|
||||
{"entrance": "Cathedral Secret Legend Room Exit", "exit": "Shop Portal 192",
|
||||
"direction": "entrance"},
|
||||
{"entrance": "Gauntlet to Swamp", "exit": "Shop Portal 193", "direction": "entrance"},
|
||||
{"entrance": "Gauntlet Elevator", "exit": "Shop Portal 194", "direction": "entrance"},
|
||||
{"entrance": "Gauntlet Shop", "exit": "Shop Portal 195", "direction": "entrance"},
|
||||
{"entrance": "Hero's Grave to Fortress", "exit": "Shop Portal 196", "direction": "entrance"},
|
||||
{"entrance": "Hero's Grave to Monastery", "exit": "Shop Portal 197", "direction": "entrance"},
|
||||
{"entrance": "Hero's Grave to West Garden", "exit": "Shop Portal 198", "direction": "entrance"},
|
||||
{"entrance": "Hero's Grave to East Forest", "exit": "Shop Portal 199", "direction": "entrance"},
|
||||
{"entrance": "Hero's Grave to Library", "exit": "Shop Portal 200", "direction": "entrance"},
|
||||
{"entrance": "Hero's Grave to Swamp", "exit": "Shop Portal 201", "direction": "entrance"},
|
||||
{"entrance": "Far Shore to West Garden", "exit": "Shop Portal 202", "direction": "entrance"},
|
||||
{"entrance": "Far Shore to Library", "exit": "Shop Portal 203", "direction": "entrance"},
|
||||
{"entrance": "Far Shore to Quarry", "exit": "Shop Portal 204", "direction": "entrance"},
|
||||
{"entrance": "Far Shore to East Forest", "exit": "Shop Portal 205", "direction": "entrance"},
|
||||
{"entrance": "Far Shore to Fortress", "exit": "Shop Portal 206", "direction": "entrance"},
|
||||
{"entrance": "Far Shore to Atoll", "exit": "Shop Portal 207", "direction": "entrance"},
|
||||
{"entrance": "Far Shore to Ziggurat", "exit": "Shop Portal 208", "direction": "entrance"},
|
||||
{"entrance": "Far Shore to Heir", "exit": "Shop Portal 209", "direction": "entrance"},
|
||||
{"entrance": "Far Shore to Town", "exit": "Shop Portal 210", "direction": "entrance"},
|
||||
{"entrance": "Far Shore to Spawn", "exit": "Shop Portal 211", "direction": "entrance"},
|
||||
{"entrance": "Heir Arena Exit", "exit": "Shop Portal 212", "direction": "entrance"},
|
||||
{"entrance": "Purgatory Bottom Exit", "exit": "Shop Portal 213", "direction": "entrance"},
|
||||
{"entrance": "Purgatory Top Exit", "exit": "Shop Portal 214", "direction": "entrance"},
|
||||
{"entrance": "Shop Portal 215", "exit": "Shop Portal 216", "direction": "entrance"},
|
||||
{"entrance": "Shop Portal 217", "exit": "Shop Portal 218", "direction": "entrance"},
|
||||
{"entrance": "Shop Portal 219", "exit": "Shop Portal 220", "direction": "entrance"},
|
||||
{"entrance": "Shop Portal 221", "exit": "Shop Portal 222", "direction": "entrance"},
|
||||
{"entrance": "Shop Portal 223", "exit": "Shop Portal 224", "direction": "entrance"},
|
||||
{"entrance": "Shop Portal 225", "exit": "Shop Portal 226", "direction": "entrance"},
|
||||
{"entrance": "Shop Portal 227", "exit": "Shop Portal 228", "direction": "entrance"},
|
||||
{"entrance": "Shop Portal 229", "exit": "Shop Portal 230", "direction": "entrance"},
|
||||
]}
|
||||
|
||||
Reference in New Issue
Block a user