diff --git a/BaseClasses.py b/BaseClasses.py
index 29264f34ab..b40b872f0c 100644
--- a/BaseClasses.py
+++ b/BaseClasses.py
@@ -692,17 +692,25 @@ class CollectionState():
def update_reachable_regions(self, player: int):
self.stale[player] = False
+ world: AutoWorld.World = self.multiworld.worlds[player]
reachable_regions = self.reachable_regions[player]
- blocked_connections = self.blocked_connections[player]
queue = deque(self.blocked_connections[player])
- start = self.multiworld.get_region("Menu", player)
+ start: Region = world.get_region(world.origin_region_name)
# init on first call - this can't be done on construction since the regions don't exist yet
if start not in reachable_regions:
reachable_regions.add(start)
- blocked_connections.update(start.exits)
+ self.blocked_connections[player].update(start.exits)
queue.extend(start.exits)
+ if world.explicit_indirect_conditions:
+ self._update_reachable_regions_explicit_indirect_conditions(player, queue)
+ else:
+ self._update_reachable_regions_auto_indirect_conditions(player, queue)
+
+ def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque):
+ reachable_regions = self.reachable_regions[player]
+ blocked_connections = self.blocked_connections[player]
# run BFS on all connections, and keep track of those blocked by missing items
while queue:
connection = queue.popleft()
@@ -722,6 +730,29 @@ class CollectionState():
if new_entrance in blocked_connections and new_entrance not in queue:
queue.append(new_entrance)
+ def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque):
+ reachable_regions = self.reachable_regions[player]
+ blocked_connections = self.blocked_connections[player]
+ new_connection: bool = True
+ # run BFS on all connections, and keep track of those blocked by missing items
+ while new_connection:
+ new_connection = False
+ while queue:
+ connection = queue.popleft()
+ new_region = connection.connected_region
+ if new_region in reachable_regions:
+ blocked_connections.remove(connection)
+ elif connection.can_reach(self):
+ assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
+ reachable_regions.add(new_region)
+ blocked_connections.remove(connection)
+ blocked_connections.update(new_region.exits)
+ queue.extend(new_region.exits)
+ self.path[new_region] = (new_region.name, self.path.get(connection, None))
+ new_connection = True
+ # sweep for indirect connections, mostly Entrance.can_reach(unrelated_Region)
+ queue.extend(blocked_connections)
+
def copy(self) -> CollectionState:
ret = CollectionState(self.multiworld)
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}
@@ -1176,7 +1207,7 @@ class ItemClassification(IntFlag):
filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
progression = 0b0001 # Item that is logically relevant
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
- trap = 0b0100 # detrimental or entirely useless (nothing) item
+ trap = 0b0100 # detrimental item
skip_balancing = 0b1000 # should technically never occur on its own
# Item that is logically relevant, but progression balancing should not touch.
# Typically currency or other counted items.
diff --git a/CommonClient.py b/CommonClient.py
index 750bee80bd..911de4226d 100644
--- a/CommonClient.py
+++ b/CommonClient.py
@@ -662,17 +662,19 @@ class CommonContext:
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
- def run_gui(self):
- """Import kivy UI system and start running it as self.ui_task."""
+ def make_gui(self) -> type:
+ """To return the Kivy App class needed for run_gui so it can be overridden before being built"""
from kvui import GameManager
class TextManager(GameManager):
- logging_pairs = [
- ("Client", "Archipelago")
- ]
base_title = "Archipelago Text Client"
- self.ui = TextManager(self)
+ return TextManager
+
+ def run_gui(self):
+ """Import kivy UI system from make_gui() and start running it as self.ui_task."""
+ ui_class = self.make_gui()
+ self.ui = ui_class(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def run_cli(self):
@@ -994,7 +996,7 @@ def get_base_parser(description: typing.Optional[str] = None):
return parser
-def run_as_textclient():
+def run_as_textclient(*args):
class TextContext(CommonContext):
# Text Mode to use !hint and such with games that have no text entry
tags = CommonContext.tags | {"TextOnly"}
@@ -1033,15 +1035,18 @@ def run_as_textclient():
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
parser.add_argument("url", nargs="?", help="Archipelago connection url")
- args = parser.parse_args()
+ args = parser.parse_args(args)
if args.url:
url = urllib.parse.urlparse(args.url)
- args.connect = url.netloc
- if url.username:
- args.name = urllib.parse.unquote(url.username)
- if url.password:
- args.password = urllib.parse.unquote(url.password)
+ if url.scheme == "archipelago":
+ args.connect = url.netloc
+ if url.username:
+ args.name = urllib.parse.unquote(url.username)
+ if url.password:
+ args.password = urllib.parse.unquote(url.password)
+ else:
+ parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
colorama.init()
@@ -1051,4 +1056,4 @@ def run_as_textclient():
if __name__ == '__main__':
logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING
- run_as_textclient()
+ run_as_textclient(*sys.argv[1:]) # default value for parse_args
diff --git a/Generate.py b/Generate.py
index 6220c0eb81..4eba05cc52 100644
--- a/Generate.py
+++ b/Generate.py
@@ -155,6 +155,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
erargs.outputpath = args.outputpath
erargs.skip_prog_balancing = args.skip_prog_balancing
erargs.skip_output = args.skip_output
+ erargs.name = {}
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
@@ -202,7 +203,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
if path == args.weights_file_path: # if name came from the weights file, just use base player name
erargs.name[player] = f"Player{player}"
- elif not erargs.name[player]: # if name was not specified, generate it from filename
+ elif player not in erargs.name: # if name was not specified, generate it from filename
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
diff --git a/Launcher.py b/Launcher.py
index 6b66b2a3a6..42f93547cc 100644
--- a/Launcher.py
+++ b/Launcher.py
@@ -16,10 +16,11 @@ import multiprocessing
import shlex
import subprocess
import sys
+import urllib.parse
import webbrowser
from os.path import isfile
from shutil import which
-from typing import Callable, Sequence, Union, Optional
+from typing import Callable, Optional, Sequence, Tuple, Union
import Utils
import settings
@@ -107,7 +108,81 @@ components.extend([
])
-def identify(path: Union[None, str]):
+def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
+ url = urllib.parse.urlparse(path)
+ queries = urllib.parse.parse_qs(url.query)
+ launch_args = (path, *launch_args)
+ client_component = None
+ text_client_component = None
+ if "game" in queries:
+ game = queries["game"][0]
+ else: # TODO around 0.6.0 - this is for pre this change webhost uri's
+ game = "Archipelago"
+ for component in components:
+ if component.supports_uri and component.game_name == game:
+ client_component = component
+ elif component.display_name == "Text Client":
+ text_client_component = component
+
+ from kvui import App, Button, BoxLayout, Label, Clock, Window
+
+ class Popup(App):
+ timer_label: Label
+ remaining_time: Optional[int]
+
+ def __init__(self):
+ self.title = "Connect to Multiworld"
+ self.icon = r"data/icon.png"
+ super().__init__()
+
+ def build(self):
+ layout = BoxLayout(orientation="vertical")
+
+ if client_component is None:
+ self.remaining_time = 7
+ label_text = (f"A game client able to parse URIs was not detected for {game}.\n"
+ f"Launching Text Client in 7 seconds...")
+ self.timer_label = Label(text=label_text)
+ layout.add_widget(self.timer_label)
+ Clock.schedule_interval(self.update_label, 1)
+ else:
+ layout.add_widget(Label(text="Select client to open and connect with."))
+ button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
+
+ text_client_button = Button(
+ text=text_client_component.display_name,
+ on_release=lambda *args: run_component(text_client_component, *launch_args)
+ )
+ button_row.add_widget(text_client_button)
+
+ game_client_button = Button(
+ text=client_component.display_name,
+ on_release=lambda *args: run_component(client_component, *launch_args)
+ )
+ button_row.add_widget(game_client_button)
+
+ layout.add_widget(button_row)
+
+ return layout
+
+ def update_label(self, dt):
+ if self.remaining_time > 1:
+ # countdown the timer and string replace the number
+ self.remaining_time -= 1
+ self.timer_label.text = self.timer_label.text.replace(
+ str(self.remaining_time + 1), str(self.remaining_time)
+ )
+ else:
+ # our timer is finished so launch text client and close down
+ run_component(text_client_component, *launch_args)
+ Clock.unschedule(self.update_label)
+ App.get_running_app().stop()
+ Window.close()
+
+ Popup().run()
+
+
+def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
if path is None:
return None, None
for component in components:
@@ -299,20 +374,24 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
elif not args:
args = {}
- if args.get("Patch|Game|Component", None) is not None:
- file, component = identify(args["Patch|Game|Component"])
+ path = args.get("Patch|Game|Component|url", None)
+ if path is not None:
+ if path.startswith("archipelago://"):
+ handle_uri(path, args.get("args", ()))
+ return
+ file, component = identify(path)
if file:
args['file'] = file
if component:
args['component'] = component
if not component:
- logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
+ logging.warning(f"Could not identify Component responsible for {path}")
if args["update_settings"]:
update_settings()
- if 'file' in args:
+ if "file" in args:
run_component(args["component"], args["file"], *args["args"])
- elif 'component' in args:
+ elif "component" in args:
run_component(args["component"], *args["args"])
elif not args["update_settings"]:
run_gui()
@@ -322,12 +401,16 @@ if __name__ == '__main__':
init_logging('Launcher')
Utils.freeze_support()
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
- parser = argparse.ArgumentParser(description='Archipelago Launcher')
+ parser = argparse.ArgumentParser(
+ description='Archipelago Launcher',
+ usage="[-h] [--update_settings] [Patch|Game|Component] [-- component args here]"
+ )
run_group = parser.add_argument_group("Run")
run_group.add_argument("--update_settings", action="store_true",
help="Update host.yaml and exit.")
- run_group.add_argument("Patch|Game|Component", type=str, nargs="?",
- help="Pass either a patch file, a generated game or the name of a component to run.")
+ run_group.add_argument("Patch|Game|Component|url", type=str, nargs="?",
+ help="Pass either a patch file, a generated game, the component name to run, or a url to "
+ "connect with.")
run_group.add_argument("args", nargs="*",
help="Arguments to pass to component.")
main(parser.parse_args())
diff --git a/Options.py b/Options.py
index ecde6275f1..b79714635d 100644
--- a/Options.py
+++ b/Options.py
@@ -973,7 +973,19 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
if random.random() < float(text.get("percentage", 100)/100):
at = text.get("at", None)
if at is not None:
+ if isinstance(at, dict):
+ if at:
+ at = random.choices(list(at.keys()),
+ weights=list(at.values()), k=1)[0]
+ else:
+ raise OptionError("\"at\" must be a valid string or weighted list of strings!")
given_text = text.get("text", [])
+ if isinstance(given_text, dict):
+ if not given_text:
+ given_text = []
+ else:
+ given_text = random.choices(list(given_text.keys()),
+ weights=list(given_text.values()), k=1)
if isinstance(given_text, str):
given_text = [given_text]
texts.append(PlandoText(
@@ -981,6 +993,8 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
given_text,
text.get("percentage", 100)
))
+ else:
+ raise OptionError("\"at\" must be a valid string or weighted list of strings!")
elif isinstance(text, PlandoText):
if random.random() < float(text.percentage/100):
texts.append(text)
diff --git a/WebHostLib/static/assets/faq/faq_en.md b/WebHostLib/static/assets/faq/faq_en.md
index fb1ccd2d6f..e64535b42d 100644
--- a/WebHostLib/static/assets/faq/faq_en.md
+++ b/WebHostLib/static/assets/faq/faq_en.md
@@ -77,4 +77,4 @@ There, you will find examples of games in the `worlds` folder:
You may also find developer documentation in the `docs` folder:
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
-If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.
+If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord.
diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html
index 7bbb894de0..6b2a4b0ed7 100644
--- a/WebHostLib/templates/macros.html
+++ b/WebHostLib/templates/macros.html
@@ -22,7 +22,7 @@
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
| {{ patch.player_id }} |
- {{ patch.player_name }} |
+ {{ patch.player_name }} |
{{ patch.game }} |
{% if patch.data %}
diff --git a/WebHostLib/templates/userContent.html b/WebHostLib/templates/userContent.html
index 3603d4112d..71a0f6747b 100644
--- a/WebHostLib/templates/userContent.html
+++ b/WebHostLib/templates/userContent.html
@@ -69,7 +69,7 @@
{% else %}
- You have no generated any seeds yet!
+ You have not generated any seeds yet!
{% endif %}
diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS
index cd1e859af9..28dcc67362 100644
--- a/docs/CODEOWNERS
+++ b/docs/CODEOWNERS
@@ -118,9 +118,6 @@
# Noita
/worlds/noita/ @ScipioWright @heinermann
-# Ocarina of Time
-/worlds/oot/ @espeon65536
-
# Old School Runescape
/worlds/osrs @digiholic
@@ -230,6 +227,9 @@
# Links Awakening DX
# /worlds/ladx/
+# Ocarina of Time
+# /worlds/oot/
+
## Disabled Unmaintained Worlds
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are
diff --git a/docs/options api.md b/docs/options api.md
index 7e479809ee..d48a56d6c7 100644
--- a/docs/options api.md
+++ b/docs/options api.md
@@ -24,7 +24,7 @@ display as `Value1` on the webhost.
files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a
Choice, and defining `alias_true = option_full`.
- All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or
-`(Named/Special)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
+`(Named)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
However, you can override `from_text` and handle `text == "random"` to customize its behavior or
implement it for additional option types.
@@ -129,6 +129,23 @@ class Difficulty(Choice):
default = 1
```
+### Option Visibility
+Every option has a Visibility IntFlag, defaulting to `all` (`0b1111`). This lets you choose where the option will be
+displayed. This only impacts where options are displayed, not how they can be used. Hidden options are still valid
+options in a yaml. The flags are as follows:
+* `none` (`0b0000`): This option is not shown anywhere
+* `template` (`0b0001`): This option shows up in template yamls
+* `simple_ui` (`0b0010`): This option shows up on the options page
+* `complex_ui` (`0b0100`): This option shows up on the advanced/weighted options page
+* `spoiler` (`0b1000`): This option shows up in spoiler logs
+
+```python
+from Options import Choice, Visibility
+
+class HiddenChoiceOption(Choice):
+ visibility = Visibility.none
+```
+
### Option Groups
Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified
by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment
diff --git a/docs/running from source.md b/docs/running from source.md
index 4bd335648d..a161265fcb 100644
--- a/docs/running from source.md
+++ b/docs/running from source.md
@@ -38,7 +38,7 @@ Recommended steps
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details.
Generally, selecting the box for "Desktop Development with C++" will provide what you need.
* Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on
- [Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
+ [Discord in #ap-core-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
diff --git a/docs/world api.md b/docs/world api.md
index 6551f22604..bf09d965f1 100644
--- a/docs/world api.md
+++ b/docs/world api.md
@@ -303,6 +303,31 @@ generation (entrance randomization).
An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state`
(items that have been collected).
+The two possible ways to make a [CollectionRule](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10) are:
+- `def rule(state: CollectionState) -> bool:`
+- `lambda state: ... boolean expression ...`
+
+An access rule can be assigned through `set_rule(location, rule)`.
+
+Access rules usually check for one of two things.
+- Items that have been collected (e.g. `state.has("Sword", player)`)
+- Locations, Regions or Entrances that have been reached (e.g. `state.can_reach_region("Boss Room")`)
+
+Keep in mind that entrances and locations implicitly check for the accessibility of their parent region, so you do not need to check explicitly for it.
+
+#### An important note on Entrance access rules:
+When using `state.can_reach` within an entrance access condition, you must also use `multiworld.register_indirect_condition`.
+
+For efficiency reasons, every time reachable regions are searched, every entrance is only checked once in a somewhat non-deterministic order.
+This is fine when checking for items using `state.has`, because items do not change during a region sweep.
+However, `state.can_reach` checks for the very same thing we are updating: Regions.
+This can lead to non-deterministic behavior and, in the worst case, even generation failures.
+Even doing `state.can_reach_location` or `state.can_reach_entrance` is problematic, as these functions call `state.can_reach_region` on the respective parent region.
+
+**Therefore, it is considered unsafe to perform `state.can_reach` from within an access condition for an entrance**, unless you are checking for something that sits in the source region of the entrance.
+You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
+You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
+
### Item Rules
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
@@ -630,7 +655,7 @@ def set_rules(self) -> None:
Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or
Entrance should be
-a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9).
+a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10).
Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other
functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly.
For an example, see [The Messenger](/worlds/messenger/rules.py).
diff --git a/docs/world maintainer.md b/docs/world maintainer.md
index 15fa46a1ef..17aacdf8c2 100644
--- a/docs/world maintainer.md
+++ b/docs/world maintainer.md
@@ -26,8 +26,17 @@ Unless these are shared between multiple people, we expect the following from ea
### Adding a World
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
-nominate someone else (i.e. there are multiple devs). You can define who is allowed to approve changes to your world
-in the [CODEOWNERS](/docs/CODEOWNERS) document.
+nominate someone else (i.e. there are multiple devs).
+
+### Being added as a maintainer to an existing implementation
+
+At any point, a world maintainer can approve the addition of another maintainer to their world.
+In order to do this, either an existing maintainer or the new maintainer must open a PR updating the
+[CODEOWNERS](/docs/CODEOWNERS) file.
+This change must be approved by all existing maintainers of the affected world, the new maintainer candidate, and
+one core maintainer.
+To help the core team review the change, information about the new maintainer and their contributions should be
+included in the PR description.
### Getting Voted
@@ -35,7 +44,7 @@ When a world is unmaintained, the [core maintainers](https://github.com/orgs/Arc
can vote for a new maintainer if there is a candidate.
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
The time limit is 1 week, but can end early if the majority is reached earlier.
-Voting shall be conducted on Discord in #archipelago-dev.
+Voting shall be conducted on Discord in #ap-core-dev.
## Dropping out
@@ -51,7 +60,7 @@ for example when they become unreachable.
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and
made their case or was pinged and has been unreachable for more than 2 weeks already.
-Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include
+Voting shall be conducted on Discord in #ap-core-dev. Commits that are a direct result of the voting shall include
date, voting members and final result in the commit message.
## Handling of Unmaintained Worlds
diff --git a/inno_setup.iss b/inno_setup.iss
index 3bb76fc40a..38e655d917 100644
--- a/inno_setup.iss
+++ b/inno_setup.iss
@@ -228,8 +228,8 @@ Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{a
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
-Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0";
-Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1""";
+Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoLauncher.exe,0";
+Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1""";
[Code]
// See: https://stackoverflow.com/a/51614652/2287576
diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py
index af067e5cb8..19ec9a14a8 100644
--- a/worlds/AutoWorld.py
+++ b/worlds/AutoWorld.py
@@ -292,6 +292,14 @@ class World(metaclass=AutoWorldRegister):
web: ClassVar[WebWorld] = WebWorld()
"""see WebWorld for options"""
+ origin_region_name: str = "Menu"
+ """Name of the Region from which accessibility is tested."""
+
+ explicit_indirect_conditions: bool = True
+ """If True, the world implementation is supposed to use MultiWorld.register_indirect_condition() correctly.
+ If False, everything is rechecked at every step, which is slower computationally,
+ but may be desirable in complex/dynamic worlds."""
+
multiworld: "MultiWorld"
"""autoset on creation. The MultiWorld object for the currently generating multiworld."""
player: int
diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py
index d127bbea36..fe6e44bb30 100644
--- a/worlds/LauncherComponents.py
+++ b/worlds/LauncherComponents.py
@@ -26,10 +26,13 @@ class Component:
cli: bool
func: Optional[Callable]
file_identifier: Optional[Callable[[str], bool]]
+ game_name: Optional[str]
+ supports_uri: Optional[bool]
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
- func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None):
+ func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None,
+ game_name: Optional[str] = None, supports_uri: Optional[bool] = False):
self.display_name = display_name
self.script_name = script_name
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
@@ -45,6 +48,8 @@ class Component:
Type.ADJUSTER if "Adjuster" in display_name else Type.MISC)
self.func = func
self.file_identifier = file_identifier
+ self.game_name = game_name
+ self.supports_uri = supports_uri
def handles_file(self, path: str):
return self.file_identifier(path) if self.file_identifier else False
@@ -56,10 +61,10 @@ class Component:
processes = weakref.WeakSet()
-def launch_subprocess(func: Callable, name: str = None):
+def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None:
global processes
import multiprocessing
- process = multiprocessing.Process(target=func, name=name)
+ process = multiprocessing.Process(target=func, name=name, args=args)
process.start()
processes.add(process)
@@ -78,9 +83,9 @@ class SuffixIdentifier:
return False
-def launch_textclient():
+def launch_textclient(*args):
import CommonClient
- launch_subprocess(CommonClient.run_as_textclient, name="TextClient")
+ launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args)
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py
index 20dd18038a..bd87cbf2c3 100644
--- a/worlds/alttp/Options.py
+++ b/worlds/alttp/Options.py
@@ -728,7 +728,7 @@ class ALttPPlandoConnections(PlandoConnections):
entrances = set([connection[0] for connection in (
*default_connections, *default_dungeon_connections, *inverted_default_connections,
*inverted_default_dungeon_connections)])
- exits = set([connection[1] for connection in (
+ exits = set([connection[0] for connection in (
*default_connections, *default_dungeon_connections, *inverted_default_connections,
*inverted_default_dungeon_connections)])
diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py
index 1ea2f6e4c9..753c567286 100644
--- a/worlds/factorio/__init__.py
+++ b/worlds/factorio/__init__.py
@@ -101,6 +101,7 @@ class Factorio(World):
tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]]
tech_mix: int = 0
skip_silo: bool = False
+ origin_region_name = "Nauvis"
science_locations: typing.List[FactorioScienceLocation]
settings: typing.ClassVar[FactorioSettings]
@@ -125,9 +126,6 @@ class Factorio(World):
def create_regions(self):
player = self.player
random = self.multiworld.random
- menu = Region("Menu", player, self.multiworld)
- crash = Entrance(player, "Crash Land", menu)
- menu.exits.append(crash)
nauvis = Region("Nauvis", player, self.multiworld)
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
@@ -184,8 +182,7 @@ class Factorio(World):
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
location.place_locked_item(event)
- crash.connect(nauvis)
- self.multiworld.regions += [menu, nauvis]
+ self.multiworld.regions.append(nauvis)
def create_items(self) -> None:
player = self.player
diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py
index cbb9096061..860243ee95 100644
--- a/worlds/hk/__init__.py
+++ b/worlds/hk/__init__.py
@@ -601,11 +601,11 @@ class HKWorld(World):
if change:
for effect_name, effect_value in item_effects.get(item.name, {}).items():
state.prog_items[item.player][effect_name] += effect_value
- if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
- if state.prog_items[item.player].get('RIGHTDASH', 0) and \
- state.prog_items[item.player].get('LEFTDASH', 0):
- (state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
- ([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
+ if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
+ if state.prog_items[item.player].get('RIGHTDASH', 0) and \
+ state.prog_items[item.player].get('LEFTDASH', 0):
+ (state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
+ ([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
return change
def remove(self, state, item: HKItem) -> bool:
diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py
index a03c33c2f7..9a38953ffb 100644
--- a/worlds/messenger/__init__.py
+++ b/worlds/messenger/__init__.py
@@ -19,7 +19,7 @@ from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shu
from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
components.append(
- Component("The Messenger", component_type=Type.CLIENT, func=launch_game)#, game_name="The Messenger", supports_uri=True)
+ Component("The Messenger", component_type=Type.CLIENT, func=launch_game, game_name="The Messenger", supports_uri=True)
)
@@ -27,6 +27,7 @@ class MessengerSettings(Group):
class GamePath(FilePath):
description = "The Messenger game executable"
is_exe = True
+ md5s = ["1b53534569060bc06179356cd968ed1d"]
game_path: GamePath = GamePath("TheMessenger.exe")
diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py
index 9fd08e52d8..77a0f63432 100644
--- a/worlds/messenger/client_setup.py
+++ b/worlds/messenger/client_setup.py
@@ -1,10 +1,10 @@
+import argparse
import io
import logging
import os.path
import subprocess
import urllib.request
from shutil import which
-from tkinter.messagebox import askyesnocancel
from typing import Any, Optional
from zipfile import ZipFile
from Utils import open_file
@@ -17,11 +17,33 @@ from Utils import is_windows, messagebox, tuplize_version
MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest"
-def launch_game(url: Optional[str] = None) -> None:
+def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]:
+ """
+ Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons.
+
+ :param title: Title to be displayed at the top of the message box.
+ :param text: Text to be displayed inside the message box.
+ :return: Returns True if yes, False if no, None if cancel.
+ """
+ from tkinter import Tk, messagebox
+ root = Tk()
+ root.withdraw()
+ ret = messagebox.askyesnocancel(title, text)
+ root.update()
+ return ret
+
+
+
+def launch_game(*args) -> None:
"""Check the game installation, then launch it"""
def courier_installed() -> bool:
"""Check if Courier is installed"""
- return os.path.exists(os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.Courier.mm.dll"))
+ assembly_path = os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.dll")
+ with open(assembly_path, "rb") as assembly:
+ for line in assembly:
+ if b"Courier" in line:
+ return True
+ return False
def mod_installed() -> bool:
"""Check if the mod is installed"""
@@ -56,27 +78,34 @@ def launch_game(url: Optional[str] = None) -> None:
if not is_windows:
mono_exe = which("mono")
if not mono_exe:
- # steam deck support but doesn't currently work
- messagebox("Failure", "Failed to install Courier", True)
- raise RuntimeError("Failed to install Courier")
- # # download and use mono kickstart
- # # this allows steam deck support
- # mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/refs/heads/master.zip"
- # target = os.path.join(folder, "monoKickstart")
- # os.makedirs(target, exist_ok=True)
- # with urllib.request.urlopen(mono_kick_url) as download:
- # with ZipFile(io.BytesIO(download.read()), "r") as zf:
- # for member in zf.infolist():
- # zf.extract(member, path=target)
- # installer = subprocess.Popen([os.path.join(target, "precompiled"),
- # os.path.join(folder, "MiniInstaller.exe")], shell=False)
- # os.remove(target)
+ # download and use mono kickstart
+ # this allows steam deck support
+ mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/716f0a2bd5d75138969090494a76328f39a6dd78.zip"
+ files = []
+ with urllib.request.urlopen(mono_kick_url) as download:
+ with ZipFile(io.BytesIO(download.read()), "r") as zf:
+ for member in zf.infolist():
+ if "precompiled/" not in member.filename or member.filename.endswith("/"):
+ continue
+ member.filename = member.filename.split("/")[-1]
+ if member.filename.endswith("bin.x86_64"):
+ member.filename = "MiniInstaller.bin.x86_64"
+ zf.extract(member, path=game_folder)
+ files.append(member.filename)
+ mono_installer = os.path.join(game_folder, "MiniInstaller.bin.x86_64")
+ os.chmod(mono_installer, 0o755)
+ installer = subprocess.Popen(mono_installer, shell=False)
+ failure = installer.wait()
+ for file in files:
+ os.remove(file)
else:
- installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=False)
+ installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=True)
+ failure = installer.wait()
else:
- installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=False)
+ installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=True)
+ failure = installer.wait()
- failure = installer.wait()
+ print(failure)
if failure:
messagebox("Failure", "Failed to install Courier", True)
os.chdir(working_directory)
@@ -124,18 +153,35 @@ def launch_game(url: Optional[str] = None) -> None:
return "alpha" in latest_version or tuplize_version(latest_version) > tuplize_version(installed_version)
from . import MessengerWorld
- game_folder = os.path.dirname(MessengerWorld.settings.game_path)
+ try:
+ game_folder = os.path.dirname(MessengerWorld.settings.game_path)
+ except ValueError as e:
+ logging.error(e)
+ messagebox("Invalid File", "Selected file did not match expected hash. "
+ "Please try again and ensure you select The Messenger.exe.")
+ return
working_directory = os.getcwd()
+ # setup ssl context
+ try:
+ import certifi
+ import ssl
+ context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
+ context.set_alpn_protocols(["http/1.1"])
+ https_handler = urllib.request.HTTPSHandler(context=context)
+ opener = urllib.request.build_opener(https_handler)
+ urllib.request.install_opener(opener)
+ except ImportError:
+ pass
if not courier_installed():
- should_install = askyesnocancel("Install Courier",
- "No Courier installation detected. Would you like to install now?")
+ should_install = ask_yes_no_cancel("Install Courier",
+ "No Courier installation detected. Would you like to install now?")
if not should_install:
return
logging.info("Installing Courier")
install_courier()
if not mod_installed():
- should_install = askyesnocancel("Install Mod",
- "No randomizer mod detected. Would you like to install now?")
+ should_install = ask_yes_no_cancel("Install Mod",
+ "No randomizer mod detected. Would you like to install now?")
if not should_install:
return
logging.info("Installing Mod")
@@ -143,22 +189,33 @@ def launch_game(url: Optional[str] = None) -> None:
else:
latest = request_data(MOD_URL)["tag_name"]
if available_mod_update(latest):
- should_update = askyesnocancel("Update Mod",
- f"New mod version detected. Would you like to update to {latest} now?")
+ should_update = ask_yes_no_cancel("Update Mod",
+ f"New mod version detected. Would you like to update to {latest} now?")
if should_update:
logging.info("Updating mod")
install_mod()
elif should_update is None:
return
+
+ if not args:
+ should_launch = ask_yes_no_cancel("Launch Game",
+ "Mod installed and up to date. Would you like to launch the game now?")
+ if not should_launch:
+ return
+
+ parser = argparse.ArgumentParser(description="Messenger Client Launcher")
+ parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.")
+ args = parser.parse_args(args)
+
if not is_windows:
- if url:
- open_file(f"steam://rungameid/764790//{url}/")
+ if args.url:
+ open_file(f"steam://rungameid/764790//{args.url}/")
else:
open_file("steam://rungameid/764790")
else:
os.chdir(game_folder)
- if url:
- subprocess.Popen([MessengerWorld.settings.game_path, str(url)])
+ if args.url:
+ subprocess.Popen([MessengerWorld.settings.game_path, str(args.url)])
else:
subprocess.Popen(MessengerWorld.settings.game_path)
os.chdir(working_directory)
diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py
index d89ab5febb..432d593873 100644
--- a/worlds/pokemon_emerald/data.py
+++ b/worlds/pokemon_emerald/data.py
@@ -276,15 +276,13 @@ def _str_to_pokemon_data_type(string: str) -> TrainerPokemonDataTypeEnum:
return TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES
-@dataclass
-class TrainerPokemonData:
+class TrainerPokemonData(NamedTuple):
species_id: int
level: int
moves: Optional[Tuple[int, int, int, int]]
-@dataclass
-class TrainerPartyData:
+class TrainerPartyData(NamedTuple):
pokemon: List[TrainerPokemonData]
pokemon_data_type: TrainerPokemonDataTypeEnum
address: int
diff --git a/worlds/pokemon_emerald/opponents.py b/worlds/pokemon_emerald/opponents.py
index 09e947546d..966d192054 100644
--- a/worlds/pokemon_emerald/opponents.py
+++ b/worlds/pokemon_emerald/opponents.py
@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING, Dict, List, Set
-from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, TrainerPokemonData, data
+from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, data
from .options import RandomizeTrainerParties
from .pokemon import filter_species_by_nearby_bst
from .util import int_to_bool_array
@@ -111,6 +111,6 @@ def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None:
hm_moves[3] if world.random.random() < 0.25 else level_up_moves[3]
)
- new_party.append(TrainerPokemonData(new_species.species_id, pokemon.level, new_moves))
+ new_party.append(pokemon._replace(species_id=new_species.species_id, moves=new_moves))
- trainer.party.pokemon = new_party
+ trainer.party = trainer.party._replace(pokemon=new_party)
diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py
index c60e5e9d4f..fec1101dab 100644
--- a/worlds/pokemon_emerald/pokemon.py
+++ b/worlds/pokemon_emerald/pokemon.py
@@ -4,8 +4,7 @@ Functions related to pokemon species and moves
import functools
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple
-from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, MiscPokemonData,
- SpeciesData, data)
+from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, SpeciesData, data)
from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters,
RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon,
TmTutorCompatibility)
@@ -461,7 +460,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None:
type_bias, normal_bias, species.types)
else:
new_move = 0
- new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move))
+ new_learnset.append(old_learnset[cursor]._replace(move_id=new_move))
cursor += 1
# All moves from here onward are actual moves.
@@ -473,7 +472,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None:
new_move = get_random_move(world.random,
{move.move_id for move in new_learnset} | world.blacklisted_moves,
type_bias, normal_bias, species.types)
- new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move))
+ new_learnset.append(old_learnset[cursor]._replace(move_id=new_move))
cursor += 1
species.learnset = new_learnset
@@ -581,8 +580,10 @@ def randomize_starters(world: "PokemonEmeraldWorld") -> None:
picked_evolution = world.random.choice(potential_evolutions)
for trainer_name, starter_position, is_evolved in rival_teams[i]:
+ new_species_id = picked_evolution if is_evolved else starter.species_id
trainer_data = world.modified_trainers[data.constants[trainer_name]]
- trainer_data.party.pokemon[starter_position].species_id = picked_evolution if is_evolved else starter.species_id
+ trainer_data.party.pokemon[starter_position] = \
+ trainer_data.party.pokemon[starter_position]._replace(species_id=new_species_id)
def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None:
@@ -594,10 +595,7 @@ def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None:
world.random.shuffle(shuffled_species)
for i, encounter in enumerate(data.legendary_encounters):
- world.modified_legendary_encounters.append(MiscPokemonData(
- shuffled_species[i],
- encounter.address
- ))
+ world.modified_legendary_encounters.append(encounter._replace(species_id=shuffled_species[i]))
else:
should_match_bst = world.options.legendary_encounters in {
RandomizeLegendaryEncounters.option_match_base_stats,
@@ -621,9 +619,8 @@ def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None:
if should_match_bst:
candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
- world.modified_legendary_encounters.append(MiscPokemonData(
- world.random.choice(candidates).species_id,
- encounter.address
+ world.modified_legendary_encounters.append(encounter._replace(
+ species_id=world.random.choice(candidates).species_id
))
@@ -637,10 +634,7 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None:
world.modified_misc_pokemon = []
for i, encounter in enumerate(data.misc_pokemon):
- world.modified_misc_pokemon.append(MiscPokemonData(
- shuffled_species[i],
- encounter.address
- ))
+ world.modified_misc_pokemon.append(encounter._replace(species_id=shuffled_species[i]))
else:
should_match_bst = world.options.misc_pokemon in {
RandomizeMiscPokemon.option_match_base_stats,
@@ -672,9 +666,8 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None:
if len(player_filtered_candidates) > 0:
candidates = player_filtered_candidates
- world.modified_misc_pokemon.append(MiscPokemonData(
- world.random.choice(candidates).species_id,
- encounter.address
+ world.modified_misc_pokemon.append(encounter._replace(
+ species_id=world.random.choice(candidates).species_id
))
diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py
index 5b2aaa1ffc..5f83686ebe 100644
--- a/worlds/pokemon_emerald/rules.py
+++ b/worlds/pokemon_emerald/rules.py
@@ -19,20 +19,20 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
hm_rules: Dict[str, Callable[[CollectionState], bool]] = {}
for hm, badges in world.hm_requirements.items():
if isinstance(badges, list):
- hm_rules[hm] = lambda state, hm=hm, badges=badges: state.has(hm, world.player) \
- and state.has_all(badges, world.player)
+ hm_rules[hm] = lambda state, hm=hm, badges=badges: \
+ state.has(hm, world.player) and state.has_all(badges, world.player)
else:
- hm_rules[hm] = lambda state, hm=hm, badges=badges: state.has(hm, world.player) \
- and state.has_group("Badges", world.player, badges)
+ hm_rules[hm] = lambda state, hm=hm, badges=badges: \
+ state.has(hm, world.player) and state.has_group_unique("Badges", world.player, badges)
def has_acro_bike(state: CollectionState):
return state.has("Acro Bike", world.player)
def has_mach_bike(state: CollectionState):
return state.has("Mach Bike", world.player)
-
+
def defeated_n_gym_leaders(state: CollectionState, n: int) -> bool:
- return sum([state.has(event, world.player) for event in [
+ return state.has_from_list_unique([
"EVENT_DEFEAT_ROXANNE",
"EVENT_DEFEAT_BRAWLY",
"EVENT_DEFEAT_WATTSON",
@@ -41,7 +41,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
"EVENT_DEFEAT_WINONA",
"EVENT_DEFEAT_TATE_AND_LIZA",
"EVENT_DEFEAT_JUAN",
- ]]) >= n
+ ], world.player, n)
huntable_legendary_events = [
f"EVENT_ENCOUNTER_{key}"
@@ -61,8 +61,9 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
}.items()
if name in world.options.allowed_legendary_hunt_encounters.value
]
+
def encountered_n_legendaries(state: CollectionState, n: int) -> bool:
- return sum(int(state.has(event, world.player)) for event in huntable_legendary_events) >= n
+ return state.has_from_list_unique(huntable_legendary_events, world.player, n)
def get_entrance(entrance: str):
return world.multiworld.get_entrance(entrance, world.player)
@@ -235,11 +236,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
if world.options.norman_requirement == NormanRequirement.option_badges:
set_rule(
get_entrance("MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"),
- lambda state: state.has_group("Badges", world.player, world.options.norman_count.value)
+ lambda state: state.has_group_unique("Badges", world.player, world.options.norman_count.value)
)
set_rule(
get_entrance("MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"),
- lambda state: state.has_group("Badges", world.player, world.options.norman_count.value)
+ lambda state: state.has_group_unique("Badges", world.player, world.options.norman_count.value)
)
else:
set_rule(
@@ -299,15 +300,15 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
)
set_rule(
get_entrance("REGION_ROUTE116/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("TERRA_CAVE_ROUTE_116_1", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("TERRA_CAVE_ROUTE_116_1", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
set_rule(
get_entrance("REGION_ROUTE116/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("TERRA_CAVE_ROUTE_116_2", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("TERRA_CAVE_ROUTE_116_2", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
# Rusturf Tunnel
@@ -347,19 +348,19 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
)
set_rule(
get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/NORTH_ABOVE_SLOPE"),
- lambda state: has_mach_bike(state)
+ has_mach_bike
)
set_rule(
get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("TERRA_CAVE_ROUTE_115_1", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("TERRA_CAVE_ROUTE_115_1", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
set_rule(
get_entrance("REGION_ROUTE115/NORTH_ABOVE_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("TERRA_CAVE_ROUTE_115_2", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("TERRA_CAVE_ROUTE_115_2", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
if world.options.extra_boulders:
@@ -375,7 +376,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
if world.options.extra_bumpy_slope:
set_rule(
get_entrance("REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"),
- lambda state: has_acro_bike(state)
+ has_acro_bike
)
else:
set_rule(
@@ -386,17 +387,17 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
# Route 105
set_rule(
get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state) and \
- state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("MARINE_CAVE_ROUTE_105_1", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: hm_rules["HM08 Dive"](state)
+ and state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("MARINE_CAVE_ROUTE_105_1", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
set_rule(
get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state) and \
- state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("MARINE_CAVE_ROUTE_105_2", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: hm_rules["HM08 Dive"](state)
+ and state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("MARINE_CAVE_ROUTE_105_2", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
set_rule(
get_entrance("MAP_ROUTE105:0/MAP_ISLAND_CAVE:0"),
@@ -439,7 +440,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
)
set_rule(
get_entrance("REGION_GRANITE_CAVE_B1F/LOWER -> REGION_GRANITE_CAVE_B1F/UPPER"),
- lambda state: has_mach_bike(state)
+ has_mach_bike
)
# Route 107
@@ -643,15 +644,15 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
)
set_rule(
get_entrance("REGION_ROUTE114/ABOVE_WATERFALL -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("TERRA_CAVE_ROUTE_114_1", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("TERRA_CAVE_ROUTE_114_1", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
set_rule(
get_entrance("REGION_ROUTE114/MAIN -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("TERRA_CAVE_ROUTE_114_2", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("TERRA_CAVE_ROUTE_114_2", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
# Meteor Falls
@@ -699,11 +700,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
# Jagged Pass
set_rule(
get_entrance("REGION_JAGGED_PASS/BOTTOM -> REGION_JAGGED_PASS/MIDDLE"),
- lambda state: has_acro_bike(state)
+ has_acro_bike
)
set_rule(
get_entrance("REGION_JAGGED_PASS/MIDDLE -> REGION_JAGGED_PASS/TOP"),
- lambda state: has_acro_bike(state)
+ has_acro_bike
)
set_rule(
get_entrance("MAP_JAGGED_PASS:4/MAP_MAGMA_HIDEOUT_1F:0"),
@@ -719,11 +720,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
# Mirage Tower
set_rule(
get_entrance("REGION_MIRAGE_TOWER_2F/TOP -> REGION_MIRAGE_TOWER_2F/BOTTOM"),
- lambda state: has_mach_bike(state)
+ has_mach_bike
)
set_rule(
get_entrance("REGION_MIRAGE_TOWER_2F/BOTTOM -> REGION_MIRAGE_TOWER_2F/TOP"),
- lambda state: has_mach_bike(state)
+ has_mach_bike
)
set_rule(
get_entrance("REGION_MIRAGE_TOWER_3F/TOP -> REGION_MIRAGE_TOWER_3F/BOTTOM"),
@@ -812,15 +813,15 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
)
set_rule(
get_entrance("REGION_ROUTE118/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("TERRA_CAVE_ROUTE_118_1", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("TERRA_CAVE_ROUTE_118_1", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
set_rule(
get_entrance("REGION_ROUTE118/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
- lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("TERRA_CAVE_ROUTE_118_2", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("TERRA_CAVE_ROUTE_118_2", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
# Route 119
@@ -830,11 +831,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
)
set_rule(
get_entrance("REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_ACROSS_RAILS"),
- lambda state: has_acro_bike(state)
+ has_acro_bike
)
set_rule(
get_entrance("REGION_ROUTE119/LOWER_ACROSS_RAILS -> REGION_ROUTE119/LOWER"),
- lambda state: has_acro_bike(state)
+ has_acro_bike
)
set_rule(
get_entrance("REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE_RIVER"),
@@ -850,7 +851,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
)
set_rule(
get_entrance("REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/ABOVE_WATERFALL_ACROSS_RAILS"),
- lambda state: has_acro_bike(state)
+ has_acro_bike
)
if "Route 119 Aqua Grunts" not in world.options.remove_roadblocks.value:
set_rule(
@@ -927,11 +928,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
)
set_rule(
get_entrance("REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_NORTH/MAIN"),
- lambda state: has_acro_bike(state)
+ has_acro_bike
)
set_rule(
get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/MAIN"),
- lambda state: has_mach_bike(state)
+ has_mach_bike
)
set_rule(
get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_SOUTHWEST/POND"),
@@ -1115,17 +1116,17 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
# Route 125
set_rule(
get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state) and \
- state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("MARINE_CAVE_ROUTE_125_1", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: hm_rules["HM08 Dive"](state)
+ and state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("MARINE_CAVE_ROUTE_125_1", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
set_rule(
get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state) and \
- state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("MARINE_CAVE_ROUTE_125_2", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: hm_rules["HM08 Dive"](state)
+ and state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("MARINE_CAVE_ROUTE_125_2", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
# Shoal Cave
@@ -1257,17 +1258,17 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
)
set_rule(
get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state) and \
- state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("MARINE_CAVE_ROUTE_127_1", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: hm_rules["HM08 Dive"](state)
+ and state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("MARINE_CAVE_ROUTE_127_1", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
set_rule(
get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state) and \
- state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("MARINE_CAVE_ROUTE_127_2", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: hm_rules["HM08 Dive"](state)
+ and state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("MARINE_CAVE_ROUTE_127_2", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
# Route 128
@@ -1374,17 +1375,17 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
# Route 129
set_rule(
get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state) and \
- state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("MARINE_CAVE_ROUTE_129_1", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: hm_rules["HM08 Dive"](state)
+ and state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("MARINE_CAVE_ROUTE_129_1", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
set_rule(
get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
- lambda state: hm_rules["HM08 Dive"](state) and \
- state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
- state.has("MARINE_CAVE_ROUTE_129_2", world.player) and \
- state.has("EVENT_DEFEAT_SHELLY", world.player)
+ lambda state: hm_rules["HM08 Dive"](state)
+ and state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ and state.has("MARINE_CAVE_ROUTE_129_2", world.player)
+ and state.has("EVENT_DEFEAT_SHELLY", world.player)
)
# Pacifidlog Town
@@ -1505,7 +1506,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
if world.options.elite_four_requirement == EliteFourRequirement.option_badges:
set_rule(
get_entrance("REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"),
- lambda state: state.has_group("Badges", world.player, world.options.elite_four_count.value)
+ lambda state: state.has_group_unique("Badges", world.player, world.options.elite_four_count.value)
)
else:
set_rule(
diff --git a/worlds/rogue_legacy/Options.py b/worlds/rogue_legacy/Options.py
index d8298c85c8..139ff60944 100644
--- a/worlds/rogue_legacy/Options.py
+++ b/worlds/rogue_legacy/Options.py
@@ -1,6 +1,6 @@
-from typing import Dict
+from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionSet, PerGameCommonOptions
-from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionSet
+from dataclasses import dataclass
class StartingGender(Choice):
@@ -175,13 +175,21 @@ class NumberOfChildren(Range):
default = 3
-class AdditionalNames(OptionSet):
+class AdditionalLadyNames(OptionSet):
"""
Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list
of names your children can have. The first value will also be your initial character's name depending on Starting
Gender.
"""
- display_name = "Additional Names"
+ display_name = "Additional Lady Names"
+
+class AdditionalSirNames(OptionSet):
+ """
+ Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list
+ of names your children can have. The first value will also be your initial character's name depending on Starting
+ Gender.
+ """
+ display_name = "Additional Sir Names"
class AllowDefaultNames(DefaultOnToggle):
@@ -336,42 +344,44 @@ class AvailableClasses(OptionSet):
The upgraded form of your starting class will be available regardless.
"""
display_name = "Available Classes"
- default = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"}
+ default = frozenset(
+ {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"}
+ )
valid_keys = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"}
-rl_options: Dict[str, type(Option)] = {
- "starting_gender": StartingGender,
- "starting_class": StartingClass,
- "available_classes": AvailableClasses,
- "new_game_plus": NewGamePlus,
- "fairy_chests_per_zone": FairyChestsPerZone,
- "chests_per_zone": ChestsPerZone,
- "universal_fairy_chests": UniversalFairyChests,
- "universal_chests": UniversalChests,
- "vendors": Vendors,
- "architect": Architect,
- "architect_fee": ArchitectFee,
- "disable_charon": DisableCharon,
- "require_purchasing": RequirePurchasing,
- "progressive_blueprints": ProgressiveBlueprints,
- "gold_gain_multiplier": GoldGainMultiplier,
- "number_of_children": NumberOfChildren,
- "free_diary_on_generation": FreeDiaryOnGeneration,
- "khidr": ChallengeBossKhidr,
- "alexander": ChallengeBossAlexander,
- "leon": ChallengeBossLeon,
- "herodotus": ChallengeBossHerodotus,
- "health_pool": HealthUpPool,
- "mana_pool": ManaUpPool,
- "attack_pool": AttackUpPool,
- "magic_damage_pool": MagicDamageUpPool,
- "armor_pool": ArmorUpPool,
- "equip_pool": EquipUpPool,
- "crit_chance_pool": CritChanceUpPool,
- "crit_damage_pool": CritDamageUpPool,
- "allow_default_names": AllowDefaultNames,
- "additional_lady_names": AdditionalNames,
- "additional_sir_names": AdditionalNames,
- "death_link": DeathLink,
-}
+@dataclass
+class RLOptions(PerGameCommonOptions):
+ starting_gender: StartingGender
+ starting_class: StartingClass
+ available_classes: AvailableClasses
+ new_game_plus: NewGamePlus
+ fairy_chests_per_zone: FairyChestsPerZone
+ chests_per_zone: ChestsPerZone
+ universal_fairy_chests: UniversalFairyChests
+ universal_chests: UniversalChests
+ vendors: Vendors
+ architect: Architect
+ architect_fee: ArchitectFee
+ disable_charon: DisableCharon
+ require_purchasing: RequirePurchasing
+ progressive_blueprints: ProgressiveBlueprints
+ gold_gain_multiplier: GoldGainMultiplier
+ number_of_children: NumberOfChildren
+ free_diary_on_generation: FreeDiaryOnGeneration
+ khidr: ChallengeBossKhidr
+ alexander: ChallengeBossAlexander
+ leon: ChallengeBossLeon
+ herodotus: ChallengeBossHerodotus
+ health_pool: HealthUpPool
+ mana_pool: ManaUpPool
+ attack_pool: AttackUpPool
+ magic_damage_pool: MagicDamageUpPool
+ armor_pool: ArmorUpPool
+ equip_pool: EquipUpPool
+ crit_chance_pool: CritChanceUpPool
+ crit_damage_pool: CritDamageUpPool
+ allow_default_names: AllowDefaultNames
+ additional_lady_names: AdditionalLadyNames
+ additional_sir_names: AdditionalSirNames
+ death_link: DeathLink
diff --git a/worlds/rogue_legacy/Regions.py b/worlds/rogue_legacy/Regions.py
index 5d07fccbc4..61b0ef73ec 100644
--- a/worlds/rogue_legacy/Regions.py
+++ b/worlds/rogue_legacy/Regions.py
@@ -1,15 +1,18 @@
-from typing import Dict, List, NamedTuple, Optional
+from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
from BaseClasses import MultiWorld, Region, Entrance
from .Locations import RLLocation, location_table, get_locations_by_category
+if TYPE_CHECKING:
+ from . import RLWorld
+
class RLRegionData(NamedTuple):
locations: Optional[List[str]]
region_exits: Optional[List[str]]
-def create_regions(multiworld: MultiWorld, player: int):
+def create_regions(world: "RLWorld"):
regions: Dict[str, RLRegionData] = {
"Menu": RLRegionData(None, ["Castle Hamson"]),
"The Manor": RLRegionData([], []),
@@ -56,9 +59,9 @@ def create_regions(multiworld: MultiWorld, player: int):
regions["The Fountain Room"].locations.append("Fountain Room")
# Chests
- chests = int(multiworld.chests_per_zone[player])
+ chests = int(world.options.chests_per_zone)
for i in range(0, chests):
- if multiworld.universal_chests[player]:
+ if world.options.universal_chests:
regions["Castle Hamson"].locations.append(f"Chest {i + 1}")
regions["Forest Abkhazia"].locations.append(f"Chest {i + 1 + chests}")
regions["The Maya"].locations.append(f"Chest {i + 1 + (chests * 2)}")
@@ -70,9 +73,9 @@ def create_regions(multiworld: MultiWorld, player: int):
regions["Land of Darkness"].locations.append(f"Land of Darkness - Chest {i + 1}")
# Fairy Chests
- chests = int(multiworld.fairy_chests_per_zone[player])
+ chests = int(world.options.fairy_chests_per_zone)
for i in range(0, chests):
- if multiworld.universal_fairy_chests[player]:
+ if world.options.universal_fairy_chests:
regions["Castle Hamson"].locations.append(f"Fairy Chest {i + 1}")
regions["Forest Abkhazia"].locations.append(f"Fairy Chest {i + 1 + chests}")
regions["The Maya"].locations.append(f"Fairy Chest {i + 1 + (chests * 2)}")
@@ -85,14 +88,14 @@ def create_regions(multiworld: MultiWorld, player: int):
# Set up the regions correctly.
for name, data in regions.items():
- multiworld.regions.append(create_region(multiworld, player, name, data))
+ world.multiworld.regions.append(create_region(world.multiworld, world.player, name, data))
- multiworld.get_entrance("Castle Hamson", player).connect(multiworld.get_region("Castle Hamson", player))
- multiworld.get_entrance("The Manor", player).connect(multiworld.get_region("The Manor", player))
- multiworld.get_entrance("Forest Abkhazia", player).connect(multiworld.get_region("Forest Abkhazia", player))
- multiworld.get_entrance("The Maya", player).connect(multiworld.get_region("The Maya", player))
- multiworld.get_entrance("Land of Darkness", player).connect(multiworld.get_region("Land of Darkness", player))
- multiworld.get_entrance("The Fountain Room", player).connect(multiworld.get_region("The Fountain Room", player))
+ world.get_entrance("Castle Hamson").connect(world.get_region("Castle Hamson"))
+ world.get_entrance("The Manor").connect(world.get_region("The Manor"))
+ world.get_entrance("Forest Abkhazia").connect(world.get_region("Forest Abkhazia"))
+ world.get_entrance("The Maya").connect(world.get_region("The Maya"))
+ world.get_entrance("Land of Darkness").connect(world.get_region("Land of Darkness"))
+ world.get_entrance("The Fountain Room").connect(world.get_region("The Fountain Room"))
def create_region(multiworld: MultiWorld, player: int, name: str, data: RLRegionData):
diff --git a/worlds/rogue_legacy/Rules.py b/worlds/rogue_legacy/Rules.py
index 2fac8d5613..505bbdd635 100644
--- a/worlds/rogue_legacy/Rules.py
+++ b/worlds/rogue_legacy/Rules.py
@@ -1,9 +1,13 @@
-from BaseClasses import CollectionState, MultiWorld
+from BaseClasses import CollectionState
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from . import RLWorld
-def get_upgrade_total(multiworld: MultiWorld, player: int) -> int:
- return int(multiworld.health_pool[player]) + int(multiworld.mana_pool[player]) + \
- int(multiworld.attack_pool[player]) + int(multiworld.magic_damage_pool[player])
+def get_upgrade_total(world: "RLWorld") -> int:
+ return int(world.options.health_pool) + int(world.options.mana_pool) + \
+ int(world.options.attack_pool) + int(world.options.magic_damage_pool)
def get_upgrade_count(state: CollectionState, player: int) -> int:
@@ -19,8 +23,8 @@ def has_upgrade_amount(state: CollectionState, player: int, amount: int) -> bool
return get_upgrade_count(state, player) >= amount
-def has_upgrades_percentage(state: CollectionState, player: int, percentage: float) -> bool:
- return has_upgrade_amount(state, player, round(get_upgrade_total(state.multiworld, player) * (percentage / 100)))
+def has_upgrades_percentage(state: CollectionState, world: "RLWorld", percentage: float) -> bool:
+ return has_upgrade_amount(state, world.player, round(get_upgrade_total(world) * (percentage / 100)))
def has_movement_rune(state: CollectionState, player: int) -> bool:
@@ -47,15 +51,15 @@ def has_defeated_dungeon(state: CollectionState, player: int) -> bool:
return state.has("Defeat Herodotus", player) or state.has("Defeat Astrodotus", player)
-def set_rules(multiworld: MultiWorld, player: int):
+def set_rules(world: "RLWorld", player: int):
# If 'vendors' are 'normal', then expect it to show up in the first half(ish) of the spheres.
- if multiworld.vendors[player] == "normal":
- multiworld.get_location("Forest Abkhazia Boss Reward", player).access_rule = \
+ if world.options.vendors == "normal":
+ world.get_location("Forest Abkhazia Boss Reward").access_rule = \
lambda state: has_vendors(state, player)
# Gate each manor location so everything isn't dumped into sphere 1.
manor_rules = {
- "Defeat Khidr" if multiworld.khidr[player] == "vanilla" else "Defeat Neo Khidr": [
+ "Defeat Khidr" if world.options.khidr == "vanilla" else "Defeat Neo Khidr": [
"Manor - Left Wing Window",
"Manor - Left Wing Rooftop",
"Manor - Right Wing Window",
@@ -66,7 +70,7 @@ def set_rules(multiworld: MultiWorld, player: int):
"Manor - Left Tree 2",
"Manor - Right Tree",
],
- "Defeat Alexander" if multiworld.alexander[player] == "vanilla" else "Defeat Alexander IV": [
+ "Defeat Alexander" if world.options.alexander == "vanilla" else "Defeat Alexander IV": [
"Manor - Left Big Upper 1",
"Manor - Left Big Upper 2",
"Manor - Left Big Windows",
@@ -78,7 +82,7 @@ def set_rules(multiworld: MultiWorld, player: int):
"Manor - Right Big Rooftop",
"Manor - Right Extension",
],
- "Defeat Ponce de Leon" if multiworld.leon[player] == "vanilla" else "Defeat Ponce de Freon": [
+ "Defeat Ponce de Leon" if world.options.leon == "vanilla" else "Defeat Ponce de Freon": [
"Manor - Right High Base",
"Manor - Right High Upper",
"Manor - Right High Tower",
@@ -90,24 +94,24 @@ def set_rules(multiworld: MultiWorld, player: int):
# Set rules for manor locations.
for event, locations in manor_rules.items():
for location in locations:
- multiworld.get_location(location, player).access_rule = lambda state: state.has(event, player)
+ world.get_location(location).access_rule = lambda state: state.has(event, player)
# Set rules for fairy chests to decrease headache of expectation to find non-movement fairy chests.
- for fairy_location in [location for location in multiworld.get_locations(player) if "Fairy" in location.name]:
+ for fairy_location in [location for location in world.multiworld.get_locations(player) if "Fairy" in location.name]:
fairy_location.access_rule = lambda state: has_fairy_progression(state, player)
# Region rules.
- multiworld.get_entrance("Forest Abkhazia", player).access_rule = \
- lambda state: has_upgrades_percentage(state, player, 12.5) and has_defeated_castle(state, player)
+ world.get_entrance("Forest Abkhazia").access_rule = \
+ lambda state: has_upgrades_percentage(state, world, 12.5) and has_defeated_castle(state, player)
- multiworld.get_entrance("The Maya", player).access_rule = \
- lambda state: has_upgrades_percentage(state, player, 25) and has_defeated_forest(state, player)
+ world.get_entrance("The Maya").access_rule = \
+ lambda state: has_upgrades_percentage(state, world, 25) and has_defeated_forest(state, player)
- multiworld.get_entrance("Land of Darkness", player).access_rule = \
- lambda state: has_upgrades_percentage(state, player, 37.5) and has_defeated_tower(state, player)
+ world.get_entrance("Land of Darkness").access_rule = \
+ lambda state: has_upgrades_percentage(state, world, 37.5) and has_defeated_tower(state, player)
- multiworld.get_entrance("The Fountain Room", player).access_rule = \
- lambda state: has_upgrades_percentage(state, player, 50) and has_defeated_dungeon(state, player)
+ world.get_entrance("The Fountain Room").access_rule = \
+ lambda state: has_upgrades_percentage(state, world, 50) and has_defeated_dungeon(state, player)
# Win condition.
- multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player)
+ world.multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player)
diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py
index 78e56a794c..290f4a60ac 100644
--- a/worlds/rogue_legacy/__init__.py
+++ b/worlds/rogue_legacy/__init__.py
@@ -4,7 +4,7 @@ from BaseClasses import Tutorial
from worlds.AutoWorld import WebWorld, World
from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table
from .Locations import RLLocation, location_table
-from .Options import rl_options
+from .Options import RLOptions
from .Presets import rl_options_presets
from .Regions import create_regions
from .Rules import set_rules
@@ -33,20 +33,17 @@ class RLWorld(World):
But that's OK, because no one is perfect, and you don't have to be to succeed.
"""
game = "Rogue Legacy"
- option_definitions = rl_options
+ options_dataclass = RLOptions
+ options: RLOptions
topology_present = True
required_client_version = (0, 3, 5)
web = RLWeb()
- item_name_to_id = {name: data.code for name, data in item_table.items()}
- location_name_to_id = {name: data.code for name, data in location_table.items()}
-
- # TODO: Replace calls to this function with "options-dict", once that PR is completed and merged.
- def get_setting(self, name: str):
- return getattr(self.multiworld, name)[self.player]
+ item_name_to_id = {name: data.code for name, data in item_table.items() if data.code is not None}
+ location_name_to_id = {name: data.code for name, data in location_table.items() if data.code is not None}
def fill_slot_data(self) -> dict:
- return {option_name: self.get_setting(option_name).value for option_name in rl_options}
+ return self.options.as_dict(*[name for name in self.options_dataclass.type_hints.keys()])
def generate_early(self):
location_ids_used_per_game = {
@@ -74,18 +71,18 @@ class RLWorld(World):
)
# Check validation of names.
- additional_lady_names = len(self.get_setting("additional_lady_names").value)
- additional_sir_names = len(self.get_setting("additional_sir_names").value)
- if not self.get_setting("allow_default_names"):
- if additional_lady_names < int(self.get_setting("number_of_children")):
+ additional_lady_names = len(self.options.additional_lady_names.value)
+ additional_sir_names = len(self.options.additional_sir_names.value)
+ if not self.options.allow_default_names:
+ if additional_lady_names < int(self.options.number_of_children):
raise Exception(
f"allow_default_names is off, but not enough names are defined in additional_lady_names. "
- f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_lady_names}")
+ f"Expected {int(self.options.number_of_children)}, Got {additional_lady_names}")
- if additional_sir_names < int(self.get_setting("number_of_children")):
+ if additional_sir_names < int(self.options.number_of_children):
raise Exception(
f"allow_default_names is off, but not enough names are defined in additional_sir_names. "
- f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_sir_names}")
+ f"Expected {int(self.options.number_of_children)}, Got {additional_sir_names}")
def create_items(self):
item_pool: List[RLItem] = []
@@ -95,110 +92,110 @@ class RLWorld(World):
# Architect
if name == "Architect":
- if self.get_setting("architect") == "disabled":
+ if self.options.architect == "disabled":
continue
- if self.get_setting("architect") == "start_unlocked":
+ if self.options.architect == "start_unlocked":
self.multiworld.push_precollected(self.create_item(name))
continue
- if self.get_setting("architect") == "early":
+ if self.options.architect == "early":
self.multiworld.local_early_items[self.player]["Architect"] = 1
# Blacksmith and Enchantress
if name == "Blacksmith" or name == "Enchantress":
- if self.get_setting("vendors") == "start_unlocked":
+ if self.options.vendors == "start_unlocked":
self.multiworld.push_precollected(self.create_item(name))
continue
- if self.get_setting("vendors") == "early":
+ if self.options.vendors == "early":
self.multiworld.local_early_items[self.player]["Blacksmith"] = 1
self.multiworld.local_early_items[self.player]["Enchantress"] = 1
# Haggling
- if name == "Haggling" and self.get_setting("disable_charon"):
+ if name == "Haggling" and self.options.disable_charon:
continue
# Blueprints
if data.category == "Blueprints":
# No progressive blueprints if progressive_blueprints are disabled.
- if name == "Progressive Blueprints" and not self.get_setting("progressive_blueprints"):
+ if name == "Progressive Blueprints" and not self.options.progressive_blueprints:
continue
# No distinct blueprints if progressive_blueprints are enabled.
- elif name != "Progressive Blueprints" and self.get_setting("progressive_blueprints"):
+ elif name != "Progressive Blueprints" and self.options.progressive_blueprints:
continue
# Classes
if data.category == "Classes":
if name == "Progressive Knights":
- if "Knight" not in self.get_setting("available_classes"):
+ if "Knight" not in self.options.available_classes:
continue
- if self.get_setting("starting_class") == "knight":
+ if self.options.starting_class == "knight":
quantity = 1
if name == "Progressive Mages":
- if "Mage" not in self.get_setting("available_classes"):
+ if "Mage" not in self.options.available_classes:
continue
- if self.get_setting("starting_class") == "mage":
+ if self.options.starting_class == "mage":
quantity = 1
if name == "Progressive Barbarians":
- if "Barbarian" not in self.get_setting("available_classes"):
+ if "Barbarian" not in self.options.available_classes:
continue
- if self.get_setting("starting_class") == "barbarian":
+ if self.options.starting_class == "barbarian":
quantity = 1
if name == "Progressive Knaves":
- if "Knave" not in self.get_setting("available_classes"):
+ if "Knave" not in self.options.available_classes:
continue
- if self.get_setting("starting_class") == "knave":
+ if self.options.starting_class == "knave":
quantity = 1
if name == "Progressive Miners":
- if "Miner" not in self.get_setting("available_classes"):
+ if "Miner" not in self.options.available_classes:
continue
- if self.get_setting("starting_class") == "miner":
+ if self.options.starting_class == "miner":
quantity = 1
if name == "Progressive Shinobis":
- if "Shinobi" not in self.get_setting("available_classes"):
+ if "Shinobi" not in self.options.available_classes:
continue
- if self.get_setting("starting_class") == "shinobi":
+ if self.options.starting_class == "shinobi":
quantity = 1
if name == "Progressive Liches":
- if "Lich" not in self.get_setting("available_classes"):
+ if "Lich" not in self.options.available_classes:
continue
- if self.get_setting("starting_class") == "lich":
+ if self.options.starting_class == "lich":
quantity = 1
if name == "Progressive Spellthieves":
- if "Spellthief" not in self.get_setting("available_classes"):
+ if "Spellthief" not in self.options.available_classes:
continue
- if self.get_setting("starting_class") == "spellthief":
+ if self.options.starting_class == "spellthief":
quantity = 1
if name == "Dragons":
- if "Dragon" not in self.get_setting("available_classes"):
+ if "Dragon" not in self.options.available_classes:
continue
if name == "Traitors":
- if "Traitor" not in self.get_setting("available_classes"):
+ if "Traitor" not in self.options.available_classes:
continue
# Skills
if name == "Health Up":
- quantity = self.get_setting("health_pool")
+ quantity = self.options.health_pool.value
elif name == "Mana Up":
- quantity = self.get_setting("mana_pool")
+ quantity = self.options.mana_pool.value
elif name == "Attack Up":
- quantity = self.get_setting("attack_pool")
+ quantity = self.options.attack_pool.value
elif name == "Magic Damage Up":
- quantity = self.get_setting("magic_damage_pool")
+ quantity = self.options.magic_damage_pool.value
elif name == "Armor Up":
- quantity = self.get_setting("armor_pool")
+ quantity = self.options.armor_pool.value
elif name == "Equip Up":
- quantity = self.get_setting("equip_pool")
+ quantity = self.options.equip_pool.value
elif name == "Crit Chance Up":
- quantity = self.get_setting("crit_chance_pool")
+ quantity = self.options.crit_chance_pool.value
elif name == "Crit Damage Up":
- quantity = self.get_setting("crit_damage_pool")
+ quantity = self.options.crit_damage_pool.value
# Ignore filler, it will be added in a later stage.
if data.category == "Filler":
@@ -215,7 +212,7 @@ class RLWorld(World):
def get_filler_item_name(self) -> str:
fillers = get_items_by_category("Filler")
weights = [data.weight for data in fillers.values()]
- return self.multiworld.random.choices([filler for filler in fillers.keys()], weights, k=1)[0]
+ return self.random.choices([filler for filler in fillers.keys()], weights, k=1)[0]
def create_item(self, name: str) -> RLItem:
data = item_table[name]
@@ -226,10 +223,10 @@ class RLWorld(World):
return RLItem(name, data.classification, data.code, self.player)
def set_rules(self):
- set_rules(self.multiworld, self.player)
+ set_rules(self, self.player)
def create_regions(self):
- create_regions(self.multiworld, self.player)
+ create_regions(self)
self._place_events()
def _place_events(self):
@@ -238,7 +235,7 @@ class RLWorld(World):
self.create_event("Defeat The Fountain"))
# Khidr / Neo Khidr
- if self.get_setting("khidr") == "vanilla":
+ if self.options.khidr == "vanilla":
self.multiworld.get_location("Castle Hamson Boss Room", self.player).place_locked_item(
self.create_event("Defeat Khidr"))
else:
@@ -246,7 +243,7 @@ class RLWorld(World):
self.create_event("Defeat Neo Khidr"))
# Alexander / Alexander IV
- if self.get_setting("alexander") == "vanilla":
+ if self.options.alexander == "vanilla":
self.multiworld.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item(
self.create_event("Defeat Alexander"))
else:
@@ -254,7 +251,7 @@ class RLWorld(World):
self.create_event("Defeat Alexander IV"))
# Ponce de Leon / Ponce de Freon
- if self.get_setting("leon") == "vanilla":
+ if self.options.leon == "vanilla":
self.multiworld.get_location("The Maya Boss Room", self.player).place_locked_item(
self.create_event("Defeat Ponce de Leon"))
else:
@@ -262,7 +259,7 @@ class RLWorld(World):
self.create_event("Defeat Ponce de Freon"))
# Herodotus / Astrodotus
- if self.get_setting("herodotus") == "vanilla":
+ if self.options.herodotus == "vanilla":
self.multiworld.get_location("Land of Darkness Boss Room", self.player).place_locked_item(
self.create_event("Defeat Herodotus"))
else:
diff --git a/worlds/rogue_legacy/test/__init__.py b/worlds/rogue_legacy/test/__init__.py
index 2639e618c6..3346476ba6 100644
--- a/worlds/rogue_legacy/test/__init__.py
+++ b/worlds/rogue_legacy/test/__init__.py
@@ -1,4 +1,4 @@
-from test.TestBase import WorldTestBase
+from test.bases import WorldTestBase
class RLTestBase(WorldTestBase):
diff --git a/worlds/sc2/docs/en_Starcraft 2.md b/worlds/sc2/docs/en_Starcraft 2.md
index 06464e3cd2..813fdb5f4a 100644
--- a/worlds/sc2/docs/en_Starcraft 2.md
+++ b/worlds/sc2/docs/en_Starcraft 2.md
@@ -1,4 +1,4 @@
-# Starcraft 2
+# StarCraft 2
## Game page in other languages:
* [Français](/games/Starcraft%202/info/fr)
@@ -7,9 +7,11 @@
The following unlocks are randomized as items:
1. Your ability to build any non-worker unit.
-2. Unit specific upgrades including some combinations not available in the vanilla campaigns, such as both strain choices simultaneously for Zerg and every Spear of Adun upgrade simultaneously for Protoss!
+2. Unit specific upgrades including some combinations not available in the vanilla campaigns, such as both strain
+choices simultaneously for Zerg and every Spear of Adun upgrade simultaneously for Protoss!
3. Your ability to get the generic unit upgrades, such as attack and armour upgrades.
-4. Other miscellaneous upgrades such as laboratory upgrades and mercenaries for Terran, Kerrigan levels and upgrades for Zerg, and Spear of Adun upgrades for Protoss.
+4. Other miscellaneous upgrades such as laboratory upgrades and mercenaries for Terran, Kerrigan levels and upgrades
+for Zerg, and Spear of Adun upgrades for Protoss.
5. Small boosts to your starting mineral, vespene gas, and supply totals on each mission.
You find items by making progress in these categories:
@@ -18,50 +20,91 @@ You find items by making progress in these categories:
* Reaching milestones in the mission, such as completing part of a main objective
* Completing challenges based on achievements in the base game, such as clearing all Zerg on Devil's Playground
-Except for mission completion, these categories can be disabled in the game's settings. For instance, you can disable getting items for reaching required milestones.
+In Archipelago's nomenclature, these are the locations where items can be found.
+Each location, including mission completion, has a set of rules that specify the items required to access it.
+These rules were designed assuming that StarCraft 2 is played on the Brutal difficulty.
+Since each location has its own rule, it's possible that an item required for progression is in a mission where you
+can't reach all of its locations or complete it.
+However, mission completion is always required to gain access to new missions.
+
+Aside from mission completion, the other location categories can be disabled in the player options.
+For instance, you can disable getting items for reaching required milestones.
When you receive items, they will immediately become available, even during a mission, and you will be
-notified via a text box in the top-right corner of the game screen. Item unlocks are also logged in the Archipelago client.
+notified via a text box in the top-right corner of the game screen.
+Item unlocks are also logged in the Archipelago client.
-Missions are launched through the Starcraft 2 Archipelago client, through the Starcraft 2 Launcher tab. The between mission segments on the Hyperion, the Leviathan, and the Spear of Adun are not included. Additionally, metaprogression currencies such as credits and Solarite are not used.
+Missions are launched through the StarCraft 2 Archipelago client, through the StarCraft 2 Launcher tab.
+The between mission segments on the Hyperion, the Leviathan, and the Spear of Adun are not included.
+Additionally, metaprogression currencies such as credits and Solarite are not used.
## What is the goal of this game when randomized?
-The goal is to beat the final mission in the mission order. The yaml configuration file controls the mission order and how missions are shuffled.
+The goal is to beat the final mission in the mission order.
+The yaml configuration file controls the mission order (e.g. blitz, grid, etc.), which combination of the four
+StarCraft 2 campaigns can be used to populate the mission order and how missions are shuffled.
+Since the first two options determine the number of missions in a StarCraft 2 world, they can be used to customize the
+expected time to complete the world.
+Note that the evolution missions from Heart of the Swarm are not included in the randomizer.
-## What non-randomized changes are there from vanilla Starcraft 2?
+## What non-randomized changes are there from vanilla StarCraft 2?
1. Some missions have more vespene geysers available to allow a wider variety of units.
-2. Many new units and upgrades have been added as items, coming from co-op, melee, later campaigns, later expansions, brood war, and original ideas.
-3. Higher-tech production structures, including Factories, Starports, Robotics Facilities, and Stargates, no longer have tech requirements.
+2. Many new units and upgrades have been added as items, coming from co-op, melee, later campaigns, later expansions,
+brood war, and original ideas.
+3. Higher-tech production structures, including Factories, Starports, Robotics Facilities, and Stargates, no longer
+have tech requirements.
4. Zerg missions have been adjusted to give the player a starting Lair where they would only have Hatcheries.
-5. Upgrades with a downside have had the downside removed, such as automated refineries costing more or tech reactors taking longer to build.
-6. Unit collision within the vents in Enemy Within has been adjusted to allow larger units to travel through them without getting stuck in odd places.
+5. Upgrades with a downside have had the downside removed, such as automated refineries costing more or tech reactors
+taking longer to build.
+6. Unit collision within the vents in Enemy Within has been adjusted to allow larger units to travel through them
+without getting stuck in odd places.
7. Several vanilla bugs have been fixed.
## Which of my items can be in another player's world?
-By default, any of StarCraft 2's items (specified above) can be in another player's world. See the
-[Advanced YAML Guide](/tutorial/Archipelago/advanced_settings/en)
-for more information on how to change this.
+By default, any of StarCraft 2's items (specified above) can be in another player's world.
+See the [Advanced YAML Guide](/tutorial/Archipelago/advanced_settings/en) for more information on how to change this.
## Unique Local Commands
-The following commands are only available when using the Starcraft 2 Client to play with Archipelago. You can list them any time in the client with `/help`.
+The following commands are only available when using the StarCraft 2 Client to play with Archipelago.
+You can list them any time in the client with `/help`.
-* `/download_data` Download the most recent release of the necessary files for playing SC2 with Archipelago. Will overwrite existing files
+* `/download_data` Download the most recent release of the necessary files for playing SC2 with Archipelago.
+Will overwrite existing files
* `/difficulty [difficulty]` Overrides the difficulty set for the world.
* Options: casual, normal, hard, brutal
* `/game_speed [game_speed]` Overrides the game speed for the world
* Options: default, slower, slow, normal, fast, faster
* `/color [faction] [color]` Changes your color for one of your playable factions.
* Faction options: raynor, kerrigan, primal, protoss, nova
- * Color options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen, brown, lightgreen, darkgrey, pink, rainbow, random, default
+ * Color options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen,
+ brown, lightgreen, darkgrey, pink, rainbow, random, default
* `/option [option_name] [option_value]` Sets an option normally controlled by your yaml after generation.
* Run without arguments to list all options.
- * Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource amounts, controlling AI allies, etc.
-* `/disable_mission_check` Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play the next mission in a chain the other player is doing.
-* `/play [mission_id]` Starts a Starcraft 2 mission based off of the mission_id provided
+ * Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource
+ amounts, controlling AI allies, etc.
+* `/disable_mission_check` Disables the check to see if a mission is available to play.
+Meant for co-op runs where one player can play the next mission in a chain the other player is doing.
+* `/play [mission_id]` Starts a StarCraft 2 mission based off of the mission_id provided
* `/available` Get what missions are currently available to play
* `/unfinished` Get what missions are currently available to play and have not had all locations checked
* `/set_path [path]` Manually set the SC2 install directory (if the automatic detection fails)
+
+Note that the behavior of the command `/received` was modified in the StarCraft 2 client.
+In the Common client of Archipelago, the command returns the list of items received in the reverse order they were
+received.
+In the StarCraft 2 client, the returned list will be divided by races (i.e., Any, Protoss, Terran, and Zerg).
+Additionally, upgrades are grouped beneath their corresponding units or buildings.
+A filter parameter can be provided, e.g., `/received Thor`, to limit the number of items shown.
+Every item whose name, race, or group name contains the provided parameter will be shown.
+
+## Known issues
+
+- StarCraft 2 Archipelago does not support loading a saved game.
+For this reason, it is recommended to play on a difficulty level lower than what you are normally comfortable with.
+- StarCraft 2 Archipelago does not support the restart of a mission from the StarCraft 2 menu.
+To restart a mission, use the StarCraft 2 Client.
+- A crash report is often generated when a mission is closed.
+This does not affect the game and can be ignored.
diff --git a/worlds/sc2/docs/fr_Starcraft 2.md b/worlds/sc2/docs/fr_Starcraft 2.md
index 4fcc8e689b..092835c8e3 100644
--- a/worlds/sc2/docs/fr_Starcraft 2.md
+++ b/worlds/sc2/docs/fr_Starcraft 2.md
@@ -21,6 +21,14 @@ Les *items* sont trouvés en accomplissant du progrès dans les catégories suiv
* Réussir des défis basés sur les succès du jeu de base, e.g. éliminer tous les *Zerg* dans la mission
*Devil's Playground*
+Dans la nomenclature d'Archipelago, il s'agit des *locations* où l'on peut trouver des *items*.
+Pour chaque *location*, incluant le fait de terminer une mission, il y a des règles qui définissent les *items*
+nécessaires pour y accéder.
+Ces règles ont été conçues en assumant que *StarCraft 2* est joué à la difficulté *Brutal*.
+Étant donné que chaque *location* a ses propres règles, il est possible qu'un *item* nécessaire à la progression se
+trouve dans une mission dont vous ne pouvez pas atteindre toutes les *locations* ou que vous ne pouvez pas terminer.
+Cependant, il est toujours nécessaire de terminer une mission pour pouvoir accéder à de nouvelles missions.
+
Ces catégories, outre la première, peuvent être désactivées dans les options du jeu.
Par exemple, vous pouvez désactiver le fait d'obtenir des *items* lorsque des étapes importantes d'une mission sont
accomplies.
@@ -37,8 +45,13 @@ Archipelago*.
## Quel est le but de ce jeu quand il est *randomized*?
-Le but est de réussir la mission finale dans la disposition des missions (e.g. *blitz*, *grid*, etc.).
-Les choix faits dans le fichier *yaml* définissent la disposition des missions et comment elles sont mélangées.
+Le but est de réussir la mission finale du *mission order* (e.g. *blitz*, *grid*, etc.).
+Le fichier de configuration yaml permet de spécifier le *mission order*, lesquelles des quatre campagnes de
+*StarCraft 2* peuvent être utilisées pour remplir le *mission order* et comment les missions sont distribuées dans le
+*mission order*.
+Étant donné que les deux premières options déterminent le nombre de missions dans un monde de *StarCraft 2*, elles
+peuvent être utilisées pour moduler le temps nécessaire pour terminer le monde.
+Notez que les missions d'évolution de Heart of the Swarm ne sont pas incluses dans le *randomizer*.
## Quelles sont les modifications non aléatoires comparativement à la version de base de *StarCraft 2*
@@ -93,3 +106,20 @@ mission de la chaîne qu'un autre joueur est en train d'entamer.
l'accès à un *item* n'ont pas été accomplis.
* `/set_path [path]` Permet de définir manuellement où *StarCraft 2* est installé ce qui est pertinent seulement si la
détection automatique de cette dernière échoue.
+
+Notez que le comportement de la commande `/received` a été modifié dans le client *StarCraft 2*.
+Dans le client *Common* d'Archipelago, elle renvoie la liste des *items* reçus dans l'ordre inverse de leur réception.
+Dans le client de *StarCraft 2*, la liste est divisée par races (i.e., *Any*, *Protoss*, *Terran*, et *Zerg*).
+De plus, les améliorations sont regroupées sous leurs unités/bâtiments correspondants.
+Un paramètre de filtrage peut aussi être fourni, e.g., `/received Thor`, pour limiter le nombre d'*items* affichés.
+Tous les *items* dont le nom, la race ou le nom de groupe contient le paramètre fourni seront affichés.
+
+## Problèmes connus
+
+- *StarCraft 2 Archipelago* ne supporte pas le chargement d'une sauvegarde.
+Pour cette raison, il est recommandé de jouer à un niveau de difficulté inférieur à celui avec lequel vous êtes
+normalement à l'aise.
+- *StarCraft 2 Archipelago* ne supporte pas le redémarrage d'une mission depuis le menu de *StarCraft 2*.
+Pour redémarrer une mission, utilisez le client de *StarCraft 2 Archipelago*.
+- Un rapport d'erreur est souvent généré lorsqu'une mission est fermée.
+Cela n'affecte pas le jeu et peut être ignoré.
diff --git a/worlds/sc2/docs/setup_en.md b/worlds/sc2/docs/setup_en.md
index 991ed57e87..5b378873f4 100644
--- a/worlds/sc2/docs/setup_en.md
+++ b/worlds/sc2/docs/setup_en.md
@@ -1,30 +1,39 @@
# StarCraft 2 Randomizer Setup Guide
-This guide contains instructions on how to install and troubleshoot the StarCraft 2 Archipelago client, as well as where
-to obtain a config file for StarCraft 2.
+This guide contains instructions on how to install and troubleshoot the StarCraft 2 Archipelago client, as well as
+where to obtain a config file for StarCraft 2.
## Required Software
- [StarCraft 2](https://starcraft2.com/en-us/)
+ - While StarCraft 2 Archipelago supports all four campaigns, they are not mandatory to play the randomizer.
+ If you do not own certain campaigns, you only need to exclude them in the configuration file of your world.
- [The most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
## How do I install this randomizer?
-1. Install StarCraft 2 and Archipelago using the links above. The StarCraft 2 Archipelago client is downloaded by the Archipelago installer.
+1. Install StarCraft 2 and Archipelago using the links above. The StarCraft 2 Archipelago client is downloaded by the
+Archipelago installer.
- Linux users should also follow the instructions found at the bottom of this page
(["Running in Linux"](#running-in-linux)).
2. Run ArchipelagoStarcraft2Client.exe.
- - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only.
-3. Type the command `/download_data`. This will automatically install the Maps and Data files from the third link above.
+ - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step
+ only.
+3. Type the command `/download_data`.
+This will automatically install the Maps and Data files needed to play StarCraft 2 Archipelago.
## Where do I get a config file (aka "YAML") for this game?
-Yaml files are configuration files that tell Archipelago how you'd like your game to be randomized, even if you're only using default options.
+Yaml files are configuration files that tell Archipelago how you'd like your game to be randomized, even if you're only
+using default options.
When you're setting up a multiworld, every world needs its own yaml file.
There are three basic ways to get a yaml:
-* You can go to the [Player Options](/games/Starcraft%202/player-options) page, set your options in the GUI, and export the yaml.
-* You can generate a template, either by downloading it from the [Player Options](/games/Starcraft%202/player-options) page or by generating it from the Launcher (ArchipelagoLauncher.exe). The template includes descriptions of each option, you just have to edit it in your text editor of choice.
+* You can go to the [Player Options](/games/Starcraft%202/player-options) page, set your options in the GUI, and export
+the yaml.
+* You can generate a template, either by downloading it from the [Player Options](/games/Starcraft%202/player-options)
+page or by generating it from the Launcher (`ArchipelagoLauncher.exe`).
+The template includes descriptions of each option, you just have to edit it in your text editor of choice.
* You can ask someone else to share their yaml to use it for yourself or adjust it as you wish.
Remember the name you enter in the options page or in the yaml file, you'll need it to connect later!
@@ -36,15 +45,31 @@ Check out [Creating a YAML](/tutorial/Archipelago/setup/en#creating-a-yaml) for
The simplest way to check is to use the website [validator](/check).
-You can also test it by attempting to generate a multiworld with your yaml. Save your yaml to the Players/ folder within your Archipelago installation and run ArchipelagoGenerate.exe. You should see a new .zip file within the output/ folder of your Archipelago installation if things worked correctly. It's advisable to run ArchipelagoGenerate through a terminal so that you can see the printout, which will include any errors and the precise output file name if it's successful. If you don't like terminals, you can also check the log file in the logs/ folder.
+You can also test it by attempting to generate a multiworld with your yaml. Save your yaml to the `Players/` folder
+within your Archipelago installation and run `ArchipelagoGenerate.exe`.
+You should see a new `.zip` file within the `output/` folder of your Archipelago installation if things worked
+correctly.
+It's advisable to run `ArchipelagoGenerate.exe` through a terminal so that you can see the printout, which will include
+any errors and the precise output file name if it's successful.
+If you don't like terminals, you can also check the log file in the `logs/` folder.
#### What does Progression Balancing do?
-For Starcraft 2, not much. It's an Archipelago-wide option meant to shift required items earlier in the playthrough, but Starcraft 2 tends to be much more open in what items you can use. As such, this adjustment isn't very noticeable. It can also increase generation times, so we generally recommend turning it off.
+For StarCraft 2, this option doesn't have much impact.
+It is an Archipelago option designed to balance world progression by swapping items in spheres.
+If the Progression Balancing of one world is greater than that of others, items in that world are more likely to be
+obtained early, and vice versa if its value is smaller.
+However, StarCraft 2 is more permissive regarding the items that can be used to progress, so this option has little
+influence on progression in a StarCraft 2 world.
+StarCraft 2.
+Since this option increases the time required to generate a MultiWorld, we recommend deactivating it (i.e., setting it
+to zero) for a StarCraft 2 world.
#### How do I specify items in a list, like in excluded items?
-You can look up the syntax for yaml collections in the [YAML specification](https://yaml.org/spec/1.2.2/#21-collections). For lists, every item goes on its own line, started with a hyphen:
+You can look up the syntax for yaml collections in the
+[YAML specification](https://yaml.org/spec/1.2.2/#21-collections).
+For lists, every item goes on its own line, started with a hyphen:
```yaml
excluded_items:
@@ -52,11 +77,13 @@ excluded_items:
- Drop-Pods (Kerrigan Tier 7)
```
-An empty list is just a matching pair of square brackets: `[]`. That's the default value in the template, which should let you know to use this syntax.
+An empty list is just a matching pair of square brackets: `[]`.
+That's the default value in the template, which should let you know to use this syntax.
#### How do I specify items for the starting inventory?
-The starting inventory is a YAML mapping rather than a list, which associates an item with the amount you start with. The syntax looks like the item name, followed by a colon, then a whitespace character, and then the value:
+The starting inventory is a YAML mapping rather than a list, which associates an item with the amount you start with.
+The syntax looks like the item name, followed by a colon, then a whitespace character, and then the value:
```yaml
start_inventory:
@@ -64,37 +91,61 @@ start_inventory:
Additional Starting Vespene: 5
```
-An empty mapping is just a matching pair of curly braces: `{}`. That's the default value in the template, which should let you know to use this syntax.
+An empty mapping is just a matching pair of curly braces: `{}`.
+That's the default value in the template, which should let you know to use this syntax.
#### How do I know the exact names of items and locations?
-The [*datapackage*](/datapackage) page of the Archipelago website provides a complete list of the items and locations for each game that it currently supports, including StarCraft 2.
+The [*datapackage*](/datapackage) page of the Archipelago website provides a complete list of the items and locations
+for each game that it currently supports, including StarCraft 2.
-You can also look up a complete list of the item names in the [Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page.
+You can also look up a complete list of the item names in the
+[Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page.
This page also contains supplementary information of each item.
-However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development.
+However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the
+former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development.
-As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over the mission in the 'StarCraft 2 Launcher' tab in the client.
+As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over
+the mission in the 'StarCraft 2 Launcher' tab in the client.
## How do I join a MultiWorld game?
1. Run ArchipelagoStarcraft2Client.exe.
- - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only.
+ - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step
+ only.
2. Type `/connect [server ip]`.
- If you're running through the website, the server IP should be displayed near the top of the room page.
3. Type your slot name from your YAML when prompted.
4. If the server has a password, enter that when prompted.
-5. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your world. Unreachable missions will have greyed-out text. Just click on an available mission to start it!
+5. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your
+world.
+Unreachable missions will have greyed-out text. Just click on an available mission to start it!
## The game isn't launching when I try to start a mission.
-First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If you can't figure out
-the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel for help. Please include a
-specific description of what's going wrong and attach your log file to your message.
+First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`).
+If you can't figure out the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel
+for help.
+Please include a specific description of what's going wrong and attach your log file to your message.
+
+## My keyboard shortcuts profile is not available when I play *StarCraft 2 Archipelago*.
+
+For your keyboard shortcuts profile to work in Archipelago, you need to copy your shortcuts file from
+`Documents/StarCraft II/Accounts/######/Hotkeys` to `Documents/StarCraft II/Hotkeys`.
+If the folder doesn't exist, create it.
+
+To enable StarCraft 2 Archipelago to use your profile, follow these steps:
+1. Launch StarCraft 2 via the Battle.net application.
+2. Change your hotkey profile to the standard mode and accept.
+3. Select your custom profile and accept.
+
+You will only need to do this once.
## Running in macOS
-To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](/tutorial/Archipelago/mac/en). Note: to launch the client, you will need to run the command `python3 Starcraft2Client.py`.
+To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here:
+[macOS Guide](/tutorial/Archipelago/mac/en).
+Note: to launch the client, you will need to run the command `python3 Starcraft2Client.py`.
## Running in Linux
@@ -102,9 +153,9 @@ To run StarCraft 2 through Archipelago in Linux, you will need to install the ga
of the Archipelago client.
Make sure you have StarCraft 2 installed using Wine, and that you have followed the
-[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location. You will not
-need to copy the .dll files. If you're having trouble installing or running StarCraft 2 on Linux, I recommend using the
-Lutris installer.
+[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location.
+You will not need to copy the `.dll` files.
+If you're having trouble installing or running StarCraft 2 on Linux, it is recommend to use the Lutris installer.
Copy the following into a .sh file, replacing the values of **WINE** and **SC2PATH** variables with the relevant
locations, as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same
@@ -139,5 +190,5 @@ below, replacing **${ID}** with the numerical ID.
lutris lutris:rungameid/${ID} --output-script sc2.sh
This will get all of the relevant environment variables Lutris sets to run StarCraft 2 in a script, including the path
-to the Wine binary that Lutris uses. You can then remove the line that runs the Battle.Net launcher and copy the code
-above into the existing script.
+to the Wine binary that Lutris uses.
+You can then remove the line that runs the Battle.Net launcher and copy the code above into the existing script.
diff --git a/worlds/sc2/docs/setup_fr.md b/worlds/sc2/docs/setup_fr.md
index bb6c35bce1..d9b754572a 100644
--- a/worlds/sc2/docs/setup_fr.md
+++ b/worlds/sc2/docs/setup_fr.md
@@ -6,6 +6,10 @@ indications pour obtenir un fichier de configuration de *StarCraft 2 Archipelago
## Logiciels requis
- [*StarCraft 2*](https://starcraft2.com/en-us/)
+ - Bien que *StarCraft 2 Archipelago* supporte les quatre campagnes, elles ne sont pas obligatoires pour jouer au
+ *randomizer*.
+ Si vous ne possédez pas certaines campagnes, il vous suffit de les exclure dans le fichier de configuration de
+ votre monde.
- [La version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
## Comment est-ce que j'installe ce *randomizer*?
@@ -41,10 +45,6 @@ préférences.
Prenez soin de vous rappeler du nom de joueur que vous avez inscrit dans la page à options ou dans le fichier *yaml*
puisque vous en aurez besoin pour vous connecter à votre monde!
-Notez que la page *Player options* ne permet pas de définir certaines des options avancées, e.g., l'exclusion de
-certaines unités ou de leurs améliorations.
-Utilisez la page [*Weighted Options*](/weighted-options) pour avoir accès à ces dernières.
-
Si vous désirez des informations et/ou instructions générales sur l'utilisation d'un fichier *yaml* pour Archipelago,
veuillez consulter [*Creating a YAML*](/tutorial/Archipelago/setup/en#creating-a-yaml).
@@ -66,15 +66,15 @@ dans le dossier `logs/`.
#### À quoi sert l'option *Progression Balancing*?
-Pour *Starcraft 2*, cette option ne fait pas grand-chose.
+Pour *StarCraft 2*, cette option ne fait pas grand-chose.
Il s'agit d'une option d'Archipelago permettant d'équilibrer la progression des mondes en interchangeant les *items*
dans les *spheres*.
Si le *Progression Balancing* d'un monde est plus grand que ceux des autres, les *items* de progression de ce monde ont
plus de chance d'être obtenus tôt et vice-versa si sa valeur est plus petite que celle des autres mondes.
-Cependant, *Starcraft 2* est beaucoup plus permissif en termes d'*items* qui permettent de progresser, ce réglage Ã
+Cependant, *StarCraft 2* est beaucoup plus permissif en termes d'*items* qui permettent de progresser, ce réglage Ã
donc peu d'influence sur la progression dans *StarCraft 2*.
Vu qu'il augmente le temps de génération d'un *MultiWorld*, nous recommandons de le désactiver, c-à -d le définir Ã
-zéro, pour *Starcraft 2*.
+zéro, pour *StarCraft 2*.
#### Comment est-ce que je définis une liste d'*items*, e.g. pour l'option *excluded items*?
@@ -122,6 +122,10 @@ Cependant, l'information présente dans cette dernière peut différer de celle
puisqu'elle est générée, habituellement, à partir de la version en développement de *StarCraft 2 Archipelago* qui
n'ont peut-être pas encore été inclus dans le site web d'Archipelago.
+Pour ce qui concerne les *locations*, vous pouvez consulter tous les *locations* associés à une mission dans votre
+monde en plaçant votre curseur sur la case correspondante dans l'onglet *StarCraft 2 Launcher* du client.
+
+
## Comment est-ce que je peux joindre un *MultiWorld*?
1. Exécuter `ArchipelagoStarcraft2Client.exe`.
@@ -152,7 +156,7 @@ qui se trouve dans `Documents/StarCraft II/Accounts/######/Hotkeys` vers `Docume
Si le dossier n'existe pas, créez-le.
Pour que *StarCraft 2 Archipelago* utilise votre profil, suivez les étapes suivantes.
-Lancez *Starcraft 2* via l'application *Battle.net*.
+Lancez *StarCraft 2* via l'application *Battle.net*.
Changez votre profil de raccourcis clavier pour le mode standard et acceptez, puis sélectionnez votre profil
personnalisé et acceptez.
Vous n'aurez besoin de faire ça qu'une seule fois.
diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py
index 4d5567302a..17fabca28d 100644
--- a/worlds/stardew_valley/logic/skill_logic.py
+++ b/worlds/stardew_valley/logic/skill_logic.py
@@ -15,13 +15,13 @@ from .. import options
from ..data.harvest import HarvestCropSource
from ..mods.logic.magic_logic import MagicLogicMixin
from ..mods.logic.mod_skills_levels import get_mod_skill_levels
-from ..stardew_rule import StardewRule, True_, False_, true_, And
+from ..stardew_rule import StardewRule, true_, True_, False_
from ..strings.craftable_names import Fishing
from ..strings.machine_names import Machine
from ..strings.performance_names import Performance
from ..strings.quality_names import ForageQuality
from ..strings.region_names import Region
-from ..strings.skill_names import Skill, all_mod_skills
+from ..strings.skill_names import Skill, all_mod_skills, all_vanilla_skills
from ..strings.tool_names import ToolMaterial, Tool
from ..strings.wallet_item_names import Wallet
@@ -43,22 +43,17 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
if level <= 0:
return True_()
- tool_level = (level - 1) // 2
+ tool_level = min(4, (level - 1) // 2)
tool_material = ToolMaterial.tiers[tool_level]
- months = max(1, level - 1)
- months_rule = self.logic.time.has_lived_months(months)
- if self.options.skill_progression != options.SkillProgression.option_vanilla:
- previous_level_rule = self.logic.skill.has_level(skill, level - 1)
- else:
- previous_level_rule = true_
+ previous_level_rule = self.logic.skill.has_previous_level(skill, level)
if skill == Skill.fishing:
xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 3))
elif skill == Skill.farming:
xp_rule = self.can_get_farming_xp & self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level)
elif skill == Skill.foraging:
- xp_rule = (self.can_get_foraging_xp & self.logic.tool.has_tool(Tool.axe, tool_material)) |\
+ xp_rule = (self.can_get_foraging_xp & self.logic.tool.has_tool(Tool.axe, tool_material)) | \
self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level)
elif skill == Skill.mining:
xp_rule = self.logic.tool.has_tool(Tool.pickaxe, tool_material) | \
@@ -70,22 +65,34 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
xp_rule = xp_rule & self.logic.region.can_reach(Region.mines_floor_5)
elif skill in all_mod_skills:
# Ideal solution would be to add a logic registry, but I'm too lazy.
- return previous_level_rule & months_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level)
+ return previous_level_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level)
else:
raise Exception(f"Unknown skill: {skill}")
- return previous_level_rule & months_rule & xp_rule
+ return previous_level_rule & xp_rule
# Should be cached
def has_level(self, skill: str, level: int) -> StardewRule:
- if level <= 0:
- return True_()
+ assert level >= 0, f"There is no level before level 0."
+ if level == 0:
+ return true_
if self.options.skill_progression == options.SkillProgression.option_vanilla:
return self.logic.skill.can_earn_level(skill, level)
return self.logic.received(f"{skill} Level", level)
+ def has_previous_level(self, skill: str, level: int) -> StardewRule:
+ assert level > 0, f"There is no level before level 0."
+ if level == 1:
+ return true_
+
+ if self.options.skill_progression == options.SkillProgression.option_vanilla:
+ months = max(1, level - 1)
+ return self.logic.time.has_lived_months(months)
+
+ return self.logic.received(f"{skill} Level", level - 1)
+
@cache_self1
def has_farming_level(self, level: int) -> StardewRule:
return self.logic.skill.has_level(Skill.farming, level)
@@ -108,18 +115,9 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
return rule_with_fishing
return self.logic.time.has_lived_months(months_with_4_skills) | rule_with_fishing
- def has_all_skills_maxed(self, included_modded_skills: bool = True) -> StardewRule:
- if self.options.skill_progression == options.SkillProgression.option_vanilla:
- return self.has_total_level(50)
- skills_items = vanilla_skill_items
- if included_modded_skills:
- skills_items += get_mod_skill_levels(self.options.mods)
- return And(*[self.logic.received(skill, 10) for skill in skills_items])
-
- def can_enter_mastery_cave(self) -> StardewRule:
- if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries:
- return self.logic.received(Wallet.mastery_of_the_five_ways)
- return self.has_all_skills_maxed()
+ def has_any_skills_maxed(self, included_modded_skills: bool = True) -> StardewRule:
+ skills = self.content.skills.keys() if included_modded_skills else sorted(all_vanilla_skills)
+ return self.logic.or_(*(self.logic.skill.has_level(skill, 10) for skill in skills))
@cached_property
def can_get_farming_xp(self) -> StardewRule:
@@ -197,13 +195,19 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
return self.has_level(Skill.foraging, 9)
return False_()
- @cached_property
- def can_earn_mastery_experience(self) -> StardewRule:
- if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries:
- return self.has_all_skills_maxed() & self.logic.time.has_lived_max_months
- return self.logic.time.has_lived_max_months
+ def can_earn_mastery(self, skill: str) -> StardewRule:
+ # Checking for level 11, so it includes having level 10 and being able to earn xp.
+ return self.logic.skill.can_earn_level(skill, 11) & self.logic.region.can_reach(Region.mastery_cave)
def has_mastery(self, skill: str) -> StardewRule:
- if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries:
- return self.can_earn_mastery_experience and self.logic.region.can_reach(Region.mastery_cave)
- return self.logic.received(f"{skill} Mastery")
+ if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries:
+ return self.logic.received(f"{skill} Mastery")
+
+ return self.logic.skill.can_earn_mastery(skill)
+
+ @cached_property
+ def can_enter_mastery_cave(self) -> StardewRule:
+ if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries:
+ return self.logic.received(Wallet.mastery_of_the_five_ways)
+
+ return self.has_any_skills_maxed(included_modded_skills=False)
diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py
index 89b1cf87c3..e9bdd8c25b 100644
--- a/worlds/stardew_valley/rules.py
+++ b/worlds/stardew_valley/rules.py
@@ -154,7 +154,7 @@ def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiw
extra_raccoons = extra_raccoons + num
bundle_rules = logic.received(CommunityUpgrade.raccoon, extra_raccoons) & bundle_rules
if num > 1:
- previous_bundle_name = f"Raccoon Request {num-1}"
+ previous_bundle_name = f"Raccoon Request {num - 1}"
bundle_rules = bundle_rules & logic.region.can_reach_location(previous_bundle_name)
room_rules.append(bundle_rules)
MultiWorldRules.set_rule(location, bundle_rules)
@@ -168,13 +168,16 @@ def set_skills_rules(logic: StardewLogic, multiworld, player, world_options: Sta
mods = world_options.mods
if world_options.skill_progression == SkillProgression.option_vanilla:
return
+
for i in range(1, 11):
set_vanilla_skill_rule_for_level(logic, multiworld, player, i)
set_modded_skill_rule_for_level(logic, multiworld, player, mods, i)
- if world_options.skill_progression != SkillProgression.option_progressive_with_masteries:
+
+ if world_options.skill_progression == SkillProgression.option_progressive:
return
+
for skill in [Skill.farming, Skill.fishing, Skill.foraging, Skill.mining, Skill.combat]:
- MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery_experience)
+ MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery(skill))
def set_vanilla_skill_rule_for_level(logic: StardewLogic, multiworld, player, level: int):
@@ -256,8 +259,7 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S
set_entrance_rule(multiworld, player, LogicEntrance.farmhouse_cooking, logic.cooking.can_cook_in_kitchen)
set_entrance_rule(multiworld, player, LogicEntrance.shipping, logic.shipping.can_use_shipping_bin)
set_entrance_rule(multiworld, player, LogicEntrance.watch_queen_of_sauce, logic.action.can_watch(Channel.queen_of_sauce))
- set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave())
- set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave())
+ set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave)
set_entrance_rule(multiworld, player, LogicEntrance.buy_experience_books, logic.time.has_lived_months(2))
set_entrance_rule(multiworld, player, LogicEntrance.buy_year1_books, logic.time.has_year_two)
set_entrance_rule(multiworld, player, LogicEntrance.buy_year3_books, logic.time.has_year_three)
diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py
index 4dee0ebf6d..e7278cba28 100644
--- a/worlds/stardew_valley/test/__init__.py
+++ b/worlds/stardew_valley/test/__init__.py
@@ -85,7 +85,7 @@ def allsanity_no_mods_6_x_x():
options.QuestLocations.internal_name: 56,
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
options.Shipsanity.internal_name: options.Shipsanity.option_everything,
- options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
+ options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries,
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
options.TrapItems.internal_name: options.TrapItems.option_nightmare,
@@ -310,6 +310,12 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
self.multiworld.worlds[self.player].total_progression_items -= 1
return created_item
+ def remove_one_by_name(self, item: str) -> None:
+ self.remove(self.create_item(item))
+
+ def reset_collection_state(self):
+ self.multiworld.state = self.original_state.copy()
+
pre_generated_worlds = {}
diff --git a/worlds/stardew_valley/test/rules/TestSkills.py b/worlds/stardew_valley/test/rules/TestSkills.py
index 1c6874f315..77adade886 100644
--- a/worlds/stardew_valley/test/rules/TestSkills.py
+++ b/worlds/stardew_valley/test/rules/TestSkills.py
@@ -1,23 +1,30 @@
-from ... import HasProgressionPercent
+from ... import HasProgressionPercent, StardewLogic
from ...options import ToolProgression, SkillProgression, Mods
-from ...strings.skill_names import all_skills
+from ...strings.skill_names import all_skills, all_vanilla_skills, Skill
from ...test import SVTestBase
-class TestVanillaSkillLogicSimplification(SVTestBase):
+class TestSkillProgressionVanilla(SVTestBase):
options = {
SkillProgression.internal_name: SkillProgression.option_vanilla,
ToolProgression.internal_name: ToolProgression.option_progressive,
}
def test_skill_logic_has_level_only_uses_one_has_progression_percent(self):
- rule = self.multiworld.worlds[1].logic.skill.has_level("Farming", 8)
- self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) == HasProgressionPercent))
+ rule = self.multiworld.worlds[1].logic.skill.has_level(Skill.farming, 8)
+ self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) is HasProgressionPercent))
+
+ def test_has_mastery_requires_month_equivalent_to_10_levels(self):
+ logic: StardewLogic = self.multiworld.worlds[1].logic
+ rule = logic.skill.has_mastery(Skill.farming)
+ time_rule = logic.time.has_lived_months(10)
+
+ self.assertIn(time_rule, rule.current_rules)
-class TestAllSkillsRequirePrevious(SVTestBase):
+class TestSkillProgressionProgressive(SVTestBase):
options = {
- SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
+ SkillProgression.internal_name: SkillProgression.option_progressive,
Mods.internal_name: frozenset(Mods.valid_keys),
}
@@ -25,16 +32,82 @@ class TestAllSkillsRequirePrevious(SVTestBase):
for skill in all_skills:
self.collect_everything()
self.remove_by_name(f"{skill} Level")
+
for level in range(1, 11):
location_name = f"Level {level} {skill}"
+ location = self.multiworld.get_location(location_name, self.player)
+
with self.subTest(location_name):
- can_reach = self.can_reach_location(location_name)
if level > 1:
- self.assertFalse(can_reach)
+ self.assert_reach_location_false(location, self.multiworld.state)
self.collect(f"{skill} Level")
- can_reach = self.can_reach_location(location_name)
- self.assertTrue(can_reach)
- self.multiworld.state = self.original_state.copy()
+
+ self.assert_reach_location_true(location, self.multiworld.state)
+
+ self.reset_collection_state()
+
+ def test_has_level_requires_exact_amount_of_levels(self):
+ logic: StardewLogic = self.multiworld.worlds[1].logic
+ rule = logic.skill.has_level(Skill.farming, 8)
+ level_rule = logic.received("Farming Level", 8)
+
+ self.assertEqual(level_rule, rule)
+
+ def test_has_previous_level_requires_one_less_level_than_requested(self):
+ logic: StardewLogic = self.multiworld.worlds[1].logic
+ rule = logic.skill.has_previous_level(Skill.farming, 8)
+ level_rule = logic.received("Farming Level", 7)
+
+ self.assertEqual(level_rule, rule)
+
+ def test_has_mastery_requires_10_levels(self):
+ logic: StardewLogic = self.multiworld.worlds[1].logic
+ rule = logic.skill.has_mastery(Skill.farming)
+ level_rule = logic.received("Farming Level", 10)
+
+ self.assertIn(level_rule, rule.current_rules)
+class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase):
+ options = {
+ SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
+ ToolProgression.internal_name: ToolProgression.option_progressive,
+ Mods.internal_name: frozenset(),
+ }
+ def test_has_mastery_requires_the_item(self):
+ logic: StardewLogic = self.multiworld.worlds[1].logic
+ rule = logic.skill.has_mastery(Skill.farming)
+ received_mastery = logic.received("Farming Mastery")
+
+ self.assertEqual(received_mastery, rule)
+
+ def test_given_all_levels_when_can_earn_mastery_then_can_earn_mastery(self):
+ self.collect_everything()
+
+ for skill in all_vanilla_skills:
+ with self.subTest(skill):
+ location = self.multiworld.get_location(f"{skill} Mastery", self.player)
+ self.assert_reach_location_true(location, self.multiworld.state)
+
+ self.reset_collection_state()
+
+ def test_given_one_level_missing_when_can_earn_mastery_then_cannot_earn_mastery(self):
+ for skill in all_vanilla_skills:
+ with self.subTest(skill):
+ self.collect_everything()
+ self.remove_one_by_name(f"{skill} Level")
+
+ location = self.multiworld.get_location(f"{skill} Mastery", self.player)
+ self.assert_reach_location_false(location, self.multiworld.state)
+
+ self.reset_collection_state()
+
+ def test_given_one_tool_missing_when_can_earn_mastery_then_cannot_earn_mastery(self):
+ self.collect_everything()
+
+ self.remove_one_by_name(f"Progressive Pickaxe")
+ location = self.multiworld.get_location("Mining Mastery", self.player)
+ self.assert_reach_location_false(location, self.multiworld.state)
+
+ self.reset_collection_state()
diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py
index 58d8fa543a..c3cf40a7c0 100644
--- a/worlds/subnautica/__init__.py
+++ b/worlds/subnautica/__init__.py
@@ -45,7 +45,7 @@ class SubnauticaWorld(World):
options_dataclass = options.SubnauticaOptions
options: options.SubnauticaOptions
required_client_version = (0, 5, 0)
-
+ origin_region_name = "Planet 4546B"
creatures_to_scan: List[str]
def generate_early(self) -> None:
@@ -66,13 +66,9 @@ class SubnauticaWorld(World):
creature_pool, self.options.creature_scans.value)
def create_regions(self):
- # Create Regions
- menu_region = Region("Menu", self.player, self.multiworld)
+ # Create Region
planet_region = Region("Planet 4546B", self.player, self.multiworld)
- # Link regions together
- menu_region.connect(planet_region, "Lifepod 5")
-
# Create regular locations
location_names = itertools.chain((location["name"] for location in locations.location_table.values()),
(creature + creatures.suffix for creature in self.creatures_to_scan))
@@ -93,11 +89,8 @@ class SubnauticaWorld(World):
# make the goal event the victory "item"
location.item.name = "Victory"
- # Register regions to multiworld
- self.multiworld.regions += [
- menu_region,
- planet_region
- ]
+ # Register region to multiworld
+ self.multiworld.regions.append(planet_region)
# refer to rules.py
set_rules = set_rules
diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py
index bbffd9c144..cdd968acce 100644
--- a/worlds/tunic/__init__.py
+++ b/worlds/tunic/__init__.py
@@ -7,8 +7,9 @@ from .rules import set_location_rules, set_region_rules, randomize_ability_unloc
from .er_rules import set_er_location_rules
from .regions import tunic_regions
from .er_scripts import create_er_regions
-from .er_data import portal_mapping
-from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections
+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)
from worlds.AutoWorld import WebWorld, World
from Options import PlandoConnection
from decimal import Decimal, ROUND_HALF_UP
@@ -48,10 +49,12 @@ class TunicLocation(Location):
class SeedGroup(TypedDict):
- logic_rules: int # logic rules value
+ laurels_zips: bool # laurels_zips value
+ 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 of plando connections for the seed group
+ plando: TunicPlandoConnections # consolidated plando connections for the seed group
class TunicWorld(World):
@@ -77,8 +80,17 @@ 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
+ er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work
def generate_early(self) -> None:
+ 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:
for index, cxn in enumerate(self.options.plando_connections):
# making shops second to simplify other things later
@@ -99,7 +111,10 @@ class TunicWorld(World):
self.options.keys_behind_bosses.value = passthrough["keys_behind_bosses"]
self.options.sword_progression.value = passthrough["sword_progression"]
self.options.ability_shuffling.value = passthrough["ability_shuffling"]
- self.options.logic_rules.value = passthrough["logic_rules"]
+ self.options.laurels_zips.value = passthrough["laurels_zips"]
+ self.options.ice_grappling.value = passthrough["ice_grappling"]
+ self.options.ladder_storage.value = passthrough["ladder_storage"]
+ self.options.ladder_storage_without_items = passthrough["ladder_storage_without_items"]
self.options.lanternless.value = passthrough["lanternless"]
self.options.maskless.value = passthrough["maskless"]
self.options.hexagon_quest.value = passthrough["hexagon_quest"]
@@ -118,19 +133,28 @@ class TunicWorld(World):
group = tunic.options.entrance_rando.value
# if this is the first world in the group, set the rules equal to its rules
if group not in cls.seed_groups:
- cls.seed_groups[group] = SeedGroup(logic_rules=tunic.options.logic_rules.value,
- laurels_at_10_fairies=tunic.options.laurels_location == 3,
- fixed_shop=bool(tunic.options.fixed_shop),
- plando=tunic.options.plando_connections)
+ cls.seed_groups[group] = \
+ SeedGroup(laurels_zips=bool(tunic.options.laurels_zips),
+ 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)
continue
-
+
+ # off is more restrictive
+ if not tunic.options.laurels_zips:
+ cls.seed_groups[group]["laurels_zips"] = False
# lower value is more restrictive
- if tunic.options.logic_rules.value < cls.seed_groups[group]["logic_rules"]:
- cls.seed_groups[group]["logic_rules"] = tunic.options.logic_rules.value
+ if tunic.options.ice_grappling < cls.seed_groups[group]["ice_grappling"]:
+ cls.seed_groups[group]["ice_grappling"] = tunic.options.ice_grappling.value
+ # lower value is more restrictive
+ if tunic.options.ladder_storage.value < cls.seed_groups[group]["ladder_storage"]:
+ cls.seed_groups[group]["ladder_storage"] = tunic.options.ladder_storage.value
# 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
- # fewer shops, one at windmill
+ # 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
@@ -366,7 +390,10 @@ class TunicWorld(World):
"ability_shuffling": self.options.ability_shuffling.value,
"hexagon_quest": self.options.hexagon_quest.value,
"fool_traps": self.options.fool_traps.value,
- "logic_rules": self.options.logic_rules.value,
+ "laurels_zips": self.options.laurels_zips.value,
+ "ice_grappling": self.options.ice_grappling.value,
+ "ladder_storage": self.options.ladder_storage.value,
+ "ladder_storage_without_items": self.options.ladder_storage_without_items.value,
"lanternless": self.options.lanternless.value,
"maskless": self.options.maskless.value,
"entrance_rando": int(bool(self.options.entrance_rando.value)),
diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md
index 27df4ce38b..b2e1a71897 100644
--- a/worlds/tunic/docs/en_TUNIC.md
+++ b/worlds/tunic/docs/en_TUNIC.md
@@ -83,8 +83,6 @@ Notes:
- 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.
-- If you have more than one shop in a scene, you may be wrong warped when exiting a shop.
-- If you have a shop in every scene, and you have an odd number of shops, it will error out.
See the [Archipelago Plando Guide](../../../tutorial/Archipelago/plando/en) for more information on Plando and Connection Plando.
diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py
index 6316292e56..343bf30553 100644
--- a/worlds/tunic/er_data.py
+++ b/worlds/tunic/er_data.py
@@ -1,6 +1,9 @@
-from typing import Dict, NamedTuple, List
+from typing import Dict, NamedTuple, List, TYPE_CHECKING, Optional
from enum import IntEnum
+if TYPE_CHECKING:
+ from . import TunicWorld
+
class Portal(NamedTuple):
name: str # human-readable name
@@ -9,6 +12,8 @@ class Portal(NamedTuple):
tag: str # vanilla tag
def scene(self) -> str: # the actual scene name in Tunic
+ if self.region.startswith("Shop"):
+ return tunic_er_regions["Shop"].game_scene
return tunic_er_regions[self.region].game_scene
def scene_destination(self) -> str: # full, nonchanging name to interpret by the mod
@@ -458,7 +463,7 @@ portal_mapping: List[Portal] = [
Portal(name="Cathedral Main Exit", region="Cathedral",
destination="Swamp Redux 2", tag="_main"),
- Portal(name="Cathedral Elevator", region="Cathedral",
+ Portal(name="Cathedral Elevator", region="Cathedral to Gauntlet",
destination="Cathedral Arena", tag="_"),
Portal(name="Cathedral Secret Legend Room Exit", region="Cathedral Secret Legend Room",
destination="Swamp Redux 2", tag="_secret"),
@@ -517,6 +522,13 @@ portal_mapping: List[Portal] = [
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
+def get_portal_outlet_region(portal: Portal, world: "TunicWorld") -> str:
+ return world.er_regions[portal.region].outlet_region or portal.region
class DeadEnd(IntEnum):
@@ -558,11 +570,11 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Overworld Ruined Passage Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal
"Overworld Old House Door": RegionInfo("Overworld Redux"), # the too-small space between the door and the portal
"Overworld Southeast Cross Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal
- "Overworld Fountain Cross Door": RegionInfo("Overworld Redux"), # the small space between the door and the portal
+ "Overworld Fountain Cross Door": RegionInfo("Overworld Redux", outlet_region="Overworld"),
"Overworld Temple Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal
- "Overworld Town Portal": RegionInfo("Overworld Redux"), # being able to go to or come from the portal
- "Overworld Spawn Portal": RegionInfo("Overworld Redux"), # being able to go to or come from the portal
- "Cube Cave Entrance Region": RegionInfo("Overworld Redux"), # other side of the bomb wall
+ "Overworld Town Portal": RegionInfo("Overworld Redux", outlet_region="Overworld"),
+ "Overworld Spawn Portal": RegionInfo("Overworld Redux", outlet_region="Overworld"),
+ "Cube Cave Entrance Region": RegionInfo("Overworld Redux", outlet_region="Overworld"), # other side of the bomb wall
"Stick House": RegionInfo("Sword Cave", dead_end=DeadEnd.all_cats),
"Windmill": RegionInfo("Windmill"),
"Old House Back": RegionInfo("Overworld Interiors"), # part with the hc door
@@ -591,7 +603,7 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Forest Belltower Lower": RegionInfo("Forest Belltower"),
"East Forest": RegionInfo("East Forest Redux"),
"East Forest Dance Fox Spot": RegionInfo("East Forest Redux"),
- "East Forest Portal": RegionInfo("East Forest Redux"),
+ "East Forest Portal": RegionInfo("East Forest Redux", outlet_region="East Forest"),
"Lower Forest": RegionInfo("East Forest Redux"), # bottom of the forest
"Guard House 1 East": RegionInfo("East Forest Redux Laddercave"),
"Guard House 1 West": RegionInfo("East Forest Redux Laddercave"),
@@ -601,7 +613,7 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Forest Grave Path Main": RegionInfo("Sword Access"),
"Forest Grave Path Upper": RegionInfo("Sword Access"),
"Forest Grave Path by Grave": RegionInfo("Sword Access"),
- "Forest Hero's Grave": RegionInfo("Sword Access"),
+ "Forest Hero's Grave": RegionInfo("Sword Access", outlet_region="Forest Grave Path by Grave"),
"Dark Tomb Entry Point": RegionInfo("Crypt Redux"), # both upper exits
"Dark Tomb Upper": RegionInfo("Crypt Redux"), # the part with the casket and the top of the ladder
"Dark Tomb Main": RegionInfo("Crypt Redux"),
@@ -614,18 +626,19 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Beneath the Well Back": RegionInfo("Sewer"), # the back two portals, and all 4 upper chests
"West Garden": RegionInfo("Archipelagos Redux"),
"Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats),
- "West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
+ "West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted, outlet_region="West Garden by Portal"),
+ "West Garden by Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
"West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
"West Garden Laurels Exit Region": RegionInfo("Archipelagos Redux"),
"West Garden after Boss": RegionInfo("Archipelagos Redux"),
- "West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux"),
+ "West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux", outlet_region="West Garden"),
"Ruined Atoll": RegionInfo("Atoll Redux"),
"Ruined Atoll Lower Entry Area": RegionInfo("Atoll Redux"),
"Ruined Atoll Ladder Tops": RegionInfo("Atoll Redux"), # at the top of the 5 ladders in south Atoll
"Ruined Atoll Frog Mouth": RegionInfo("Atoll Redux"),
"Ruined Atoll Frog Eye": RegionInfo("Atoll Redux"),
- "Ruined Atoll Portal": RegionInfo("Atoll Redux"),
- "Ruined Atoll Statue": RegionInfo("Atoll Redux"),
+ "Ruined Atoll Portal": RegionInfo("Atoll Redux", outlet_region="Ruined Atoll"),
+ "Ruined Atoll Statue": RegionInfo("Atoll Redux", outlet_region="Ruined Atoll"),
"Frog Stairs Eye Exit": RegionInfo("Frog Stairs"),
"Frog Stairs Upper": RegionInfo("Frog Stairs"),
"Frog Stairs Lower": RegionInfo("Frog Stairs"),
@@ -633,18 +646,20 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Frog's Domain Entry": RegionInfo("frog cave main"),
"Frog's Domain": RegionInfo("frog cave main"),
"Frog's Domain Back": RegionInfo("frog cave main"),
- "Library Exterior Tree Region": RegionInfo("Library Exterior"),
+ "Library Exterior Tree Region": RegionInfo("Library Exterior", outlet_region="Library Exterior by Tree"),
+ "Library Exterior by Tree": RegionInfo("Library Exterior"),
"Library Exterior Ladder Region": RegionInfo("Library Exterior"),
"Library Hall Bookshelf": RegionInfo("Library Hall"),
"Library Hall": RegionInfo("Library Hall"),
- "Library Hero's Grave Region": RegionInfo("Library Hall"),
+ "Library Hero's Grave Region": RegionInfo("Library Hall", outlet_region="Library Hall"),
"Library Hall to Rotunda": RegionInfo("Library Hall"),
"Library Rotunda to Hall": RegionInfo("Library Rotunda"),
"Library Rotunda": RegionInfo("Library Rotunda"),
"Library Rotunda to Lab": RegionInfo("Library Rotunda"),
"Library Lab": RegionInfo("Library Lab"),
"Library Lab Lower": RegionInfo("Library Lab"),
- "Library Portal": RegionInfo("Library Lab"),
+ "Library Portal": RegionInfo("Library Lab", outlet_region="Library Lab on Portal Pad"),
+ "Library Lab on Portal Pad": RegionInfo("Library Lab"),
"Library Lab to Librarian": RegionInfo("Library Lab"),
"Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats),
"Fortress Exterior from East Forest": RegionInfo("Fortress Courtyard"),
@@ -663,22 +678,22 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Fortress Grave Path": RegionInfo("Fortress Reliquary"),
"Fortress Grave Path Upper": RegionInfo("Fortress Reliquary", dead_end=DeadEnd.restricted),
"Fortress Grave Path Dusty Entrance Region": RegionInfo("Fortress Reliquary"),
- "Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary"),
+ "Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary", outlet_region="Fortress Grave Path"),
"Fortress Leaf Piles": RegionInfo("Dusty", dead_end=DeadEnd.all_cats),
"Fortress Arena": RegionInfo("Fortress Arena"),
- "Fortress Arena Portal": RegionInfo("Fortress Arena"),
+ "Fortress Arena Portal": RegionInfo("Fortress Arena", outlet_region="Fortress Arena"),
"Lower Mountain": RegionInfo("Mountain"),
"Lower Mountain Stairs": RegionInfo("Mountain"),
"Top of the Mountain": RegionInfo("Mountaintop", dead_end=DeadEnd.all_cats),
"Quarry Connector": RegionInfo("Darkwoods Tunnel"),
"Quarry Entry": RegionInfo("Quarry Redux"),
"Quarry": RegionInfo("Quarry Redux"),
- "Quarry Portal": RegionInfo("Quarry Redux"),
+ "Quarry Portal": RegionInfo("Quarry Redux", outlet_region="Quarry Entry"),
"Quarry Back": RegionInfo("Quarry Redux"),
"Quarry Monastery Entry": RegionInfo("Quarry Redux"),
"Monastery Front": RegionInfo("Monastery"),
"Monastery Back": RegionInfo("Monastery"),
- "Monastery Hero's Grave Region": RegionInfo("Monastery"),
+ "Monastery Hero's Grave Region": RegionInfo("Monastery", outlet_region="Monastery Back"),
"Monastery Rope": RegionInfo("Quarry Redux"),
"Lower Quarry": RegionInfo("Quarry Redux"),
"Even Lower Quarry": RegionInfo("Quarry Redux"),
@@ -691,19 +706,21 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Rooted Ziggurat Middle Bottom": RegionInfo("ziggurat2020_2"),
"Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the vanilla entry point side
"Rooted Ziggurat Lower Back": RegionInfo("ziggurat2020_3"), # the boss side
- "Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special), # the exit from zig skip, for use with fixed shop on
- "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3"), # the door itself on the zig 3 side
- "Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom"),
+ "Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Front"), # the exit from zig skip, 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"),
"Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom"),
"Swamp Front": RegionInfo("Swamp Redux 2"), # from the main entry to the top of the ladder after south
"Swamp Mid": RegionInfo("Swamp Redux 2"), # from the bottom of the ladder to the cathedral door
"Swamp Ledge under Cathedral Door": RegionInfo("Swamp Redux 2"), # the ledge with the chest and secret door
- "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2"), # just the door
+ "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2", outlet_region="Swamp Ledge under Cathedral Door"), # just the door
"Swamp to Cathedral Main Entrance Region": RegionInfo("Swamp Redux 2"), # just the door
"Back of Swamp": RegionInfo("Swamp Redux 2"), # the area with hero grave and gauntlet entrance
- "Swamp Hero's Grave Region": RegionInfo("Swamp Redux 2"),
+ "Swamp Hero's Grave Region": RegionInfo("Swamp Redux 2", outlet_region="Back of Swamp"),
"Back of Swamp Laurels Area": RegionInfo("Swamp Redux 2"), # the spots you need laurels to traverse
"Cathedral": RegionInfo("Cathedral Redux"),
+ "Cathedral to Gauntlet": RegionInfo("Cathedral Redux"), # the elevator
"Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats),
"Cathedral Gauntlet Checkpoint": RegionInfo("Cathedral Arena"),
"Cathedral Gauntlet": RegionInfo("Cathedral Arena"),
@@ -711,10 +728,10 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Far Shore": RegionInfo("Transit"),
"Far Shore to Spawn Region": RegionInfo("Transit"),
"Far Shore to East Forest Region": RegionInfo("Transit"),
- "Far Shore to Quarry Region": RegionInfo("Transit"),
- "Far Shore to Fortress Region": RegionInfo("Transit"),
- "Far Shore to Library Region": RegionInfo("Transit"),
- "Far Shore to West Garden Region": RegionInfo("Transit"),
+ "Far Shore to Quarry Region": RegionInfo("Transit", outlet_region="Far Shore"),
+ "Far Shore to Fortress Region": RegionInfo("Transit", outlet_region="Far Shore"),
+ "Far Shore to Library Region": RegionInfo("Transit", outlet_region="Far Shore"),
+ "Far Shore to West Garden Region": RegionInfo("Transit", outlet_region="Far Shore"),
"Hero Relic - Fortress": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats),
"Hero Relic - Quarry": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats),
"Hero Relic - West Garden": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats),
@@ -728,6 +745,16 @@ tunic_er_regions: Dict[str, RegionInfo] = {
}
+# this is essentially a pared down version of the region connections in rules.py, with some minor differences
+# the main purpose of this is to make it so that you can access every region
+# most items are excluded from the rules here, since we can assume Archipelago will properly place them
+# laurels (hyperdash) can be locked at 10 fairies, requiring access to secret gathering place
+# so until secret gathering place has been paired, you do not have hyperdash, so you cannot use hyperdash entrances
+# Zip means you need the laurels zips option enabled
+# IG# refers to ice grappling difficulties
+# LS# refers to ladder storage difficulties
+# LS rules are used for region connections here regardless of whether you have being knocked out of the air in logic
+# this is because it just means you can reach the entrances in that region via ladder storage
traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Overworld": {
"Overworld Beach":
@@ -735,13 +762,13 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Overworld to Atoll Upper":
[["Hyperdash"]],
"Overworld Belltower":
- [["Hyperdash"], ["UR"]],
+ [["Hyperdash"], ["LS1"]],
"Overworld Swamp Upper Entry":
- [["Hyperdash"], ["UR"]],
+ [["Hyperdash"], ["LS1"]],
"Overworld Swamp Lower Entry":
[],
"Overworld Special Shop Entry":
- [["Hyperdash"], ["UR"]],
+ [["Hyperdash"], ["LS1"]],
"Overworld Well Ladder":
[],
"Overworld Ruined Passage Door":
@@ -759,11 +786,11 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Overworld after Envoy":
[],
"Overworld Quarry Entry":
- [["NMG"]],
+ [["IG2"], ["LS1"]],
"Overworld Tunnel Turret":
- [["NMG"], ["Hyperdash"]],
+ [["IG1"], ["LS1"], ["Hyperdash"]],
"Overworld Temple Door":
- [["NMG"], ["Forest Belltower Upper", "Overworld Belltower"]],
+ [["IG2"], ["LS3"], ["Forest Belltower Upper", "Overworld Belltower"]],
"Overworld Southeast Cross Door":
[],
"Overworld Fountain Cross Door":
@@ -773,25 +800,28 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Overworld Spawn Portal":
[],
"Overworld Well to Furnace Rail":
- [["UR"]],
+ [["LS2"]],
"Overworld Old House Door":
[],
"Cube Cave Entrance Region":
[],
+ # drop a rudeling, icebolt or ice bomb
+ "Overworld to West Garden from Furnace":
+ [["IG3"]],
},
"East Overworld": {
"Above Ruined Passage":
[],
"After Ruined Passage":
- [["NMG"]],
- "Overworld":
- [],
+ [["IG1"], ["LS1"]],
+ # "Overworld":
+ # [],
"Overworld at Patrol Cave":
[],
"Overworld above Patrol Cave":
[],
"Overworld Special Shop Entry":
- [["Hyperdash"], ["UR"]]
+ [["Hyperdash"], ["LS1"]]
},
"Overworld Special Shop Entry": {
"East Overworld":
@@ -800,8 +830,8 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Overworld Belltower": {
"Overworld Belltower at Bell":
[],
- "Overworld":
- [],
+ # "Overworld":
+ # [],
"Overworld to West Garden Upper":
[],
},
@@ -809,19 +839,19 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Overworld Belltower":
[],
},
- "Overworld Swamp Upper Entry": {
- "Overworld":
- [],
- },
- "Overworld Swamp Lower Entry": {
- "Overworld":
- [],
- },
+ # "Overworld Swamp Upper Entry": {
+ # "Overworld":
+ # [],
+ # },
+ # "Overworld Swamp Lower Entry": {
+ # "Overworld":
+ # [],
+ # },
"Overworld Beach": {
- "Overworld":
- [],
+ # "Overworld":
+ # [],
"Overworld West Garden Laurels Entry":
- [["Hyperdash"]],
+ [["Hyperdash"], ["LS1"]],
"Overworld to Atoll Upper":
[],
"Overworld Tunnel Turret":
@@ -832,38 +862,37 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
[["Hyperdash"]],
},
"Overworld to Atoll Upper": {
- "Overworld":
- [],
+ # "Overworld":
+ # [],
"Overworld Beach":
[],
},
"Overworld Tunnel Turret": {
- "Overworld":
- [],
+ # "Overworld":
+ # [],
"Overworld Beach":
[],
},
"Overworld Well Ladder": {
- "Overworld":
- [],
+ # "Overworld":
+ # [],
},
"Overworld at Patrol Cave": {
"East Overworld":
- [["Hyperdash"]],
+ [["Hyperdash"], ["LS1"], ["IG1"]],
"Overworld above Patrol Cave":
[],
},
"Overworld above Patrol Cave": {
- "Overworld":
- [],
+ # "Overworld":
+ # [],
"East Overworld":
[],
"Upper Overworld":
[],
"Overworld at Patrol Cave":
[],
- "Overworld Belltower at Bell":
- [["NMG"]],
+ # readd long dong if we ever do a misc tricks option
},
"Upper Overworld": {
"Overworld above Patrol Cave":
@@ -878,51 +907,49 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
[],
},
"Overworld above Quarry Entrance": {
- "Overworld":
- [],
+ # "Overworld":
+ # [],
"Upper Overworld":
[],
},
"Overworld Quarry Entry": {
"Overworld after Envoy":
[],
- "Overworld":
- [["NMG"]],
+ # "Overworld":
+ # [["IG1"]],
},
"Overworld after Envoy": {
- "Overworld":
- [],
+ # "Overworld":
+ # [],
"Overworld Quarry Entry":
[],
},
"After Ruined Passage": {
- "Overworld":
- [],
+ # "Overworld":
+ # [],
"Above Ruined Passage":
[],
- "East Overworld":
- [["NMG"]],
},
"Above Ruined Passage": {
- "Overworld":
- [],
+ # "Overworld":
+ # [],
"After Ruined Passage":
[],
"East Overworld":
[],
},
- "Overworld Ruined Passage Door": {
- "Overworld":
- [["Hyperdash", "NMG"]],
- },
- "Overworld Town Portal": {
- "Overworld":
- [],
- },
- "Overworld Spawn Portal": {
- "Overworld":
- [],
- },
+ # "Overworld Ruined Passage Door": {
+ # "Overworld":
+ # [["Hyperdash", "Zip"]],
+ # },
+ # "Overworld Town Portal": {
+ # "Overworld":
+ # [],
+ # },
+ # "Overworld Spawn Portal": {
+ # "Overworld":
+ # [],
+ # },
"Cube Cave Entrance Region": {
"Overworld":
[],
@@ -933,7 +960,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"Old House Back": {
"Old House Front":
- [["Hyperdash", "NMG"]],
+ [["Hyperdash", "Zip"]],
},
"Furnace Fuse": {
"Furnace Ladder Area":
@@ -941,9 +968,9 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"Furnace Ladder Area": {
"Furnace Fuse":
- [["Hyperdash"], ["UR"]],
+ [["Hyperdash"], ["LS1"]],
"Furnace Walking Path":
- [["Hyperdash"], ["UR"]],
+ [["Hyperdash"], ["LS1"]],
},
"Furnace Walking Path": {
"Furnace Ladder Area":
@@ -971,7 +998,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"East Forest": {
"East Forest Dance Fox Spot":
- [["Hyperdash"], ["NMG"]],
+ [["Hyperdash"], ["IG1"], ["LS1"]],
"East Forest Portal":
[],
"Lower Forest":
@@ -979,7 +1006,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"East Forest Dance Fox Spot": {
"East Forest":
- [["Hyperdash"], ["NMG"]],
+ [["Hyperdash"], ["IG1"]],
},
"East Forest Portal": {
"East Forest":
@@ -995,7 +1022,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"Guard House 1 West": {
"Guard House 1 East":
- [["Hyperdash"], ["UR"]],
+ [["Hyperdash"], ["LS1"]],
},
"Guard House 2 Upper": {
"Guard House 2 Lower":
@@ -1007,19 +1034,19 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"Forest Grave Path Main": {
"Forest Grave Path Upper":
- [["Hyperdash"], ["UR"]],
+ [["Hyperdash"], ["LS2"], ["IG3"]],
"Forest Grave Path by Grave":
[],
},
"Forest Grave Path Upper": {
"Forest Grave Path Main":
- [["Hyperdash"], ["NMG"]],
+ [["Hyperdash"], ["IG1"]],
},
"Forest Grave Path by Grave": {
"Forest Hero's Grave":
[],
"Forest Grave Path Main":
- [["NMG"]],
+ [["IG1"]],
},
"Forest Hero's Grave": {
"Forest Grave Path by Grave":
@@ -1051,7 +1078,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"Dark Tomb Checkpoint": {
"Well Boss":
- [["Hyperdash", "NMG"]],
+ [["Hyperdash", "Zip"]],
},
"Dark Tomb Entry Point": {
"Dark Tomb Upper":
@@ -1075,13 +1102,13 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"West Garden": {
"West Garden Laurels Exit Region":
- [["Hyperdash"], ["UR"]],
+ [["Hyperdash"], ["LS1"]],
"West Garden after Boss":
[],
"West Garden Hero's Grave Region":
[],
"West Garden Portal Item":
- [["NMG"]],
+ [["IG2"]],
},
"West Garden Laurels Exit Region": {
"West Garden":
@@ -1093,13 +1120,19 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"West Garden Portal Item": {
"West Garden":
- [["NMG"]],
- "West Garden Portal":
- [["Hyperdash", "West Garden"]],
+ [["IG1"]],
+ "West Garden by Portal":
+ [["Hyperdash"]],
},
- "West Garden Portal": {
+ "West Garden by Portal": {
"West Garden Portal Item":
[["Hyperdash"]],
+ "West Garden Portal":
+ [["West Garden"]],
+ },
+ "West Garden Portal": {
+ "West Garden by Portal":
+ [],
},
"West Garden Hero's Grave Region": {
"West Garden":
@@ -1107,7 +1140,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"Ruined Atoll": {
"Ruined Atoll Lower Entry Area":
- [["Hyperdash"], ["UR"]],
+ [["Hyperdash"], ["LS1"]],
"Ruined Atoll Ladder Tops":
[],
"Ruined Atoll Frog Mouth":
@@ -1174,11 +1207,17 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
[],
},
"Library Exterior Ladder Region": {
+ "Library Exterior by Tree":
+ [],
+ },
+ "Library Exterior by Tree": {
"Library Exterior Tree Region":
[],
+ "Library Exterior Ladder Region":
+ [],
},
"Library Exterior Tree Region": {
- "Library Exterior Ladder Region":
+ "Library Exterior by Tree":
[],
},
"Library Hall Bookshelf": {
@@ -1223,15 +1262,21 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Library Lab": {
"Library Lab Lower":
[["Hyperdash"]],
- "Library Portal":
+ "Library Lab on Portal Pad":
[],
"Library Lab to Librarian":
[],
},
- "Library Portal": {
+ "Library Lab on Portal Pad": {
+ "Library Portal":
+ [],
"Library Lab":
[],
},
+ "Library Portal": {
+ "Library Lab on Portal Pad":
+ [],
+ },
"Library Lab to Librarian": {
"Library Lab":
[],
@@ -1240,11 +1285,9 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Fortress Exterior from Overworld":
[],
"Fortress Courtyard Upper":
- [["UR"]],
- "Fortress Exterior near cave":
- [["UR"]],
+ [["LS2"]],
"Fortress Courtyard":
- [["UR"]],
+ [["LS1"]],
},
"Fortress Exterior from Overworld": {
"Fortress Exterior from East Forest":
@@ -1252,15 +1295,15 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Fortress Exterior near cave":
[],
"Fortress Courtyard":
- [["Hyperdash"], ["NMG"]],
+ [["Hyperdash"], ["IG1"], ["LS1"]],
},
"Fortress Exterior near cave": {
"Fortress Exterior from Overworld":
- [["Hyperdash"], ["UR"]],
- "Fortress Courtyard":
- [["UR"]],
+ [["Hyperdash"], ["LS1"]],
+ "Fortress Courtyard": # ice grapple hard: shoot far fire pot, it aggros one of the enemies over to you
+ [["IG3"], ["LS1"]],
"Fortress Courtyard Upper":
- [["UR"]],
+ [["LS2"]],
"Beneath the Vault Entry":
[],
},
@@ -1270,7 +1313,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"Fortress Courtyard": {
"Fortress Courtyard Upper":
- [["NMG"]],
+ [["IG1"]],
"Fortress Exterior from Overworld":
[["Hyperdash"]],
},
@@ -1296,7 +1339,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"Fortress East Shortcut Lower": {
"Fortress East Shortcut Upper":
- [["NMG"]],
+ [["IG1"]],
},
"Fortress East Shortcut Upper": {
"Fortress East Shortcut Lower":
@@ -1304,11 +1347,11 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"Eastern Vault Fortress": {
"Eastern Vault Fortress Gold Door":
- [["NMG"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]],
+ [["IG2"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]],
},
"Eastern Vault Fortress Gold Door": {
"Eastern Vault Fortress":
- [["NMG"]],
+ [["IG1"]],
},
"Fortress Grave Path": {
"Fortress Hero's Grave Region":
@@ -1318,7 +1361,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"Fortress Grave Path Upper": {
"Fortress Grave Path":
- [["NMG"]],
+ [["IG1"]],
},
"Fortress Grave Path Dusty Entrance Region": {
"Fortress Grave Path":
@@ -1346,7 +1389,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"Monastery Back": {
"Monastery Front":
- [["Hyperdash", "NMG"]],
+ [["Hyperdash", "Zip"]],
"Monastery Hero's Grave Region":
[],
},
@@ -1363,6 +1406,8 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
[["Quarry Connector"]],
"Quarry":
[],
+ "Monastery Rope":
+ [["LS2"]],
},
"Quarry Portal": {
"Quarry Entry":
@@ -1374,7 +1419,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Quarry Back":
[["Hyperdash"]],
"Monastery Rope":
- [["UR"]],
+ [["LS2"]],
},
"Quarry Back": {
"Quarry":
@@ -1392,7 +1437,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Quarry Monastery Entry":
[],
"Lower Quarry Zig Door":
- [["NMG"]],
+ [["IG3"]],
},
"Lower Quarry": {
"Even Lower Quarry":
@@ -1402,7 +1447,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Lower Quarry":
[],
"Lower Quarry Zig Door":
- [["Quarry", "Quarry Connector"], ["NMG"]],
+ [["Quarry", "Quarry Connector"], ["IG3"]],
},
"Monastery Rope": {
"Quarry Back":
@@ -1430,7 +1475,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"Rooted Ziggurat Lower Back": {
"Rooted Ziggurat Lower Front":
- [["Hyperdash"], ["UR"]],
+ [["Hyperdash"], ["LS2"], ["IG1"]],
"Rooted Ziggurat Portal Room Entrance":
[],
},
@@ -1443,26 +1488,35 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
[],
},
"Rooted Ziggurat Portal Room Exit": {
- "Rooted Ziggurat Portal":
+ "Rooted Ziggurat Portal Room":
[],
},
- "Rooted Ziggurat Portal": {
+ "Rooted Ziggurat Portal Room": {
+ "Rooted Ziggurat Portal":
+ [],
"Rooted Ziggurat Portal Room Exit":
[["Rooted Ziggurat Lower Back"]],
},
+ "Rooted Ziggurat Portal": {
+ "Rooted Ziggurat Portal Room":
+ [],
+ },
"Swamp Front": {
"Swamp Mid":
[],
+ # get one pillar from the gate, then dash onto the gate, very tricky
+ "Back of Swamp Laurels Area":
+ [["Hyperdash", "Zip"]],
},
"Swamp Mid": {
"Swamp Front":
[],
"Swamp to Cathedral Main Entrance Region":
- [["Hyperdash"], ["NMG"]],
+ [["Hyperdash"], ["IG2"], ["LS3"]],
"Swamp Ledge under Cathedral Door":
[],
"Back of Swamp":
- [["UR"]],
+ [["LS1"]], # ig3 later?
},
"Swamp Ledge under Cathedral Door": {
"Swamp Mid":
@@ -1476,24 +1530,41 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
},
"Swamp to Cathedral Main Entrance Region": {
"Swamp Mid":
- [["NMG"]],
+ [["IG1"]],
},
"Back of Swamp": {
"Back of Swamp Laurels Area":
- [["Hyperdash"], ["UR"]],
+ [["Hyperdash"], ["LS2"]],
"Swamp Hero's Grave Region":
[],
+ "Swamp Mid":
+ [["LS2"]],
+ "Swamp Front":
+ [["LS1"]],
+ "Swamp to Cathedral Main Entrance Region":
+ [["LS3"]],
+ "Swamp to Cathedral Treasure Room":
+ [["LS3"]]
},
"Back of Swamp Laurels Area": {
"Back of Swamp":
[["Hyperdash"]],
+ # get one pillar from the gate, then dash onto the gate, very tricky
"Swamp Mid":
- [["NMG", "Hyperdash"]],
+ [["IG1", "Hyperdash"], ["Hyperdash", "Zip"]],
},
"Swamp Hero's Grave Region": {
"Back of Swamp":
[],
},
+ "Cathedral": {
+ "Cathedral to Gauntlet":
+ [],
+ },
+ "Cathedral to Gauntlet": {
+ "Cathedral":
+ [],
+ },
"Cathedral Gauntlet Checkpoint": {
"Cathedral Gauntlet":
[],
diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py
index 3d1973beb3..65175e41ca 100644
--- a/worlds/tunic/er_rules.py
+++ b/worlds/tunic/er_rules.py
@@ -1,8 +1,10 @@
-from typing import Dict, Set, List, Tuple, TYPE_CHECKING
+from typing import Dict, FrozenSet, Tuple, TYPE_CHECKING
from worlds.generic.Rules import set_rule, forbid_item
+from .options import IceGrappling, LadderStorage
from .rules import (has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage,
- bomb_walls)
-from .er_data import Portal
+ laurels_zip, bomb_walls)
+from .er_data import Portal, get_portal_outlet_region
+from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls
from BaseClasses import Region, CollectionState
if TYPE_CHECKING:
@@ -82,13 +84,16 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Overworld"].connect(
connecting_region=regions["Overworld Belltower"],
- rule=lambda state: state.has(laurels, player))
+ rule=lambda state: state.has(laurels, player)
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
regions["Overworld Belltower"].connect(
connecting_region=regions["Overworld"])
+ # ice grapple rudeling across rubble, drop bridge, ice grapple rudeling down
regions["Overworld Belltower"].connect(
connecting_region=regions["Overworld to West Garden Upper"],
- rule=lambda state: has_ladder("Ladders to West Bell", state, world))
+ rule=lambda state: has_ladder("Ladders to West Bell", state, world)
+ or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
regions["Overworld to West Garden Upper"].connect(
connecting_region=regions["Overworld Belltower"],
rule=lambda state: has_ladder("Ladders to West Bell", state, world))
@@ -97,32 +102,35 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["Overworld Belltower at Bell"],
rule=lambda state: has_ladder("Ladders to West Bell", state, world))
- # long dong, do not make a reverse connection here or to belltower
- regions["Overworld above Patrol Cave"].connect(
- connecting_region=regions["Overworld Belltower at Bell"],
- rule=lambda state: options.logic_rules and state.has(fire_wand, player))
+ # long dong, do not make a reverse connection here or to belltower, maybe readd later
+ # regions["Overworld above Patrol Cave"].connect(
+ # connecting_region=regions["Overworld Belltower at Bell"],
+ # rule=lambda state: options.logic_rules and state.has(fire_wand, player))
- # nmg: can laurels through the ruined passage door
+ # can laurels through the ruined passage door at either corner
regions["Overworld"].connect(
connecting_region=regions["Overworld Ruined Passage Door"],
rule=lambda state: state.has(key, player, 2)
- or (state.has(laurels, player) and options.logic_rules))
+ or laurels_zip(state, world))
regions["Overworld Ruined Passage Door"].connect(
connecting_region=regions["Overworld"],
- rule=lambda state: state.has(laurels, player) and options.logic_rules)
+ rule=lambda state: laurels_zip(state, world))
regions["Overworld"].connect(
connecting_region=regions["After Ruined Passage"],
rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["After Ruined Passage"].connect(
connecting_region=regions["Overworld"],
rule=lambda state: has_ladder("Ladders near Weathervane", state, world))
+ # for the hard ice grapple, get to the chest after the bomb wall, grab a slime, and grapple push down
+ # you can ice grapple through the bomb wall, so no need for shop logic checking
regions["Overworld"].connect(
connecting_region=regions["Above Ruined Passage"],
rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
- or state.has(laurels, player))
+ or state.has(laurels, player)
+ or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
regions["Above Ruined Passage"].connect(
connecting_region=regions["Overworld"],
rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
@@ -138,7 +146,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Above Ruined Passage"].connect(
connecting_region=regions["East Overworld"],
rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["East Overworld"].connect(
connecting_region=regions["Above Ruined Passage"],
rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
@@ -147,15 +155,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
# nmg: ice grapple the slimes, works both ways consistently
regions["East Overworld"].connect(
connecting_region=regions["After Ruined Passage"],
- rule=lambda state: has_ice_grapple_logic(True, state, world))
+ rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["After Ruined Passage"].connect(
connecting_region=regions["East Overworld"],
- rule=lambda state: has_ice_grapple_logic(True, state, world))
+ rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Overworld"].connect(
connecting_region=regions["East Overworld"],
rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["East Overworld"].connect(
connecting_region=regions["Overworld"],
rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world))
@@ -169,7 +177,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Overworld at Patrol Cave"].connect(
connecting_region=regions["Overworld above Patrol Cave"],
rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Overworld above Patrol Cave"].connect(
connecting_region=regions["Overworld at Patrol Cave"],
rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world))
@@ -185,7 +193,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["East Overworld"].connect(
connecting_region=regions["Overworld above Patrol Cave"],
rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Overworld above Patrol Cave"].connect(
connecting_region=regions["East Overworld"],
rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world))
@@ -193,7 +201,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Overworld above Patrol Cave"].connect(
connecting_region=regions["Upper Overworld"],
rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Upper Overworld"].connect(
connecting_region=regions["Overworld above Patrol Cave"],
rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world)
@@ -206,13 +214,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["Upper Overworld"],
rule=lambda state: state.has_any({grapple, laurels}, player))
+ # ice grapple push guard captain down the ledge
regions["Upper Overworld"].connect(
connecting_region=regions["Overworld after Temple Rafters"],
- rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world))
+ rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world)
+ or has_ice_grapple_logic(True, IceGrappling.option_medium, state, world))
regions["Overworld after Temple Rafters"].connect(
connecting_region=regions["Upper Overworld"],
rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Overworld above Quarry Entrance"].connect(
connecting_region=regions["Overworld"],
@@ -224,13 +234,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Overworld"].connect(
connecting_region=regions["Overworld after Envoy"],
rule=lambda state: state.has_any({laurels, grapple, gun}, player)
- or state.has("Sword Upgrade", player, 4)
- or options.logic_rules)
+ or state.has("Sword Upgrade", player, 4))
regions["Overworld after Envoy"].connect(
connecting_region=regions["Overworld"],
rule=lambda state: state.has_any({laurels, grapple, gun}, player)
- or state.has("Sword Upgrade", player, 4)
- or options.logic_rules)
+ or state.has("Sword Upgrade", player, 4))
regions["Overworld after Envoy"].connect(
connecting_region=regions["Overworld Quarry Entry"],
@@ -242,10 +250,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
# ice grapple through the gate
regions["Overworld"].connect(
connecting_region=regions["Overworld Quarry Entry"],
- rule=lambda state: has_ice_grapple_logic(False, state, world))
+ rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
regions["Overworld Quarry Entry"].connect(
connecting_region=regions["Overworld"],
- rule=lambda state: has_ice_grapple_logic(False, state, world))
+ rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world))
regions["Overworld"].connect(
connecting_region=regions["Overworld Swamp Upper Entry"],
@@ -256,7 +264,8 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Overworld"].connect(
connecting_region=regions["Overworld Swamp Lower Entry"],
- rule=lambda state: has_ladder("Ladder to Swamp", state, world))
+ rule=lambda state: has_ladder("Ladder to Swamp", state, world)
+ or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
regions["Overworld Swamp Lower Entry"].connect(
connecting_region=regions["Overworld"],
rule=lambda state: has_ladder("Ladder to Swamp", state, world))
@@ -279,20 +288,21 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Overworld"].connect(
connecting_region=regions["Overworld Old House Door"],
rule=lambda state: state.has(house_key, player)
- or has_ice_grapple_logic(False, state, world))
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
- # not including ice grapple through this because it's very tedious to get an enemy here
+ # lure enemy over and ice grapple through
regions["Overworld"].connect(
connecting_region=regions["Overworld Southeast Cross Door"],
- rule=lambda state: has_ability(holy_cross, state, world))
+ rule=lambda state: has_ability(holy_cross, state, world)
+ or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
regions["Overworld Southeast Cross Door"].connect(
connecting_region=regions["Overworld"],
rule=lambda state: has_ability(holy_cross, state, world))
- # not including ice grapple through this because we're not including it on the other door
regions["Overworld"].connect(
connecting_region=regions["Overworld Fountain Cross Door"],
- rule=lambda state: has_ability(holy_cross, state, world))
+ rule=lambda state: has_ability(holy_cross, state, world)
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
regions["Overworld Fountain Cross Door"].connect(
connecting_region=regions["Overworld"])
@@ -312,7 +322,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Overworld"].connect(
connecting_region=regions["Overworld Temple Door"],
rule=lambda state: state.has_all({"Ring Eastern Bell", "Ring Western Bell"}, player)
- or has_ice_grapple_logic(False, state, world))
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
regions["Overworld Temple Door"].connect(
connecting_region=regions["Overworld above Patrol Cave"],
@@ -325,12 +335,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Overworld Beach"].connect(
connecting_region=regions["Overworld Tunnel Turret"],
rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Overworld"].connect(
connecting_region=regions["Overworld Tunnel Turret"],
- rule=lambda state: state.has(laurels, player)
- or has_ice_grapple_logic(True, state, world))
+ 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))
@@ -341,13 +350,18 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Cube Cave Entrance Region"].connect(
connecting_region=regions["Overworld"])
+ # drop a rudeling down, icebolt or ice bomb
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld to West Garden from Furnace"],
+ rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world))
+
# Overworld side areas
regions["Old House Front"].connect(
connecting_region=regions["Old House Back"])
- # nmg: laurels through the gate
+ # laurels through the gate, use left wall to space yourself
regions["Old House Back"].connect(
connecting_region=regions["Old House Front"],
- rule=lambda state: state.has(laurels, player) and options.logic_rules)
+ rule=lambda state: laurels_zip(state, world))
regions["Sealed Temple"].connect(
connecting_region=regions["Sealed Temple Rafters"])
@@ -388,15 +402,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["Forest Belltower Lower"],
rule=lambda state: has_ladder("Ladder to East Forest", state, world))
- # nmg: ice grapple up to dance fox spot, and vice versa
+ # ice grapple up to dance fox spot, and vice versa
regions["East Forest"].connect(
connecting_region=regions["East Forest Dance Fox Spot"],
rule=lambda state: state.has(laurels, player)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["East Forest Dance Fox Spot"].connect(
connecting_region=regions["East Forest"],
rule=lambda state: state.has(laurels, player)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["East Forest"].connect(
connecting_region=regions["East Forest Portal"],
@@ -407,7 +421,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["East Forest"].connect(
connecting_region=regions["Lower Forest"],
rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)
- or (state.has_all({grapple, fire_wand, ice_dagger}, player) and has_ability(icebolt, state, world)))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Lower Forest"].connect(
connecting_region=regions["East Forest"],
rule=lambda state: has_ladder("Ladders to Lower Forest", state, world))
@@ -425,22 +439,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["Guard House 2 Upper"],
rule=lambda state: has_ladder("Ladders to Lower Forest", state, world))
- # nmg: ice grapple from upper grave path exit to the rest of it
+ # ice grapple from upper grave path exit to the rest of it
regions["Forest Grave Path Upper"].connect(
connecting_region=regions["Forest Grave Path Main"],
rule=lambda state: state.has(laurels, player)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
+ # for the ice grapple, lure a rudeling up top, then grapple push it across
regions["Forest Grave Path Main"].connect(
connecting_region=regions["Forest Grave Path Upper"],
- rule=lambda state: state.has(laurels, player))
+ rule=lambda state: state.has(laurels, player)
+ or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
regions["Forest Grave Path Main"].connect(
connecting_region=regions["Forest Grave Path by Grave"])
- # nmg: ice grapple or laurels through the gate
+ # ice grapple or laurels through the gate
regions["Forest Grave Path by Grave"].connect(
connecting_region=regions["Forest Grave Path Main"],
- rule=lambda state: has_ice_grapple_logic(False, state, world)
- or (state.has(laurels, player) and options.logic_rules))
+ rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)
+ or laurels_zip(state, world))
regions["Forest Grave Path by Grave"].connect(
connecting_region=regions["Forest Hero's Grave"],
@@ -473,10 +489,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Well Boss"].connect(
connecting_region=regions["Dark Tomb Checkpoint"])
- # nmg: can laurels through the gate
+ # can laurels through the gate, no setup needed
regions["Dark Tomb Checkpoint"].connect(
connecting_region=regions["Well Boss"],
- rule=lambda state: state.has(laurels, player) and options.logic_rules)
+ rule=lambda state: laurels_zip(state, world))
regions["Dark Tomb Entry Point"].connect(
connecting_region=regions["Dark Tomb Upper"],
@@ -505,12 +521,16 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["West Garden Laurels Exit Region"],
rule=lambda state: state.has(laurels, player))
+ # you can grapple Garden Knight to aggro it, then ledge it
regions["West Garden after Boss"].connect(
connecting_region=regions["West Garden"],
- rule=lambda state: state.has(laurels, player))
+ rule=lambda state: state.has(laurels, player)
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
+ # ice grapple push Garden Knight off the side
regions["West Garden"].connect(
connecting_region=regions["West Garden after Boss"],
- rule=lambda state: state.has(laurels, player) or has_sword(state, player))
+ rule=lambda state: state.has(laurels, player) or has_sword(state, player)
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
regions["West Garden"].connect(
connecting_region=regions["West Garden Hero's Grave Region"],
@@ -519,26 +539,32 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["West Garden"])
regions["West Garden Portal"].connect(
+ connecting_region=regions["West Garden by Portal"])
+ regions["West Garden by Portal"].connect(
+ connecting_region=regions["West Garden Portal"],
+ rule=lambda state: has_ability(prayer, state, world) and state.has("Activate West Garden Fuse", player))
+
+ regions["West Garden by Portal"].connect(
connecting_region=regions["West Garden Portal Item"],
rule=lambda state: state.has(laurels, player))
regions["West Garden Portal Item"].connect(
- connecting_region=regions["West Garden Portal"],
- rule=lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
+ connecting_region=regions["West Garden by Portal"],
+ rule=lambda state: state.has(laurels, player))
- # nmg: can ice grapple to and from the item behind the magic dagger house
+ # can ice grapple to and from the item behind the magic dagger house
regions["West Garden Portal Item"].connect(
connecting_region=regions["West Garden"],
- rule=lambda state: has_ice_grapple_logic(True, state, world))
+ rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["West Garden"].connect(
connecting_region=regions["West Garden Portal Item"],
- rule=lambda state: has_ice_grapple_logic(True, state, world))
+ rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_medium, state, world))
# Atoll and Frog's Domain
- # nmg: ice grapple the bird below the portal
+ # ice grapple the bird below the portal
regions["Ruined Atoll"].connect(
connecting_region=regions["Ruined Atoll Lower Entry Area"],
rule=lambda state: state.has(laurels, player)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Ruined Atoll Lower Entry Area"].connect(
connecting_region=regions["Ruined Atoll"],
rule=lambda state: state.has(laurels, player) or state.has(grapple, player))
@@ -570,13 +596,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Ruined Atoll"].connect(
connecting_region=regions["Ruined Atoll Statue"],
rule=lambda state: has_ability(prayer, state, world)
- and has_ladder("Ladders in South Atoll", state, world))
+ and (has_ladder("Ladders in South Atoll", state, world)
+ # shoot fuse and have the shot hit you mid-LS
+ or (can_ladder_storage(state, world) and state.has(fire_wand, player)
+ and options.ladder_storage >= LadderStorage.option_hard)))
regions["Ruined Atoll Statue"].connect(
connecting_region=regions["Ruined Atoll"])
regions["Frog Stairs Eye Exit"].connect(
connecting_region=regions["Frog Stairs Upper"],
- rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
+ rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Frog Stairs Upper"].connect(
connecting_region=regions["Frog Stairs Eye Exit"],
rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
@@ -605,14 +635,19 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
# Library
regions["Library Exterior Tree Region"].connect(
+ connecting_region=regions["Library Exterior by Tree"])
+ regions["Library Exterior by Tree"].connect(
+ connecting_region=regions["Library Exterior Tree Region"],
+ rule=lambda state: has_ability(prayer, state, world))
+
+ regions["Library Exterior by Tree"].connect(
connecting_region=regions["Library Exterior Ladder Region"],
rule=lambda state: state.has_any({grapple, laurels}, player)
and has_ladder("Ladders in Library", state, world))
regions["Library Exterior Ladder Region"].connect(
- connecting_region=regions["Library Exterior Tree Region"],
- rule=lambda state: has_ability(prayer, state, world)
- and ((state.has(laurels, player) and has_ladder("Ladders in Library", state, world))
- or state.has(grapple, player)))
+ connecting_region=regions["Library Exterior by Tree"],
+ rule=lambda state: state.has(grapple, player)
+ or (state.has(laurels, player) and has_ladder("Ladders in Library", state, world)))
regions["Library Hall Bookshelf"].connect(
connecting_region=regions["Library Hall"],
@@ -658,14 +693,19 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
and has_ladder("Ladders in Library", state, world))
regions["Library Lab"].connect(
- connecting_region=regions["Library Portal"],
- rule=lambda state: has_ability(prayer, state, world)
- and has_ladder("Ladders in Library", state, world))
- regions["Library Portal"].connect(
+ connecting_region=regions["Library Lab on Portal Pad"],
+ rule=lambda state: has_ladder("Ladders in Library", state, world))
+ regions["Library Lab on Portal Pad"].connect(
connecting_region=regions["Library Lab"],
rule=lambda state: has_ladder("Ladders in Library", state, world)
or state.has(laurels, player))
+ regions["Library Lab on Portal Pad"].connect(
+ connecting_region=regions["Library Portal"],
+ rule=lambda state: has_ability(prayer, state, world))
+ regions["Library Portal"].connect(
+ connecting_region=regions["Library Lab on Portal Pad"])
+
regions["Library Lab"].connect(
connecting_region=regions["Library Lab to Librarian"],
rule=lambda state: has_ladder("Ladders in Library", state, world))
@@ -688,6 +728,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["Fortress Exterior near cave"],
rule=lambda state: state.has(laurels, player) or has_ability(prayer, state, world))
+ # shoot far fire pot, enemy gets aggro'd
+ regions["Fortress Exterior near cave"].connect(
+ connecting_region=regions["Fortress Courtyard"],
+ rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world))
+
regions["Fortress Exterior near cave"].connect(
connecting_region=regions["Beneath the Vault Entry"],
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world))
@@ -702,14 +747,14 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Fortress Exterior from Overworld"].connect(
connecting_region=regions["Fortress Courtyard"],
rule=lambda state: state.has(laurels, player)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Fortress Courtyard Upper"].connect(
connecting_region=regions["Fortress Courtyard"])
# nmg: can ice grapple to the upper ledge
regions["Fortress Courtyard"].connect(
connecting_region=regions["Fortress Courtyard Upper"],
- rule=lambda state: has_ice_grapple_logic(True, state, world))
+ rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Fortress Courtyard Upper"].connect(
connecting_region=regions["Fortress Exterior from Overworld"])
@@ -733,17 +778,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
# nmg: can ice grapple upwards
regions["Fortress East Shortcut Lower"].connect(
connecting_region=regions["Fortress East Shortcut Upper"],
- rule=lambda state: has_ice_grapple_logic(True, state, world))
+ rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
# nmg: ice grapple through the big gold door, can do it both ways
regions["Eastern Vault Fortress"].connect(
connecting_region=regions["Eastern Vault Fortress Gold Door"],
rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses",
"Activate Eastern Vault East Fuse"}, player)
- or has_ice_grapple_logic(False, state, world))
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
regions["Eastern Vault Fortress Gold Door"].connect(
connecting_region=regions["Eastern Vault Fortress"],
- rule=lambda state: has_ice_grapple_logic(True, state, world))
+ rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world))
regions["Fortress Grave Path"].connect(
connecting_region=regions["Fortress Grave Path Dusty Entrance Region"],
@@ -761,7 +806,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
# nmg: ice grapple from upper grave path to lower
regions["Fortress Grave Path Upper"].connect(
connecting_region=regions["Fortress Grave Path"],
- rule=lambda state: has_ice_grapple_logic(True, state, world))
+ rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Fortress Arena"].connect(
connecting_region=regions["Fortress Arena Portal"],
@@ -819,25 +864,25 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Lower Quarry"].connect(
connecting_region=regions["Even Lower Quarry"],
rule=lambda state: has_ladder("Ladders in Lower Quarry", state, world)
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
# nmg: bring a scav over, then ice grapple through the door, only with ER on to avoid soft lock
regions["Even Lower Quarry"].connect(
connecting_region=regions["Lower Quarry Zig Door"],
rule=lambda state: state.has("Activate Quarry Fuse", player)
- or (has_ice_grapple_logic(False, state, world) and options.entrance_rando))
+ or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
# nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask only with ER on
regions["Quarry"].connect(
connecting_region=regions["Lower Quarry Zig Door"],
- rule=lambda state: has_ice_grapple_logic(True, state, world) and options.entrance_rando)
+ rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world))
regions["Monastery Front"].connect(
connecting_region=regions["Monastery Back"])
- # nmg: can laurels through the gate
+ # laurels through the gate, no setup needed
regions["Monastery Back"].connect(
connecting_region=regions["Monastery Front"],
- rule=lambda state: state.has(laurels, player) and options.logic_rules)
+ rule=lambda state: laurels_zip(state, world))
regions["Monastery Back"].connect(
connecting_region=regions["Monastery Hero's Grave Region"],
@@ -863,14 +908,13 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["Rooted Ziggurat Lower Back"],
rule=lambda state: state.has(laurels, player)
or (has_sword(state, player) and has_ability(prayer, state, world)))
- # unrestricted: use ladder storage to get to the front, get hit by one of the many enemies
# nmg: can ice grapple on the voidlings to the double admin fight, still need to pray at the fuse
regions["Rooted Ziggurat Lower Back"].connect(
connecting_region=regions["Rooted Ziggurat Lower Front"],
- rule=lambda state: ((state.has(laurels, player) or has_ice_grapple_logic(True, state, world))
- and has_ability(prayer, state, world)
- and has_sword(state, player))
- or can_ladder_storage(state, world))
+ rule=lambda state: (state.has(laurels, player)
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
+ and has_ability(prayer, state, world)
+ and has_sword(state, player))
regions["Rooted Ziggurat Lower Back"].connect(
connecting_region=regions["Rooted Ziggurat Portal Room Entrance"],
@@ -882,40 +926,62 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
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(
+ connecting_region=regions["Rooted Ziggurat Portal"],
+ rule=lambda state: has_ability(prayer, state, world))
+
+ regions["Rooted Ziggurat Portal Room"].connect(
connecting_region=regions["Rooted Ziggurat Portal Room Exit"],
rule=lambda state: state.has("Activate Ziggurat Fuse", player))
regions["Rooted Ziggurat Portal Room Exit"].connect(
- connecting_region=regions["Rooted Ziggurat Portal"],
- rule=lambda state: has_ability(prayer, state, world))
+ connecting_region=regions["Rooted Ziggurat Portal Room"])
# Swamp and Cathedral
regions["Swamp Front"].connect(
connecting_region=regions["Swamp Mid"],
rule=lambda state: has_ladder("Ladders in Swamp", state, world)
or state.has(laurels, player)
- or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate
+ or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
regions["Swamp Mid"].connect(
connecting_region=regions["Swamp Front"],
rule=lambda state: has_ladder("Ladders in Swamp", state, world)
or state.has(laurels, player)
- or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate
+ or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
- # nmg: ice grapple through cathedral door, can do it both ways
- regions["Swamp Mid"].connect(
+ # a whole lot of stuff to basically say "you need to pray at the overworld fuse"
+ swamp_mid_to_cath = regions["Swamp Mid"].connect(
connecting_region=regions["Swamp to Cathedral Main Entrance Region"],
- rule=lambda state: (has_ability(prayer, state, world) and state.has(laurels, player))
- or has_ice_grapple_logic(False, state, world))
+ rule=lambda state: (has_ability(prayer, state, world)
+ and (state.has(laurels, player)
+ # blam yourself in the face with a wand shot off the fuse
+ or (can_ladder_storage(state, world) and state.has(fire_wand, player)
+ and options.ladder_storage >= LadderStorage.option_hard
+ and (not options.shuffle_ladders
+ or state.has_any({"Ladders in Overworld Town",
+ "Ladder to Swamp",
+ "Ladders near Weathervane"}, player)
+ or (state.has("Ladder to Ruined Atoll", player)
+ and state.can_reach_region("Overworld Beach", player))))))
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
+
+ if options.ladder_storage >= LadderStorage.option_hard and options.shuffle_ladders:
+ world.multiworld.register_indirect_condition(regions["Overworld Beach"], swamp_mid_to_cath)
+
regions["Swamp to Cathedral Main Entrance Region"].connect(
connecting_region=regions["Swamp Mid"],
- rule=lambda state: has_ice_grapple_logic(False, state, world))
+ rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world))
+ # grapple push the enemy by the door down, then grapple to it. Really jank
regions["Swamp Mid"].connect(
connecting_region=regions["Swamp Ledge under Cathedral Door"],
- rule=lambda state: has_ladder("Ladders in Swamp", state, world))
+ rule=lambda state: has_ladder("Ladders in Swamp", state, world)
+ or has_ice_grapple_logic(True, IceGrappling.option_hard, state, world))
+ # ice grapple enemy standing at the door
regions["Swamp Ledge under Cathedral Door"].connect(
connecting_region=regions["Swamp Mid"],
rule=lambda state: has_ladder("Ladders in Swamp", state, world)
- or has_ice_grapple_logic(True, state, world)) # nmg: ice grapple the enemy at door
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Swamp Ledge under Cathedral Door"].connect(
connecting_region=regions["Swamp to Cathedral Treasure Room"],
@@ -930,11 +996,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["Back of Swamp"],
rule=lambda state: state.has(laurels, player))
- # nmg: can ice grapple down while you're on the pillars
+ # ice grapple down from the pillar, or do that really annoying laurels zip
+ # the zip goes to front or mid, just doing mid since mid -> front can be done with laurels alone
regions["Back of Swamp Laurels Area"].connect(
connecting_region=regions["Swamp Mid"],
- rule=lambda state: state.has(laurels, player)
- and has_ice_grapple_logic(True, state, world))
+ rule=lambda state: laurels_zip(state, world)
+ or (state.has(laurels, player)
+ and has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)))
+ # get one pillar from the gate, then dash onto the gate, very tricky
+ regions["Swamp Front"].connect(
+ connecting_region=regions["Back of Swamp Laurels Area"],
+ rule=lambda state: laurels_zip(state, world))
regions["Back of Swamp"].connect(
connecting_region=regions["Swamp Hero's Grave Region"],
@@ -942,6 +1014,14 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Swamp Hero's Grave Region"].connect(
connecting_region=regions["Back of Swamp"])
+ regions["Cathedral"].connect(
+ connecting_region=regions["Cathedral to Gauntlet"],
+ rule=lambda state: (has_ability(prayer, state, world)
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
+ or options.entrance_rando) # elevator is always there in ER
+ regions["Cathedral to Gauntlet"].connect(
+ connecting_region=regions["Cathedral"])
+
regions["Cathedral Gauntlet Checkpoint"].connect(
connecting_region=regions["Cathedral Gauntlet"])
@@ -1000,337 +1080,141 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
and state.has_group_unique("Hero Relics", player, 6)
and has_sword(state, player))))
- # connecting the regions portals are in to other portals you can access via ladder storage
- # using has_stick instead of can_ladder_storage since it's already checking the logic rules
- if options.logic_rules == "unrestricted":
+ 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, portal2.region
+ return portal1.name, get_portal_outlet_region(portal2, world)
if portal2.scene_destination() == portal_sd:
- return portal2.name, portal1.region
+ return portal2.name, get_portal_outlet_region(portal1, world)
raise Exception("no matches found in get_paired_region")
- ladder_storages: List[Tuple[str, str, Set[str]]] = [
- # LS from Overworld main
- # The upper Swamp entrance
- ("Overworld", "Overworld Redux, Swamp Redux 2_wall",
- {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}),
- # Upper atoll entrance
- ("Overworld", "Overworld Redux, Atoll Redux_upper",
- {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}),
- # Furnace entrance, next to the sign that leads to West Garden
- ("Overworld", "Overworld Redux, Furnace_gyro_west",
- {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}),
- # Upper West Garden entry, by the belltower
- ("Overworld", "Overworld Redux, Archipelagos Redux_upper",
- {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}),
- # Ruined Passage
- ("Overworld", "Overworld Redux, Ruins Passage_east",
- {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}),
- # Well rail, west side. Can ls in town, get extra height by going over the portal pad
- ("Overworld", "Overworld Redux, Sewer_west_aqueduct",
- {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladder to Quarry"}),
- # Well rail, east side. Need some height from the temple stairs
- ("Overworld", "Overworld Redux, Furnace_gyro_upper_north",
- {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladder to Quarry"}),
- # Quarry entry
- ("Overworld", "Overworld Redux, Darkwoods Tunnel_",
- {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well"}),
- # East Forest entry
- ("Overworld", "Overworld Redux, Forest Belltower_",
- {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well",
- "Ladders near Patrol Cave", "Ladder to Quarry", "Ladders near Dark Tomb"}),
- # Fortress entry
- ("Overworld", "Overworld Redux, Fortress Courtyard_",
- {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well",
- "Ladders near Patrol Cave", "Ladder to Quarry", "Ladders near Dark Tomb"}),
- # Patrol Cave entry
- ("Overworld", "Overworld Redux, PatrolCave_",
- {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well",
- "Ladders near Overworld Checkpoint", "Ladder to Quarry", "Ladders near Dark Tomb"}),
- # Special Shop entry, excluded in non-ER due to soft lock potential
- ("Overworld", "Overworld Redux, ShopSpecial_",
- {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well",
- "Ladders near Overworld Checkpoint", "Ladders near Patrol Cave", "Ladder to Quarry",
- "Ladders near Dark Tomb"}),
- # Temple Rafters, excluded in non-ER + ladder rando due to soft lock potential
- ("Overworld", "Overworld Redux, Temple_rafters",
- {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well",
- "Ladders near Overworld Checkpoint", "Ladders near Patrol Cave", "Ladder to Quarry",
- "Ladders near Dark Tomb"}),
- # Spot above the Quarry entrance,
- # only gets you to the mountain stairs
- ("Overworld above Quarry Entrance", "Overworld Redux, Mountain_",
- {"Ladders near Dark Tomb"}),
+ # 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)
+ ladder_regions[origin_name].connect(
+ regions[paired_region_name],
+ name=p_name + " (LS) " + origin_name)
- # LS from the Overworld Beach
- # West Garden entry by the Furnace
- ("Overworld Beach", "Overworld Redux, Archipelagos Redux_lower",
- {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}),
- # West Garden laurels entry
- ("Overworld Beach", "Overworld Redux, Archipelagos Redux_lowest",
- {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}),
- # Swamp lower entrance
- ("Overworld Beach", "Overworld Redux, Swamp Redux 2_conduit",
- {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}),
- # Rotating Lights entrance
- ("Overworld Beach", "Overworld Redux, Overworld Cave_",
- {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}),
- # Swamp upper entrance
- ("Overworld Beach", "Overworld Redux, Swamp Redux 2_wall",
- {"Ladder to Ruined Atoll"}),
- # Furnace entrance, next to the sign that leads to West Garden
- ("Overworld Beach", "Overworld Redux, Furnace_gyro_west",
- {"Ladder to Ruined Atoll"}),
- # Upper West Garden entry, by the belltower
- ("Overworld Beach", "Overworld Redux, Archipelagos Redux_upper",
- {"Ladder to Ruined Atoll"}),
- # Ruined Passage
- ("Overworld Beach", "Overworld Redux, Ruins Passage_east",
- {"Ladder to Ruined Atoll"}),
- # Well rail, west side. Can ls in town, get extra height by going over the portal pad
- ("Overworld Beach", "Overworld Redux, Sewer_west_aqueduct",
- {"Ladder to Ruined Atoll"}),
- # Well rail, east side. Need some height from the temple stairs
- ("Overworld Beach", "Overworld Redux, Furnace_gyro_upper_north",
- {"Ladder to Ruined Atoll"}),
- # Quarry entry
- ("Overworld Beach", "Overworld Redux, Darkwoods Tunnel_",
- {"Ladder to Ruined Atoll"}),
+ # get what non-overworld ladder storage connections we want together
+ non_ow_ls_list = []
+ non_ow_ls_list.extend(easy_ls)
+ if options.ladder_storage >= LadderStorage.option_medium:
+ non_ow_ls_list.extend(medium_ls)
+ if options.ladder_storage >= LadderStorage.option_hard:
+ non_ow_ls_list.extend(hard_ls)
- # LS from that low spot where you normally walk to swamp
- # Only has low ones you can't get to from main Overworld
- # West Garden main entry from swamp ladder
- ("Overworld Swamp Lower Entry", "Overworld Redux, Archipelagos Redux_lower",
- {"Ladder to Swamp"}),
- # Maze Cave entry from swamp ladder
- ("Overworld Swamp Lower Entry", "Overworld Redux, Maze Room_",
- {"Ladder to Swamp"}),
- # Hourglass Cave entry from swamp ladder
- ("Overworld Swamp Lower Entry", "Overworld Redux, Town Basement_beach",
- {"Ladder to Swamp"}),
- # Lower Atoll entry from swamp ladder
- ("Overworld Swamp Lower Entry", "Overworld Redux, Atoll Redux_lower",
- {"Ladder to Swamp"}),
- # Lowest West Garden entry from swamp ladder
- ("Overworld Swamp Lower Entry", "Overworld Redux, Archipelagos Redux_lowest",
- {"Ladder to Swamp"}),
+ # create the ls elevation regions
+ ladder_regions: Dict[str, Region] = {}
+ for name in ow_ladder_groups.keys():
+ ladder_regions[name] = Region(name, player, world.multiworld)
- # from the ladders by the belltower
- # Ruined Passage
- ("Overworld to West Garden Upper", "Overworld Redux, Ruins Passage_east",
- {"Ladders to West Bell"}),
- # Well rail, west side. Can ls in town, get extra height by going over the portal pad
- ("Overworld to West Garden Upper", "Overworld Redux, Sewer_west_aqueduct",
- {"Ladders to West Bell"}),
- # Well rail, east side. Need some height from the temple stairs
- ("Overworld to West Garden Upper", "Overworld Redux, Furnace_gyro_upper_north",
- {"Ladders to West Bell"}),
- # Quarry entry
- ("Overworld to West Garden Upper", "Overworld Redux, Darkwoods Tunnel_",
- {"Ladders to West Bell"}),
- # East Forest entry
- ("Overworld to West Garden Upper", "Overworld Redux, Forest Belltower_",
- {"Ladders to West Bell"}),
- # Fortress entry
- ("Overworld to West Garden Upper", "Overworld Redux, Fortress Courtyard_",
- {"Ladders to West Bell"}),
- # Patrol Cave entry
- ("Overworld to West Garden Upper", "Overworld Redux, PatrolCave_",
- {"Ladders to West Bell"}),
- # Special Shop entry, excluded in non-ER due to soft lock potential
- ("Overworld to West Garden Upper", "Overworld Redux, ShopSpecial_",
- {"Ladders to West Bell"}),
- # Temple Rafters, excluded in non-ER and ladder rando due to soft lock potential
- ("Overworld to West Garden Upper", "Overworld Redux, Temple_rafters",
- {"Ladders to West Bell"}),
+ # connect the ls elevations to each other where applicable
+ if options.ladder_storage >= LadderStorage.option_medium:
+ for i in range(len(ow_ladder_groups) - 1):
+ ladder_regions[f"LS Elev {i}"].connect(ladder_regions[f"LS Elev {i + 1}"])
- # In the furnace
- # Furnace ladder to the fuse entrance
- ("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_upper_north", set()),
- # Furnace ladder to Dark Tomb
- ("Furnace Ladder Area", "Furnace, Crypt Redux_", set()),
- # Furnace ladder to the West Garden connector
- ("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_west", set()),
+ # connect the applicable overworld regions to the ls elevation regions
+ for origin_region, ladders in region_ladders.items():
+ for ladder_region, region_info in ow_ladder_groups.items():
+ # checking if that region has a ladder or ladders for that elevation
+ common_ladders: FrozenSet[str] = frozenset(ladders.intersection(region_info.ladders))
+ if common_ladders:
+ if options.shuffle_ladders:
+ regions[origin_region].connect(
+ connecting_region=ladder_regions[ladder_region],
+ rule=lambda state, lads=common_ladders: state.has_any(lads, player)
+ and can_ladder_storage(state, world))
+ else:
+ regions[origin_region].connect(
+ connecting_region=ladder_regions[ladder_region],
+ rule=lambda state: can_ladder_storage(state, world))
- # West Garden
- # exit after Garden Knight
- ("West Garden", "Archipelagos Redux, Overworld Redux_upper", set()),
- # West Garden laurels exit
- ("West Garden", "Archipelagos Redux, Overworld Redux_lowest", set()),
+ # connect ls elevation regions to the region on the other side of the portals
+ for ladder_region, region_info in ow_ladder_groups.items():
+ for portal_dest in region_info.portals:
+ ls_connect(ladder_region, "Overworld Redux, " + portal_dest)
- # Atoll, use the little ladder you fix at the beginning
- ("Ruined Atoll", "Atoll Redux, Overworld Redux_lower", set()),
- ("Ruined Atoll", "Atoll Redux, Frog Stairs_mouth", set()),
- ("Ruined Atoll", "Atoll Redux, Frog Stairs_eye", set()),
+ # connect ls elevation regions to regions where you can get an enemy to knock you down, also well rail
+ if options.ladder_storage >= LadderStorage.option_medium:
+ for ladder_region, region_info in ow_ladder_groups.items():
+ for dest_region in region_info.regions:
+ ladder_regions[ladder_region].connect(
+ connecting_region=regions[dest_region],
+ name=ladder_region + " (LS) " + dest_region)
+ # well rail, need height off portal pad for one side, and a tiny extra from stairs on the other
+ ls_connect("LS Elev 3", "Overworld Redux, Sewer_west_aqueduct")
+ ls_connect("LS Elev 3", "Overworld Redux, Furnace_gyro_upper_north")
- # East Forest
- # Entrance by the dancing fox holy cross spot
- ("East Forest", "East Forest Redux, East Forest Redux Laddercave_upper", set()),
+ # connect ls elevation regions to portals where you need to get behind the map to enter it
+ if options.ladder_storage >= LadderStorage.option_hard:
+ ls_connect("LS Elev 1", "Overworld Redux, EastFiligreeCache_")
+ ls_connect("LS Elev 2", "Overworld Redux, Town_FiligreeRoom_")
+ ls_connect("LS Elev 3", "Overworld Redux, Overworld Interiors_house")
+ ls_connect("LS Elev 5", "Overworld Redux, Temple_main")
- # From the west side of Guard House 1 to the east side
- ("Guard House 1 West", "East Forest Redux Laddercave, East Forest Redux_gate", set()),
- ("Guard House 1 West", "East Forest Redux Laddercave, Forest Boss Room_", set()),
-
- # Upper exit from the Forest Grave Path, use LS at the ladder by the gate switch
- ("Forest Grave Path Main", "Sword Access, East Forest Redux_upper", set()),
-
- # Fortress Exterior
- # shop, ls at the ladder by the telescope
- ("Fortress Exterior from Overworld", "Fortress Courtyard, Shop_", set()),
- # Fortress main entry and grave path lower entry, ls at the ladder by the telescope
- ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Main_Big Door", set()),
- ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Lower", set()),
- # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse
- ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Upper", set()),
- ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress East_", set()),
-
- # same as above, except from the east side of the area
- ("Fortress Exterior from East Forest", "Fortress Courtyard, Overworld Redux_", set()),
- ("Fortress Exterior from East Forest", "Fortress Courtyard, Shop_", set()),
- ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Main_Big Door", set()),
- ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Lower", set()),
- ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Upper", set()),
- ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress East_", set()),
-
- # same as above, except from the Beneath the Vault entrance ladder
- ("Fortress Exterior near cave", "Fortress Courtyard, Overworld Redux_",
- {"Ladder to Beneath the Vault"}),
- ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Main_Big Door",
- {"Ladder to Beneath the Vault"}),
- ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Lower",
- {"Ladder to Beneath the Vault"}),
- ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Upper",
- {"Ladder to Beneath the Vault"}),
- ("Fortress Exterior near cave", "Fortress Courtyard, Fortress East_",
- {"Ladder to Beneath the Vault"}),
-
- # ls at the ladder, need to gain a little height to get up the stairs
- # excluded in non-ER due to soft lock potential
- ("Lower Mountain", "Mountain, Mountaintop_", set()),
-
- # Where the rope is behind Monastery. Connecting here since, if you have this region, you don't need a sword
- ("Quarry Monastery Entry", "Quarry Redux, Monastery_back", set()),
-
- # Swamp to Gauntlet
- ("Swamp Mid", "Swamp Redux 2, Cathedral Arena_",
- {"Ladders in Swamp"}),
- # Swamp to Overworld upper
- ("Swamp Mid", "Swamp Redux 2, Overworld Redux_wall",
- {"Ladders in Swamp"}),
- # Ladder by the hero grave
- ("Back of Swamp", "Swamp Redux 2, Overworld Redux_conduit", set()),
- ("Back of Swamp", "Swamp Redux 2, Shop_", set()),
- # Need to put the cathedral HC code mid-flight
- ("Back of Swamp", "Swamp Redux 2, Cathedral Redux_secret", set()),
- ]
-
- for region_name, scene_dest, ladders in ladder_storages:
- portal_name, paired_region = get_portal_info(scene_dest)
- # this is the only exception, requiring holy cross as well
- if portal_name == "Swamp to Cathedral Secret Legend Room Entrance" and region_name == "Back of Swamp":
- regions[region_name].connect(
- regions[paired_region],
- name=portal_name + " (LS) " + region_name,
- rule=lambda state: has_stick(state, player)
- and has_ability(holy_cross, state, world)
- and (has_ladder("Ladders in Swamp", state, world)
- or has_ice_grapple_logic(True, state, world)
- or not options.entrance_rando))
- # soft locked without this ladder
- elif portal_name == "West Garden Exit after Boss" and not options.entrance_rando:
- regions[region_name].connect(
- regions[paired_region],
- name=portal_name + " (LS) " + region_name,
- rule=lambda state: has_stick(state, player)
- and (state.has("Ladders to West Bell", player)))
- # soft locked unless you have either ladder. if you have laurels, you use the other Entrance
- elif portal_name in {"Furnace Exit towards West Garden", "Furnace Exit to Dark Tomb"} \
- and not options.entrance_rando:
- regions[region_name].connect(
- regions[paired_region],
- name=portal_name + " (LS) " + region_name,
- rule=lambda state: has_stick(state, player)
- and state.has_any({"Ladder in Dark Tomb", "Ladders to West Bell"}, player))
- # soft locked for the same reasons as above
- elif portal_name in {"Entrance to Furnace near West Garden", "West Garden Entrance from Furnace"} \
- and not options.entrance_rando:
- regions[region_name].connect(
- regions[paired_region],
- name=portal_name + " (LS) " + region_name,
- rule=lambda state: has_stick(state, player) and state.has_any(ladders, player)
- and state.has_any({"Ladder in Dark Tomb", "Ladders to West Bell"}, player))
- # soft locked if you can't get past garden knight backwards or up the belltower ladders
- elif portal_name == "West Garden Entrance near Belltower" and not options.entrance_rando:
- regions[region_name].connect(
- regions[paired_region],
- name=portal_name + " (LS) " + region_name,
- rule=lambda state: has_stick(state, player) and state.has_any(ladders, player)
- and state.has_any({"Ladders to West Bell", laurels}, player))
- # soft locked if you can't get back out
- elif portal_name == "Fortress Courtyard to Beneath the Vault" and not options.entrance_rando:
- regions[region_name].connect(
- regions[paired_region],
- name=portal_name + " (LS) " + region_name,
- rule=lambda state: has_stick(state, player) and state.has("Ladder to Beneath the Vault", player)
- and has_lantern(state, world))
- elif portal_name == "Atoll Lower Entrance" and not options.entrance_rando:
- regions[region_name].connect(
- regions[paired_region],
- name=portal_name + " (LS) " + region_name,
- rule=lambda state: has_stick(state, player) and state.has_any(ladders, player)
- and (state.has_any({"Ladders in Overworld Town", grapple}, player)
- or has_ice_grapple_logic(True, state, world)))
- elif portal_name == "Atoll Upper Entrance" and not options.entrance_rando:
- regions[region_name].connect(
- regions[paired_region],
- name=portal_name + " (LS) " + region_name,
- rule=lambda state: has_stick(state, player) and state.has_any(ladders, player)
- and state.has(grapple, player) or has_ability(prayer, state, world))
- # soft lock potential
- elif portal_name in {"Special Shop Entrance", "Stairs to Top of the Mountain", "Swamp Upper Entrance",
- "Swamp Lower Entrance", "Caustic Light Cave Entrance"} and not options.entrance_rando:
+ # connect the non-overworld ones
+ for ls_info in non_ow_ls_list:
+ # for places where the destination is a region (so you have to get knocked down)
+ if ls_info.dest_is_region:
+ # none of the non-ow ones have multiple ladders that can be used, so don't need has_any
+ if options.shuffle_ladders and ls_info.ladders_req:
+ regions[ls_info.origin].connect(
+ connecting_region=regions[ls_info.destination],
+ name=ls_info.destination + " (LS) " + ls_info.origin,
+ rule=lambda state, lad=ls_info.ladders_req: can_ladder_storage(state, world)
+ and state.has(lad, player))
+ else:
+ regions[ls_info.origin].connect(
+ connecting_region=regions[ls_info.destination],
+ name=ls_info.destination + " (LS) " + ls_info.origin,
+ rule=lambda state: can_ladder_storage(state, world))
continue
- # soft lock if you don't have the ladder, I regret writing unrestricted logic
- elif portal_name == "Temple Rafters Entrance" and not options.entrance_rando:
- regions[region_name].connect(
- regions[paired_region],
- name=portal_name + " (LS) " + region_name,
- rule=lambda state: has_stick(state, player)
- and state.has_any(ladders, player)
- and (state.has("Ladder near Temple Rafters", player)
- or (state.has_all({laurels, grapple}, player)
- and ((state.has("Ladders near Patrol Cave", player)
- and (state.has("Ladders near Dark Tomb", player)
- or state.has("Ladder to Quarry", player)
- and (state.has(fire_wand, player) or has_sword(state, player))))
- or state.has("Ladders near Overworld Checkpoint", player)
- or has_ice_grapple_logic(True, state, world)))))
- # if no ladder items are required, just do the basic stick only lambda
- elif not ladders or not options.shuffle_ladders:
- regions[region_name].connect(
- regions[paired_region],
- name=portal_name + " (LS) " + region_name,
- rule=lambda state: has_stick(state, player))
- # one ladder required
- elif len(ladders) == 1:
- ladder = ladders.pop()
- regions[region_name].connect(
- regions[paired_region],
- name=portal_name + " (LS) " + region_name,
- rule=lambda state: has_stick(state, player) and state.has(ladder, player))
- # if multiple ladders can be used
+
+ portal_name, dest_region = get_portal_info(ls_info.destination)
+ # these two are special cases
+ if ls_info.destination == "Atoll Redux, Frog Stairs_mouth":
+ regions[ls_info.origin].connect(
+ connecting_region=regions[dest_region],
+ name=portal_name + " (LS) " + ls_info.origin,
+ rule=lambda state: can_ladder_storage(state, world)
+ and (has_ladder("Ladders in South Atoll", state, world)
+ or state.has(key, player, 2) # can do it from the rope
+ # ice grapple push a crab into the door
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
+ or options.ladder_storage >= LadderStorage.option_medium)) # use the little ladder
+ # holy cross mid-ls to get in here
+ elif ls_info.destination == "Swamp Redux 2, Cathedral Redux_secret":
+ if ls_info.origin == "Swamp Mid":
+ regions[ls_info.origin].connect(
+ connecting_region=regions[dest_region],
+ name=portal_name + " (LS) " + ls_info.origin,
+ rule=lambda state: can_ladder_storage(state, world) and has_ability(holy_cross, state, world)
+ and has_ladder("Ladders in Swamp", state, world))
+ else:
+ regions[ls_info.origin].connect(
+ connecting_region=regions[dest_region],
+ name=portal_name + " (LS) " + ls_info.origin,
+ rule=lambda state: can_ladder_storage(state, world) and has_ability(holy_cross, state, world))
+
+ elif options.shuffle_ladders and ls_info.ladders_req:
+ regions[ls_info.origin].connect(
+ connecting_region=regions[dest_region],
+ name=portal_name + " (LS) " + ls_info.origin,
+ rule=lambda state, lad=ls_info.ladders_req: can_ladder_storage(state, world)
+ and state.has(lad, player))
else:
- regions[region_name].connect(
- regions[paired_region],
- name=portal_name + " (LS) " + region_name,
- rule=lambda state: has_stick(state, player) and state.has_any(ladders, player))
+ regions[ls_info.origin].connect(
+ connecting_region=regions[dest_region],
+ name=portal_name + " (LS) " + ls_info.origin,
+ rule=lambda state: can_ladder_storage(state, world))
+
+ for region in ladder_regions.values():
+ world.multiworld.regions.append(region)
def set_er_location_rules(world: "TunicWorld") -> None:
player = world.player
- options = world.options
forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player)
@@ -1439,10 +1323,13 @@ def set_er_location_rules(world: "TunicWorld") -> None:
# Ruined Atoll
set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"),
lambda state: state.has(laurels, player))
+ # ice grapple push a crab through the door
set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"),
- lambda state: state.has(laurels, player) or state.has(key, player, 2))
+ lambda state: state.has(laurels, player) or state.has(key, player, 2)
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"),
- lambda state: state.has(laurels, player) or state.has(key, player, 2))
+ lambda state: state.has(laurels, player) or state.has(key, player, 2)
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
# Frog's Domain
set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"),
@@ -1465,23 +1352,25 @@ def set_er_location_rules(world: "TunicWorld") -> None:
lambda state: state.has(laurels, player))
# Ziggurat
- # if ER is off, you still need to get past the Admin or you'll get stuck in lower zig
+ # if ER is off, while you can get the chest, you won't be able to actually get through zig
set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"),
- lambda state: has_sword(state, player) or (state.has(fire_wand, player) and (state.has(laurels, player)
- or options.entrance_rando)))
+ lambda state: has_sword(state, player) or (state.has(fire_wand, player)
+ and (state.has(laurels, player)
+ or world.options.entrance_rando)))
set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"),
lambda state: has_sword(state, player) and has_ability(prayer, state, world))
# Bosses
set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"),
lambda state: has_sword(state, player))
- # nmg - kill Librarian with a lure, or gun I guess
set_rule(world.get_location("Librarian - Hexagon Green"),
- lambda state: (has_sword(state, player) or options.logic_rules)
+ lambda state: has_sword(state, player)
and has_ladder("Ladders in Library", state, world))
- # nmg - kill boss scav with orb + firecracker, or similar
+ # can ice grapple boss scav off the side
+ # the grapple from the other side of the bridge isn't in logic 'cause we don't have a misc tricks option
set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"),
- lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules))
+ lambda state: has_sword(state, player)
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
# Swamp
set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"),
@@ -1490,7 +1379,7 @@ def set_er_location_rules(world: "TunicWorld") -> None:
lambda state: state.has(laurels, player))
set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"),
lambda state: state.has(laurels, player))
- # these two swamp checks really want you to kill the big skeleton first
+ # really hard to do 4 skulls with a big skeleton chasing you around
set_rule(world.get_location("Swamp - [South Graveyard] 4 Orange Skulls"),
lambda state: has_sword(state, player))
@@ -1541,7 +1430,13 @@ def set_er_location_rules(world: "TunicWorld") -> None:
# Bombable Walls
for location_name in bomb_walls:
- set_rule(world.get_location(location_name), lambda state: state.has(gun, player) or can_shop(state, world))
+ set_rule(world.get_location(location_name),
+ lambda state: state.has(gun, player)
+ or can_shop(state, world)
+ or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
+ # not enough space to ice grapple into here
+ set_rule(world.get_location("Quarry - [East] Bombable Wall"),
+ lambda state: state.has(gun, player) or can_shop(state, world))
# Shop
set_rule(world.get_location("Shop - Potion 1"),
diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py
index e7c8fd58d0..05f6177aa5 100644
--- a/worlds/tunic/er_scripts.py
+++ b/worlds/tunic/er_scripts.py
@@ -1,7 +1,7 @@
-from typing import Dict, List, Set, TYPE_CHECKING
+from typing import Dict, List, Set, Tuple, TYPE_CHECKING
from BaseClasses import Region, ItemClassification, Item, Location
from .locations import location_table
-from .er_data import Portal, tunic_er_regions, portal_mapping, traversal_requirements, DeadEnd
+from .er_data import Portal, portal_mapping, traversal_requirements, DeadEnd, RegionInfo
from .er_rules import set_er_region_rules
from Options import PlandoConnection
from .options import EntranceRando
@@ -22,17 +22,18 @@ class TunicERLocation(Location):
def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
regions: Dict[str, Region] = {}
+ for region_name, region_data in world.er_regions.items():
+ regions[region_name] = Region(region_name, world.player, world.multiworld)
+
if world.options.entrance_rando:
- portal_pairs = pair_portals(world)
+ 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)
else:
- portal_pairs = vanilla_portals()
-
- for region_name, region_data in tunic_er_regions.items():
- regions[region_name] = Region(region_name, world.player, world.multiworld)
+ portal_pairs = vanilla_portals(world, regions)
set_er_region_rules(world, regions, portal_pairs)
@@ -93,7 +94,18 @@ def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None:
region.locations.append(location)
-def vanilla_portals() -> Dict[Portal, Portal]:
+# 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}"
+ 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
+
+
+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
portal_map = [portal for portal in portal_mapping if portal.name != "Ziggurat Lower Falling Entrance"]
@@ -105,8 +117,9 @@ def vanilla_portals() -> Dict[Portal, Portal]:
portal2_sdt = portal1.destination_scene()
if portal2_sdt.startswith("Shop,"):
- portal2 = Portal(name="Shop", region="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)
elif portal2_sdt == "Purgatory, Purgatory_bottom":
portal2_sdt = "Purgatory, Purgatory_top"
@@ -125,14 +138,15 @@ def vanilla_portals() -> Dict[Portal, Portal]:
# pairing off portals, starting with dead ends
-def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
- # separate the portals into dead ends and non-dead ends
+def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]:
portal_pairs: Dict[Portal, Portal] = {}
dead_ends: List[Portal] = []
two_plus: List[Portal] = []
player_name = world.player_name
portal_map = portal_mapping.copy()
- logic_rules = world.options.logic_rules.value
+ 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
laurels_location = world.options.laurels_location
traversal_reqs = deepcopy(traversal_requirements)
@@ -142,19 +156,21 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
# if it's not one of the EntranceRando options, it's a custom seed
if world.options.entrance_rando.value not in EntranceRando.options.values():
seed_group = world.seed_groups[world.options.entrance_rando.value]
- logic_rules = seed_group["logic_rules"]
+ laurels_zips = seed_group["laurels_zips"]
+ ice_grappling = seed_group["ice_grappling"]
+ ladder_storage = seed_group["ladder_storage"]
fixed_shop = seed_group["fixed_shop"]
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)
+
# marking that you don't immediately have laurels
if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
has_laurels = False
- shop_scenes: Set[str] = set()
shop_count = 6
if fixed_shop:
shop_count = 0
- shop_scenes.add("Overworld Redux")
else:
# if fixed shop is off, remove this portal
for portal in portal_map:
@@ -169,13 +185,13 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
# create separate lists for dead ends and non-dead ends
for portal in portal_map:
- dead_end_status = tunic_er_regions[portal.region].dead_end
+ dead_end_status = world.er_regions[portal.region].dead_end
if dead_end_status == DeadEnd.free:
two_plus.append(portal)
elif dead_end_status == DeadEnd.all_cats:
dead_ends.append(portal)
elif dead_end_status == DeadEnd.restricted:
- if logic_rules:
+ if ice_grappling:
two_plus.append(portal)
else:
dead_ends.append(portal)
@@ -196,7 +212,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
# make better start region stuff when/if implementing random start
start_region = "Overworld"
connected_regions.add(start_region)
- connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules)
+ connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks)
if world.options.entrance_rando.value in EntranceRando.options.values():
plando_connections = world.options.plando_connections.value
@@ -225,12 +241,14 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both"))
non_dead_end_regions = set()
- for region_name, region_info in tunic_er_regions.items():
+ for region_name, region_info in world.er_regions.items():
if not region_info.dead_end:
non_dead_end_regions.add(region_name)
- elif region_info.dead_end == 2 and logic_rules:
+ # 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)
- elif region_info.dead_end == 3:
+ # secret gathering place and zig skip get weird, special handling
+ 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):
non_dead_end_regions.add(region_name)
@@ -239,6 +257,9 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
for connection in 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]:
+ waterfall_plando = True
portal1_dead_end = True
portal2_dead_end = True
@@ -285,16 +306,13 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
break
# if it's not a dead end, it might be a shop
if p_exit == "Shop Portal":
- portal2 = Portal(name="Shop Portal", region="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)
shop_count -= 1
# need to maintain an even number of portals total
if shop_count < 0:
shop_count += 2
- for p in portal_mapping:
- if p.name == p_entrance:
- shop_scenes.add(p.scene())
- break
# and if it's neither shop nor dead end, it just isn't correct
else:
if not portal2:
@@ -327,11 +345,10 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
else:
raise Exception(f"{player_name} paired a dead end to a dead end in their "
"plando connections.")
- waterfall_plando = True
portal_pairs[portal1] = portal2
# if we have plando connections, our connected regions may change somewhat
- connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules)
+ connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks)
if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
portal1 = None
@@ -343,7 +360,9 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
raise Exception(f"Failed to do Fixed Shop option. "
f"Did {player_name} plando connection the Windmill Shop entrance?")
- portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region", tag="_")
+ 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.remove(portal1)
@@ -393,7 +412,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
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_rules):
+ if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_tricks):
continue
elif portal.region != "Secret Gathering Place":
continue
@@ -405,9 +424,9 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
# once we have both portals, connect them and add the new region(s) to connected_regions
if check_success == 2:
- connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules)
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)
@@ -418,16 +437,12 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
shop_count = 0
for i in range(shop_count):
- portal1 = None
- for portal in two_plus:
- if portal.scene() not in shop_scenes:
- shop_scenes.add(portal.scene())
- portal1 = portal
- two_plus.remove(portal)
- break
+ portal1 = two_plus.pop()
if portal1 is None:
- raise Exception("Too many shops in the pool, or something else went wrong.")
- portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region", tag="_")
+ 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
@@ -460,13 +475,12 @@ def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dic
region1 = regions[portal1.region]
region2 = regions[portal2.region]
region1.connect(connecting_region=region2, name=portal1.name)
- # prevent the logic from thinking you can get to any shop-connected region from the shop
- if portal2.name not in {"Shop", "Shop Portal"}:
- region2.connect(connecting_region=region1, name=portal2.name)
+ region2.connect(connecting_region=region1, name=portal2.name)
def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[str, Dict[str, List[List[str]]]],
- has_laurels: bool, logic: int) -> Set[str]:
+ has_laurels: bool, logic: Tuple[bool, int, int]) -> Set[str]:
+ zips, ice_grapples, ls = logic
# starting count, so we can run it again if this changes
region_count = len(connected_regions)
for origin, destinations in traversal_reqs.items():
@@ -485,11 +499,15 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s
if req == "Hyperdash":
if not has_laurels:
break
- elif req == "NMG":
- if not logic:
+ elif req == "Zip":
+ if not zips:
break
- elif req == "UR":
- if logic < 2:
+ # if req is higher than logic option, then it breaks since it's not a valid connection
+ elif req.startswith("IG"):
+ if int(req[-1]) > ice_grapples:
+ break
+ elif req.startswith("LS"):
+ if int(req[-1]) > ls:
break
elif req not in connected_regions:
break
diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py
index e0ee17831a..55aa3468fc 100644
--- a/worlds/tunic/items.py
+++ b/worlds/tunic/items.py
@@ -166,6 +166,7 @@ item_table: Dict[str, TunicItemData] = {
"Ladders in Swamp": TunicItemData(ItemClassification.progression, 0, 150, "Ladders"),
}
+# items to be replaced by fool traps
fool_tiers: List[List[str]] = [
[],
["Money x1", "Money x10", "Money x15", "Money x16"],
@@ -173,6 +174,7 @@ fool_tiers: List[List[str]] = [
["Money x1", "Money x10", "Money x15", "Money x16", "Money x20", "Money x25", "Money x30"],
]
+# items we'll want the location of in slot data, for generating in-game hints
slot_data_item_names = [
"Stick",
"Sword",
diff --git a/worlds/tunic/ladder_storage_data.py b/worlds/tunic/ladder_storage_data.py
new file mode 100644
index 0000000000..a29d50b4f4
--- /dev/null
+++ b/worlds/tunic/ladder_storage_data.py
@@ -0,0 +1,186 @@
+from typing import Dict, List, Set, NamedTuple, Optional
+
+
+# ladders in overworld, since it is the most complex area for ladder storage
+class OWLadderInfo(NamedTuple):
+ ladders: Set[str] # ladders where the top or bottom is at the same elevation
+ portals: List[str] # portals at the same elevation, only those without doors
+ regions: List[str] # regions where a melee enemy can hit you out of ladder storage
+
+
+# groups for ladders at the same elevation, for use in determing whether you can ls to entrances in diff rulesets
+ow_ladder_groups: Dict[str, OWLadderInfo] = {
+ # lowest elevation
+ "LS Elev 0": OWLadderInfo({"Ladders in Overworld Town", "Ladder to Ruined Atoll", "Ladder to Swamp"},
+ ["Swamp Redux 2_conduit", "Overworld Cave_", "Atoll Redux_lower", "Maze Room_",
+ "Town Basement_beach", "Archipelagos Redux_lower", "Archipelagos Redux_lowest"],
+ ["Overworld Beach"]),
+ # also the east filigree room
+ "LS Elev 1": OWLadderInfo({"Ladders near Weathervane", "Ladders in Overworld Town", "Ladder to Swamp"},
+ ["Furnace_gyro_lower", "Swamp Redux 2_wall"],
+ ["Overworld Tunnel Turret"]),
+ # also the fountain filigree room and ruined passage door
+ "LS Elev 2": OWLadderInfo({"Ladders near Weathervane", "Ladders to West Bell"},
+ ["Archipelagos Redux_upper", "Ruins Passage_east"],
+ ["After Ruined Passage"]),
+ # also old house door
+ "LS Elev 3": OWLadderInfo({"Ladders near Weathervane", "Ladder to Quarry", "Ladders to West Bell",
+ "Ladders in Overworld Town"},
+ [],
+ ["Overworld after Envoy", "East Overworld"]),
+ # skip top of top ladder next to weathervane level, does not provide logical access to anything
+ "LS Elev 4": OWLadderInfo({"Ladders near Dark Tomb", "Ladder to Quarry", "Ladders to West Bell", "Ladders in Well",
+ "Ladders in Overworld Town"},
+ ["Darkwoods Tunnel_"],
+ []),
+ "LS Elev 5": OWLadderInfo({"Ladders near Overworld Checkpoint", "Ladders near Patrol Cave"},
+ ["PatrolCave_", "Forest Belltower_", "Fortress Courtyard_", "ShopSpecial_"],
+ ["East Overworld"]),
+ # skip top of belltower, middle of dark tomb ladders, and top of checkpoint, does not grant access to anything
+ "LS Elev 6": OWLadderInfo({"Ladders near Patrol Cave", "Ladder near Temple Rafters"},
+ ["Temple_rafters"],
+ ["Overworld above Patrol Cave"]),
+ # in-line with the chest above dark tomb, gets you up the mountain stairs
+ "LS Elev 7": OWLadderInfo({"Ladders near Patrol Cave", "Ladder near Temple Rafters", "Ladders near Dark Tomb"},
+ ["Mountain_"],
+ ["Upper Overworld"]),
+}
+
+
+# ladders accessible within different regions of overworld, only those that are relevant
+# other scenes will just have them hardcoded since this type of structure is not necessary there
+region_ladders: Dict[str, Set[str]] = {
+ "Overworld": {"Ladders near Weathervane", "Ladders near Overworld Checkpoint", "Ladders near Dark Tomb",
+ "Ladders in Overworld Town", "Ladder to Swamp", "Ladders in Well"},
+ "Overworld Beach": {"Ladder to Ruined Atoll"},
+ "Overworld at Patrol Cave": {"Ladders near Patrol Cave"},
+ "Overworld Quarry Entry": {"Ladder to Quarry"},
+ "Overworld Belltower": {"Ladders to West Bell"},
+ "Overworld after Temple Rafters": {"Ladders near Temple Rafters"},
+}
+
+
+class LadderInfo(NamedTuple):
+ origin: str # origin region
+ destination: str # destination portal
+ ladders_req: Optional[str] = None # ladders required to do this
+ dest_is_region: bool = False # whether it is a region that you are going to
+
+
+easy_ls: List[LadderInfo] = [
+ # In the furnace
+ # Furnace ladder to the fuse entrance
+ LadderInfo("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_upper_north"),
+ # Furnace ladder to Dark Tomb
+ LadderInfo("Furnace Ladder Area", "Furnace, Crypt Redux_"),
+ # Furnace ladder to the West Garden connector
+ LadderInfo("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_west"),
+
+ # West Garden
+ # exit after Garden Knight
+ LadderInfo("West Garden", "Archipelagos Redux, Overworld Redux_upper"),
+ # West Garden laurels exit
+ LadderInfo("West Garden", "Archipelagos Redux, Overworld Redux_lowest"),
+
+ # Atoll, use the little ladder you fix at the beginning
+ LadderInfo("Ruined Atoll", "Atoll Redux, Overworld Redux_lower"),
+ LadderInfo("Ruined Atoll", "Atoll Redux, Frog Stairs_mouth"), # special case
+
+ # East Forest
+ # Entrance by the dancing fox holy cross spot
+ LadderInfo("East Forest", "East Forest Redux, East Forest Redux Laddercave_upper"),
+
+ # From the west side of Guard House 1 to the east side
+ LadderInfo("Guard House 1 West", "East Forest Redux Laddercave, East Forest Redux_gate"),
+ LadderInfo("Guard House 1 West", "East Forest Redux Laddercave, Forest Boss Room_"),
+
+ # Fortress Exterior
+ # shop, ls at the ladder by the telescope
+ LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Shop_"),
+ # Fortress main entry and grave path lower entry, ls at the ladder by the telescope
+ LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Main_Big Door"),
+ LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Lower"),
+ # Use the top of the ladder by the telescope
+ LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Upper"),
+ LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress East_"),
+
+ # same as above, except from the east side of the area
+ LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Overworld Redux_"),
+ LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Shop_"),
+ LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Main_Big Door"),
+ LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Lower"),
+
+ # same as above, except from the Beneath the Vault entrance ladder
+ LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Overworld Redux_", "Ladder to Beneath the Vault"),
+ LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress Main_Big Door",
+ "Ladder to Beneath the Vault"),
+ LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Lower",
+ "Ladder to Beneath the Vault"),
+
+ # Swamp to Gauntlet
+ LadderInfo("Swamp Mid", "Swamp Redux 2, Cathedral Arena_", "Ladders in Swamp"),
+
+ # Ladder by the hero grave
+ LadderInfo("Back of Swamp", "Swamp Redux 2, Overworld Redux_conduit"),
+ LadderInfo("Back of Swamp", "Swamp Redux 2, Shop_"),
+]
+
+# if we can gain elevation or get knocked down, add the harder ones
+medium_ls: List[LadderInfo] = [
+ # region-destination versions of easy ls spots
+ LadderInfo("East Forest", "East Forest Dance Fox Spot", dest_is_region=True),
+ # fortress courtyard knockdowns are never logically relevant, the fuse requires upper
+ LadderInfo("Back of Swamp", "Swamp Mid", dest_is_region=True),
+ LadderInfo("Back of Swamp", "Swamp Front", dest_is_region=True),
+
+ # gain height off the northeast fuse ramp
+ LadderInfo("Ruined Atoll", "Atoll Redux, Frog Stairs_eye"),
+
+ # Upper exit from the Forest Grave Path, use LS at the ladder by the gate switch
+ LadderInfo("Forest Grave Path Main", "Sword Access, East Forest Redux_upper"),
+
+ # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse
+ LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard Upper", dest_is_region=True),
+ LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Upper"),
+ LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress East_"),
+ LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard Upper", dest_is_region=True),
+ LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Upper",
+ "Ladder to Beneath the Vault"),
+ LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress East_", "Ladder to Beneath the Vault"),
+ LadderInfo("Fortress Exterior near cave", "Fortress Courtyard Upper", "Ladder to Beneath the Vault",
+ dest_is_region=True),
+
+ # need to gain height to get up the stairs
+ LadderInfo("Lower Mountain", "Mountain, Mountaintop_"),
+
+ # Where the rope is behind Monastery
+ LadderInfo("Quarry Entry", "Quarry Redux, Monastery_back"),
+ LadderInfo("Quarry Monastery Entry", "Quarry Redux, Monastery_back"),
+ LadderInfo("Quarry Back", "Quarry Redux, Monastery_back"),
+
+ LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_2_"),
+ LadderInfo("Rooted Ziggurat Lower Back", "Rooted Ziggurat Lower Front", dest_is_region=True),
+
+ # Swamp to Overworld upper
+ LadderInfo("Swamp Mid", "Swamp Redux 2, Overworld Redux_wall", "Ladders in Swamp"),
+ LadderInfo("Back of Swamp", "Swamp Redux 2, Overworld Redux_wall"),
+]
+
+hard_ls: List[LadderInfo] = [
+ # lower ladder, go into the waterfall then above the bonfire, up a ramp, then through the right wall
+ LadderInfo("Beneath the Well Front", "Sewer, Sewer_Boss_", "Ladders in Well"),
+ LadderInfo("Beneath the Well Front", "Sewer, Overworld Redux_west_aqueduct", "Ladders in Well"),
+ LadderInfo("Beneath the Well Front", "Beneath the Well Back", "Ladders in Well", dest_is_region=True),
+ # go through the hexagon engraving above the vault door
+ LadderInfo("Frog's Domain", "frog cave main, Frog Stairs_Exit", "Ladders to Frog's Domain"),
+ # the turret at the end here is not affected by enemy rando
+ LadderInfo("Frog's Domain", "Frog's Domain Back", "Ladders to Frog's Domain", dest_is_region=True),
+ # todo: see if we can use that new laurels strat here
+ # LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_FTRoom_"),
+ # go behind the cathedral to reach the door, pretty easily doable
+ LadderInfo("Swamp Mid", "Swamp Redux 2, Cathedral Redux_main", "Ladders in Swamp"),
+ LadderInfo("Back of Swamp", "Swamp Redux 2, Cathedral Redux_main"),
+ # need to do hc midair, probably cannot get into this without hc
+ LadderInfo("Swamp Mid", "Swamp Redux 2, Cathedral Redux_secret", "Ladders in Swamp"),
+ LadderInfo("Back of Swamp", "Swamp Redux 2, Cathedral Redux_secret"),
+]
diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py
index 0991622816..442e0c0144 100644
--- a/worlds/tunic/locations.py
+++ b/worlds/tunic/locations.py
@@ -47,7 +47,7 @@ location_table: Dict[str, TunicLocationData] = {
"Guardhouse 1 - Upper Floor": TunicLocationData("East Forest", "Guard House 1 East"),
"East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", location_group="Holy Cross"),
"East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "Lower Forest", location_group="Holy Cross"),
- "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "East Forest"),
+ "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "Lower Forest"),
"East Forest - Above Save Point": TunicLocationData("East Forest", "East Forest"),
"East Forest - Above Save Point Obscured": TunicLocationData("East Forest", "East Forest"),
"East Forest - From Guardhouse 1 Chest": TunicLocationData("East Forest", "East Forest Dance Fox Spot"),
@@ -205,7 +205,7 @@ location_table: Dict[str, TunicLocationData] = {
"Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", location_group="Holy Cross"),
"Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", location_group="Holy Cross"),
"Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", location_group="Holy Cross"),
- "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"),
+ "Monastery - Monastery Chest": TunicLocationData("Monastery", "Monastery Back"),
"Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="Holy Cross"),
"Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"),
"Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry Back", "Quarry Back"),
@@ -224,7 +224,7 @@ location_table: Dict[str, TunicLocationData] = {
"Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Quarry", "Quarry Monastery Entry"),
"Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"),
"Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"),
- "Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry"),
+ "Hero's Grave - Ash Relic": TunicLocationData("Monastery", "Hero Relic - Quarry"),
"Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"),
diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py
index 92cbafba23..1683b3ca5a 100644
--- a/worlds/tunic/options.py
+++ b/worlds/tunic/options.py
@@ -1,7 +1,7 @@
from dataclasses import dataclass
from typing import Dict, Any
from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections,
- PerGameCommonOptions, OptionGroup)
+ PerGameCommonOptions, OptionGroup, Visibility)
from .er_data import portal_mapping
@@ -39,27 +39,6 @@ class AbilityShuffling(Toggle):
display_name = "Shuffle Abilities"
-class LogicRules(Choice):
- """
- Set which logic rules to use for your world.
- Restricted: Standard logic, no glitches.
- No Major Glitches: Sneaky Laurels zips, ice grapples through doors, shooting the west bell, and boss quick kills are included in logic.
- * Ice grappling through the Ziggurat door is not in logic since you will get stuck in there without Prayer.
- Unrestricted: Logic in No Major Glitches, as well as ladder storage to get to certain places early.
- * Torch is given to the player at the start of the game due to the high softlock potential with various tricks. Using the torch is not required in logic.
- * Using Ladder Storage to get to individual chests is not in logic to avoid tedium.
- * Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on.
- """
- internal_name = "logic_rules"
- display_name = "Logic Rules"
- option_restricted = 0
- option_no_major_glitches = 1
- alias_nmg = 1
- option_unrestricted = 2
- alias_ur = 2
- default = 0
-
-
class Lanternless(Toggle):
"""
Choose whether you require the Lantern for dark areas.
@@ -173,8 +152,8 @@ class ShuffleLadders(Toggle):
"""
internal_name = "shuffle_ladders"
display_name = "Shuffle Ladders"
-
-
+
+
class TunicPlandoConnections(PlandoConnections):
"""
Generic connection plando. Format is:
@@ -189,6 +168,82 @@ class TunicPlandoConnections(PlandoConnections):
duplicate_exits = True
+class LaurelsZips(Toggle):
+ """
+ Choose whether to include using the Hero's Laurels to zip through gates, doors, and tricky spots.
+ Notable inclusions are the Monastery gate, Ruined Passage door, Old House gate, Forest Grave Path gate, and getting from the Back of Swamp to the Middle of Swamp.
+ """
+ internal_name = "laurels_zips"
+ display_name = "Laurels Zips Logic"
+
+
+class IceGrappling(Choice):
+ """
+ Choose whether grappling frozen enemies is in logic.
+ Easy includes ice grappling enemies that are in range without luring them. May include clips through terrain.
+ Medium includes using ice grapples to push enemies through doors or off ledges without luring them. Also includes bringing an enemy over to the Temple Door to grapple through it.
+ Hard includes luring or grappling enemies to get to where you want to go.
+ The Medium and Hard options will give the player the Torch to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic.
+ Note: You will still be expected to ice grapple to the slime in East Forest from below with this option off.
+ """
+ internal_name = "ice_grappling"
+ display_name = "Ice Grapple Logic"
+ option_off = 0
+ option_easy = 1
+ option_medium = 2
+ option_hard = 3
+ default = 0
+
+
+class LadderStorage(Choice):
+ """
+ Choose whether Ladder Storage is in logic.
+ Easy includes uses of Ladder Storage to get to open doors over a long distance without too much difficulty. May include convenient elevation changes (going up Mountain stairs, stairs in front of Special Shop, etc.).
+ Medium includes the above as well as changing your elevation using the environment and getting knocked down by melee enemies mid-LS.
+ Hard includes the above as well as going behind the map to enter closed doors from behind, shooting a fuse with the magic wand to knock yourself down at close range, and getting into the Cathedral Secret Legend room mid-LS.
+ Enabling any of these difficulty options will give the player the Torch item to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic.
+ Opening individual chests while doing ladder storage is excluded due to tedium.
+ Knocking yourself out of LS with a bomb is excluded due to the problematic nature of consumables in logic.
+ """
+ internal_name = "ladder_storage"
+ display_name = "Ladder Storage Logic"
+ option_off = 0
+ option_easy = 1
+ option_medium = 2
+ option_hard = 3
+ default = 0
+
+
+class LadderStorageWithoutItems(Toggle):
+ """
+ If disabled, you logically require Stick, Sword, or Magic Orb to perform Ladder Storage.
+ If enabled, you will be expected to perform Ladder Storage without progression items.
+ This can be done with the plushie code, a Golden Coin, Prayer, and many other options.
+
+ This option has no effect if you do not have Ladder Storage Logic enabled.
+ """
+ internal_name = "ladder_storage_without_items"
+ display_name = "Ladder Storage without Items"
+
+
+class LogicRules(Choice):
+ """
+ This option has been superseded by the individual trick options.
+ If set to nmg, it will set Ice Grappling to medium and Laurels Zips on.
+ If set to ur, it will do nmg as well as set Ladder Storage to medium.
+ It is here to avoid breaking old yamls, and will be removed at a later date.
+ """
+ visibility = Visibility.none
+ internal_name = "logic_rules"
+ display_name = "Logic Rules"
+ option_restricted = 0
+ option_no_major_glitches = 1
+ alias_nmg = 1
+ option_unrestricted = 2
+ alias_ur = 2
+ default = 0
+
+
@dataclass
class TunicOptions(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
@@ -199,22 +254,30 @@ class TunicOptions(PerGameCommonOptions):
shuffle_ladders: ShuffleLadders
entrance_rando: EntranceRando
fixed_shop: FixedShop
- logic_rules: LogicRules
fool_traps: FoolTraps
hexagon_quest: HexagonQuest
hexagon_goal: HexagonGoal
extra_hexagon_percentage: ExtraHexagonPercentage
+ laurels_location: LaurelsLocation
lanternless: Lanternless
maskless: Maskless
- laurels_location: LaurelsLocation
+ laurels_zips: LaurelsZips
+ ice_grappling: IceGrappling
+ ladder_storage: LadderStorage
+ ladder_storage_without_items: LadderStorageWithoutItems
plando_connections: TunicPlandoConnections
+
+ logic_rules: LogicRules
tunic_option_groups = [
OptionGroup("Logic Options", [
- LogicRules,
Lanternless,
Maskless,
+ LaurelsZips,
+ IceGrappling,
+ LadderStorage,
+ LadderStorageWithoutItems
])
]
@@ -231,9 +294,12 @@ tunic_option_presets: Dict[str, Dict[str, Any]] = {
"Glace Mode": {
"accessibility": "minimal",
"ability_shuffling": True,
- "entrance_rando": "yes",
+ "entrance_rando": True,
"fool_traps": "onslaught",
- "logic_rules": "unrestricted",
+ "laurels_zips": True,
+ "ice_grappling": "hard",
+ "ladder_storage": "hard",
+ "ladder_storage_without_items": True,
"maskless": True,
"lanternless": True,
},
diff --git a/worlds/tunic/regions.py b/worlds/tunic/regions.py
index c30a44bb8f..93ec5640e0 100644
--- a/worlds/tunic/regions.py
+++ b/worlds/tunic/regions.py
@@ -16,7 +16,8 @@ tunic_regions: Dict[str, Set[str]] = {
"Eastern Vault Fortress": {"Beneath the Vault"},
"Beneath the Vault": {"Eastern Vault Fortress"},
"Quarry Back": {"Quarry"},
- "Quarry": {"Lower Quarry"},
+ "Quarry": {"Monastery", "Lower Quarry"},
+ "Monastery": set(),
"Lower Quarry": {"Rooted Ziggurat"},
"Rooted Ziggurat": set(),
"Swamp": {"Cathedral"},
diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py
index 6875686903..942bbc773a 100644
--- a/worlds/tunic/rules.py
+++ b/worlds/tunic/rules.py
@@ -3,7 +3,7 @@ from typing import Dict, TYPE_CHECKING
from worlds.generic.Rules import set_rule, forbid_item, add_rule
from BaseClasses import CollectionState
-from .options import TunicOptions
+from .options import TunicOptions, LadderStorage, IceGrappling
if TYPE_CHECKING:
from . import TunicWorld
@@ -27,10 +27,10 @@ green_hexagon = "Green Questagon"
blue_hexagon = "Blue Questagon"
gold_hexagon = "Gold Questagon"
+# "Quarry - [East] Bombable Wall" is excluded from this list since it has slightly different rules
bomb_walls = ["East Forest - Bombable Wall", "Eastern Vault Fortress - [East Wing] Bombable Wall",
"Overworld - [Central] Bombable Wall", "Overworld - [Southwest] Bombable Wall Near Fountain",
- "Quarry - [West] Upper Area Bombable Wall", "Quarry - [East] Bombable Wall",
- "Ruined Atoll - [Northwest] Bombable Wall"]
+ "Quarry - [West] Upper Area Bombable Wall", "Ruined Atoll - [Northwest] Bombable Wall"]
def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str, int]:
@@ -64,32 +64,33 @@ def has_sword(state: CollectionState, player: int) -> bool:
return state.has("Sword", player) or state.has("Sword Upgrade", player, 2)
-def has_ice_grapple_logic(long_range: bool, state: CollectionState, world: "TunicWorld") -> bool:
- player = world.player
- if not world.options.logic_rules:
+def laurels_zip(state: CollectionState, world: "TunicWorld") -> bool:
+ return world.options.laurels_zips and state.has(laurels, world.player)
+
+
+def has_ice_grapple_logic(long_range: bool, difficulty: IceGrappling, state: CollectionState, world: "TunicWorld") -> bool:
+ if world.options.ice_grappling < difficulty:
return False
if not long_range:
- return state.has_all({ice_dagger, grapple}, player)
+ return state.has_all({ice_dagger, grapple}, world.player)
else:
- return state.has_all({ice_dagger, fire_wand, grapple}, player) and has_ability(icebolt, state, world)
+ return state.has_all({ice_dagger, fire_wand, grapple}, world.player) and has_ability(icebolt, state, world)
def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool:
- return world.options.logic_rules == "unrestricted" and has_stick(state, world.player)
+ if not world.options.ladder_storage:
+ return False
+ if world.options.ladder_storage_without_items:
+ return True
+ return has_stick(state, world.player) or state.has(grapple, world.player)
def has_mask(state: CollectionState, world: "TunicWorld") -> bool:
- if world.options.maskless:
- return True
- else:
- return state.has(mask, world.player)
+ return world.options.maskless or state.has(mask, world.player)
def has_lantern(state: CollectionState, world: "TunicWorld") -> bool:
- if world.options.lanternless:
- return True
- else:
- return state.has(lantern, world.player)
+ return world.options.lanternless or state.has(lantern, world.player)
def set_region_rules(world: "TunicWorld") -> None:
@@ -102,12 +103,14 @@ def set_region_rules(world: "TunicWorld") -> None:
lambda state: has_stick(state, player) or state.has(fire_wand, player)
world.get_entrance("Overworld -> Dark Tomb").access_rule = \
lambda state: has_lantern(state, world)
+ # laurels in, ladder storage in through the furnace, or ice grapple down the belltower
world.get_entrance("Overworld -> West Garden").access_rule = \
- lambda state: state.has(laurels, player) \
- or can_ladder_storage(state, world)
+ lambda state: (state.has(laurels, player)
+ or can_ladder_storage(state, world)
+ or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
world.get_entrance("Overworld -> Eastern Vault Fortress").access_rule = \
lambda state: state.has(laurels, player) \
- or has_ice_grapple_logic(True, state, world) \
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) \
or can_ladder_storage(state, world)
# using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules
world.get_entrance("Overworld -> Beneath the Vault").access_rule = \
@@ -124,8 +127,8 @@ def set_region_rules(world: "TunicWorld") -> None:
world.get_entrance("Lower Quarry -> Rooted Ziggurat").access_rule = \
lambda state: state.has(grapple, player) and has_ability(prayer, state, world)
world.get_entrance("Swamp -> Cathedral").access_rule = \
- lambda state: state.has(laurels, player) and has_ability(prayer, state, world) \
- or has_ice_grapple_logic(False, state, world)
+ lambda state: (state.has(laurels, player) and has_ability(prayer, state, world)) \
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
world.get_entrance("Overworld -> Spirit Arena").access_rule = \
lambda state: ((state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value
else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player)
@@ -133,10 +136,18 @@ def set_region_rules(world: "TunicWorld") -> None:
and has_ability(prayer, state, world) and has_sword(state, player)
and state.has_any({lantern, laurels}, player))
+ world.get_region("Quarry").connect(world.get_region("Rooted Ziggurat"),
+ rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)
+ and has_ability(prayer, state, world))
+
+ 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))
+
def set_location_rules(world: "TunicWorld") -> None:
player = world.player
- options = world.options
forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player)
@@ -147,11 +158,13 @@ def set_location_rules(world: "TunicWorld") -> None:
lambda state: has_ability(prayer, state, world)
or state.has(laurels, player)
or can_ladder_storage(state, world)
- or (has_ice_grapple_logic(True, state, world) and has_lantern(state, world)))
+ or (has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)
+ and has_lantern(state, world)))
set_rule(world.get_location("Fortress Courtyard - Page Near Cave"),
lambda state: has_ability(prayer, state, world) or state.has(laurels, player)
or can_ladder_storage(state, world)
- or (has_ice_grapple_logic(True, state, world) and has_lantern(state, world)))
+ or (has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)
+ and has_lantern(state, world)))
set_rule(world.get_location("East Forest - Dancing Fox Spirit Holy Cross"),
lambda state: has_ability(holy_cross, state, world))
set_rule(world.get_location("Forest Grave Path - Holy Cross Code by Grave"),
@@ -186,17 +199,17 @@ def set_location_rules(world: "TunicWorld") -> None:
lambda state: state.has(laurels, player))
set_rule(world.get_location("Old House - Normal Chest"),
lambda state: state.has(house_key, player)
- or has_ice_grapple_logic(False, state, world)
- or (state.has(laurels, player) and options.logic_rules))
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
+ or laurels_zip(state, world))
set_rule(world.get_location("Old House - Holy Cross Chest"),
lambda state: has_ability(holy_cross, state, world) and (
state.has(house_key, player)
- or has_ice_grapple_logic(False, state, world)
- or (state.has(laurels, player) and options.logic_rules)))
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
+ or laurels_zip(state, world)))
set_rule(world.get_location("Old House - Shield Pickup"),
lambda state: state.has(house_key, player)
- or has_ice_grapple_logic(False, state, world)
- or (state.has(laurels, player) and options.logic_rules))
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
+ or laurels_zip(state, world))
set_rule(world.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb"),
lambda state: state.has(laurels, player))
set_rule(world.get_location("Overworld - [Southwest] From West Garden"),
@@ -206,7 +219,7 @@ def set_location_rules(world: "TunicWorld") -> None:
or (has_lantern(state, world) and has_sword(state, player))
or can_ladder_storage(state, world))
set_rule(world.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate"),
- lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules)
+ lambda state: state.has_any({grapple, laurels}, player))
set_rule(world.get_location("Overworld - [East] Grapple Chest"),
lambda state: state.has(grapple, player))
set_rule(world.get_location("Special Shop - Secret Page Pickup"),
@@ -215,11 +228,11 @@ def set_location_rules(world: "TunicWorld") -> None:
lambda state: has_ability(holy_cross, state, world)
and (state.has(laurels, player) or (has_lantern(state, world) and (has_sword(state, player)
or state.has(fire_wand, player)))
- or has_ice_grapple_logic(False, state, world)))
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)))
set_rule(world.get_location("Sealed Temple - Page Pickup"),
lambda state: state.has(laurels, player)
or (has_lantern(state, world) and (has_sword(state, player) or state.has(fire_wand, player)))
- or has_ice_grapple_logic(False, state, world))
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
set_rule(world.get_location("West Furnace - Lantern Pickup"),
lambda state: has_stick(state, player) or state.has_any({fire_wand, laurels}, player))
@@ -254,7 +267,7 @@ def set_location_rules(world: "TunicWorld") -> None:
lambda state: state.has(laurels, player) and has_ability(holy_cross, state, world))
set_rule(world.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House"),
lambda state: (state.has(laurels, player) and has_ability(prayer, state, world))
- or has_ice_grapple_logic(True, state, world))
+ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
set_rule(world.get_location("West Garden - [Central Lowlands] Below Left Walkway"),
lambda state: state.has(laurels, player))
set_rule(world.get_location("West Garden - [Central Highlands] After Garden Knight"),
@@ -265,12 +278,15 @@ def set_location_rules(world: "TunicWorld") -> None:
# Ruined Atoll
set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"),
lambda state: state.has(laurels, player))
+ # ice grapple push a crab through the door
set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"),
- lambda state: state.has(laurels, player) or state.has(key, player, 2))
+ lambda state: state.has(laurels, player) or state.has(key, player, 2)
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"),
- lambda state: state.has(laurels, player) or state.has(key, player, 2))
+ lambda state: state.has(laurels, player) or state.has(key, player, 2)
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
set_rule(world.get_location("Librarian - Hexagon Green"),
- lambda state: has_sword(state, player) or options.logic_rules)
+ lambda state: has_sword(state, player))
# Frog's Domain
set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"),
@@ -285,10 +301,12 @@ def set_location_rules(world: "TunicWorld") -> None:
lambda state: state.has(laurels, player))
set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"),
lambda state: has_sword(state, player)
- and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world)))
+ and (has_ability(prayer, state, world)
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)))
set_rule(world.get_location("Fortress Arena - Hexagon Red"),
lambda state: state.has(vault_key, player)
- and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world)))
+ and (has_ability(prayer, state, world)
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)))
# Beneath the Vault
set_rule(world.get_location("Beneath the Fortress - Bridge"),
@@ -301,14 +319,14 @@ def set_location_rules(world: "TunicWorld") -> None:
lambda state: state.has(laurels, player))
set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"),
lambda state: has_sword(state, player) or state.has_all({fire_wand, laurels}, player))
- # nmg - kill boss scav with orb + firecracker, or similar
set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"),
- lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules))
+ lambda state: has_sword(state, player))
# Swamp
set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"),
lambda state: (state.has(fire_wand, player) and has_sword(state, player))
- and (state.has(laurels, player) or has_ice_grapple_logic(False, state, world)))
+ and (state.has(laurels, player)
+ or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)))
set_rule(world.get_location("Swamp - [Entrance] Above Entryway"),
lambda state: state.has(laurels, player))
set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"),
@@ -335,8 +353,16 @@ def set_location_rules(world: "TunicWorld") -> None:
# Bombable Walls
for location_name in bomb_walls:
# has_sword is there because you can buy bombs in the shop
- set_rule(world.get_location(location_name), lambda state: state.has(gun, player) or has_sword(state, player))
+ set_rule(world.get_location(location_name),
+ lambda state: state.has(gun, player)
+ or has_sword(state, player)
+ or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
add_rule(world.get_location("Cube Cave - Holy Cross Chest"),
+ lambda state: state.has(gun, player)
+ or has_sword(state, player)
+ or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
+ # can't ice grapple to this one, not enough space
+ set_rule(world.get_location("Quarry - [East] Bombable Wall"),
lambda state: state.has(gun, player) or has_sword(state, player))
# Shop
diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py
index 72d4a498d1..bbceb7468f 100644
--- a/worlds/tunic/test/test_access.py
+++ b/worlds/tunic/test/test_access.py
@@ -68,3 +68,57 @@ class TestER(TunicTestBase):
self.assertFalse(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross"))
self.collect_by_name(["Pages 42-43 (Holy Cross)"])
self.assertTrue(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross"))
+
+
+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.IceGrappling.internal_name: options.IceGrappling.option_easy,
+ "plando_connections": [
+ {
+ "entrance": "Stick House Entrance",
+ "exit": "Ziggurat Portal Room Entrance"
+ },
+ {
+ "entrance": "Ziggurat Lower to Ziggurat Tower",
+ "exit": "Secret Gathering Place Exit"
+ }
+ ]}
+ # with these plando connections, you need to ice grapple from the back of lower zig to the front to get laurels
+
+
+# ensure that ladder storage connections connect to the outlet region, not the portal's region
+class TestLadderStorage(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.LadderStorage.internal_name: options.LadderStorage.option_hard,
+ options.LadderStorageWithoutItems.internal_name: options.LadderStorageWithoutItems.option_false,
+ "plando_connections": [
+ {
+ "entrance": "Fortress Courtyard Shop",
+ # "exit": "Ziggurat Portal Room Exit"
+ "exit": "Spawn to Far Shore"
+ },
+ {
+ "entrance": "Fortress Courtyard to Beneath the Vault",
+ "exit": "Stick House Exit"
+ },
+ {
+ "entrance": "Stick House Entrance",
+ "exit": "Fortress Courtyard to Overworld"
+ },
+ {
+ "entrance": "Old House Waterfall Entrance",
+ "exit": "Ziggurat Portal Room Entrance"
+ },
+ ]}
+
+ def test_ls_to_shop_entrance(self) -> None:
+ self.collect_by_name(["Magic Orb"])
+ 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"))
diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py
index 6229e5ffc9..b4b38c883e 100644
--- a/worlds/witness/__init__.py
+++ b/worlds/witness/__init__.py
@@ -61,7 +61,7 @@ class WitnessWorld(World):
item_name_groups = static_witness_items.ITEM_GROUPS
location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS
- required_client_version = (0, 4, 5)
+ required_client_version = (0, 5, 1)
player_logic: WitnessPlayerLogic
player_locations: WitnessPlayerLocations
@@ -204,11 +204,10 @@ class WitnessWorld(World):
]
if early_items:
random_early_item = self.random.choice(early_items)
- if (
- self.options.puzzle_randomization == "sigma_expert"
- or self.options.victory_condition == "panel_hunt"
- ):
- # In Expert and Panel Hunt, only tag the item as early, rather than forcing it onto the gate.
+ mode = self.options.puzzle_randomization
+ if mode == "sigma_expert" or mode == "umbra_variety" or self.options.victory_condition == "panel_hunt":
+ # In Expert and Variety, only tag the item as early, rather than forcing it onto the gate.
+ # Same with panel hunt, since the Tutorial Gate Open panel is used for something else
self.multiworld.local_early_items[self.player][random_early_item] = 1
else:
# Force the item onto the tutorial gate check and remove it from our random pool.
@@ -255,7 +254,7 @@ class WitnessWorld(World):
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
warning(
- f"""Location "{loc}" had to be added to {self.player_name}'s world
+ f"""Location "{loc}" had to be added to {self.player_name}'s world
due to insufficient sphere 1 size."""
)
diff --git a/worlds/witness/data/WitnessLogicVariety.txt b/worlds/witness/data/WitnessLogicVariety.txt
new file mode 100644
index 0000000000..a3c388dfb1
--- /dev/null
+++ b/worlds/witness/data/WitnessLogicVariety.txt
@@ -0,0 +1,1223 @@
+==Tutorial (Inside)==
+
+Menu (Menu) - Entry - True:
+
+Entry (Entry):
+
+Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064:
+158000 - 0x00064 (Straight) - True - True
+159510 - 0x01848 (EP) - 0x00064 - True
+
+Tutorial First Hallway Room (Tutorial First Hallway) - Tutorial - 0x00182:
+158001 - 0x00182 (Bend) - True - True
+
+Tutorial (Tutorial) - Outside Tutorial - True:
+158002 - 0x00293 (Front Center) - True - Dots
+158003 - 0x00295 (Center Left) - 0x00293 - Black/White Squares & Colored Squares
+158004 - 0x002C2 (Front Left) - 0x00295 - Stars
+158005 - 0x0A3B5 (Back Left) - True - True
+158006 - 0x0A3B2 (Back Right) - True - True
+158007 - 0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 & 0x0A3B2 - True
+158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - True
+158009 - 0x0C335 (Pillar) - True - Triangles
+158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots
+159512 - 0x33530 (Cloud EP) - True - True
+159513 - 0x33600 (Patio Flowers EP) - 0x0C373 - True
+159517 - 0x3352F (Gate EP) - 0x03505 - True
+
+==Tutorial (Outside)==
+
+Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2 - Outside Tutorial Vault - 0x033D0:
+158650 - 0x033D4 (Vault Panel) - True - Dots & Black/White Squares & Colored Squares & Symmetry
+Door - 0x033D0 (Vault Door) - 0x033D4
+158013 - 0x0005D (Shed Row 1) - True - Dots & Full Dots & Black/White Squares & Colored Squares
+158014 - 0x0005E (Shed Row 2) - 0x0005D - Dots & Full Dots & Stars
+158015 - 0x0005F (Shed Row 3) - 0x0005E - Dots & Full Dots & Shapers & Negative Shapers
+158016 - 0x00060 (Shed Row 4) - 0x0005F - Dots & Full Dots & Black/White Squares & Stars & Stars + Same Colored Symbol & Eraser
+158017 - 0x00061 (Shed Row 5) - 0x00060 - Dots & Full Dots & Triangles
+158018 - 0x018AF (Tree Row 1) - True - Arrows
+158019 - 0x0001B (Tree Row 2) - 0x018AF - Arrows
+158020 - 0x012C9 (Tree Row 3) - 0x0001B - Arrows
+158021 - 0x0001C (Tree Row 4) - 0x012C9 - Arrows & Black/White Squares & Colored Squares
+158022 - 0x0001D (Tree Row 5) - 0x0001C - Arrows & Black/White Squares & Colored Squares
+158023 - 0x0001E (Tree Row 6) - 0x0001D - Arrows & Black/White Squares & Colored Squares
+158024 - 0x0001F (Tree Row 7) - 0x0001E - Arrows & Black/White Squares & Colored Squares
+158025 - 0x00020 (Tree Row 8) - 0x0001F - Arrows & Black/White Squares & Colored Squares
+158026 - 0x00021 (Tree Row 9) - 0x00020 - Arrows & Black/White Squares & Colored Squares
+Door - 0x03BA2 (Outpost Path) - 0x0A3B5
+159511 - 0x03D06 (Garden EP) - True - True
+159514 - 0x28A2F (Town Sewer EP) - True - True
+159516 - 0x334A3 (Path EP) - True - True
+159500 - 0x035C7 (Tractor EP) - True - True
+
+Outside Tutorial Vault (Outside Tutorial):
+158651 - 0x03481 (Vault Box) - True - True
+
+Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170:
+158011 - 0x0A171 (Outpost Entry Panel) - True - Dots & Full Dots & Triangles & Black/White Squares
+Door - 0x0A170 (Outpost Entry) - 0x0A171
+
+Outside Tutorial Outpost (Outside Tutorial) - Outside Tutorial - 0x04CA3:
+158012 - 0x04CA4 (Outpost Exit Panel) - True - Dots & Full Dots & Triangles & Black/White Squares
+Door - 0x04CA3 (Outpost Exit) - 0x04CA4
+158600 - 0x17CFB (Discard) - True - Arrows & Triangles
+
+Orchard (Orchard) - Main Island - True - Orchard Beyond First Gate - 0x03307:
+158071 - 0x00143 (Apple Tree 1) - True - True
+158072 - 0x0003B (Apple Tree 2) - 0x00143 - True
+158073 - 0x00055 (Apple Tree 3) - 0x0003B - True
+Door - 0x03307 (First Gate) - 0x00055
+
+Orchard Beyond First Gate (Orchard) - Orchard End - 0x03313:
+158074 - 0x032F7 (Apple Tree 4) - 0x00055 - True
+158075 - 0x032FF (Apple Tree 5) - 0x032F7 - True
+Door - 0x03313 (Second Gate) - 0x032FF
+
+Orchard End (Orchard):
+
+Main Island (Main Island) - Outside Tutorial - True:
+159801 - 0xFFD00 (Reached Independently) - True - True
+
+==Glass Factory==
+
+Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29:
+158027 - 0x01A54 (Entry Panel) - True - Symmetry
+Door - 0x01A29 (Entry) - 0x01A54
+158601 - 0x3C12B (Discard) - True - Arrows & Triangles
+159002 - 0x28B8A (Vase EP) - 0x01A54 - True
+
+Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0x0D7ED:
+158028 - 0x00086 (Back Wall 1) - True - Symmetry
+158029 - 0x00087 (Back Wall 2) - 0x00086 - Symmetry
+158030 - 0x00059 (Back Wall 3) - 0x00087 - Symmetry
+158031 - 0x00062 (Back Wall 4) - 0x00059 - Symmetry
+158032 - 0x0005C (Back Wall 5) - 0x00062 - Symmetry
+158033 - 0x0008D (Front 1) - 0x0005C - Symmetry & Dots
+158034 - 0x00081 (Front 2) - 0x0008D - Symmetry & Dots
+158035 - 0x00083 (Front 3) - 0x00081 - Symmetry & Dots
+158036 - 0x00084 (Melting 1) - 0x00083 - Symmetry & Dots
+158037 - 0x00082 (Melting 2) - 0x00084 - Symmetry & Dots
+158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry & Dots
+Door - 0x0D7ED (Back Wall) - 0x0005C
+
+Inside Glass Factory Behind Back Wall (Glass Factory) - The Ocean - 0x17CC8:
+158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat
+
+==Symmetry Island==
+
+Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E:
+158040 - 0x000B0 (Lower Panel) - 0x0343A - Dots
+Door - 0x17F3E (Lower) - 0x000B0
+
+Symmetry Island Lower (Symmetry Island) - Symmetry Island Upper - 0x18269:
+158041 - 0x00022 (Right 1) - True - Symmetry & Dots & Full Dots & Triangles
+158042 - 0x00023 (Right 2) - 0x00022 - Symmetry & Dots & Full Dots & Triangles
+158043 - 0x00024 (Right 3) - 0x00023 - Symmetry & Dots & Full Dots & Triangles
+158044 - 0x00025 (Right 4) - 0x00024 - Symmetry & Dots & Full Dots & Triangles
+158045 - 0x00026 (Right 5) - 0x00025 - Symmetry & Dots & Full Dots & Triangles
+158046 - 0x0007C (Back 1) - 0x00026 - Symmetry & Dots & Colored Dots
+158047 - 0x0007E (Back 2) - 0x0007C - Symmetry & Dots & Colored Dots
+158048 - 0x00075 (Back 3) - 0x0007E - Symmetry & Dots & Colored Dots
+158049 - 0x00073 (Back 4) - 0x00075 - Symmetry & Dots & Colored Dots & Eraser
+158050 - 0x00077 (Back 5) - 0x00073 - Symmetry & Dots & Colored Dots & Eraser
+158051 - 0x00079 (Back 6) - 0x00077 - Symmetry & Dots & Colored Dots & Eraser
+158052 - 0x00065 (Left 1) - 0x00079 - Symmetry & Dots & Colored Dots
+158053 - 0x0006D (Left 2) - 0x00065 - Symmetry & Colored Squares
+158054 - 0x00072 (Left 3) - 0x0006D - Symmetry & Stars
+158055 - 0x0006F (Left 4) - 0x00072 - Symmetry & Stars & Stars + Same Colored Symbol & Colored Squares
+158056 - 0x00070 (Left 5) - 0x0006F - Symmetry & Stars & Stars + Same Colored Symbol & Colored Squares
+158057 - 0x00071 (Left 6) - 0x00070 - Symmetry & Colored Dots & Eraser
+158058 - 0x00076 (Left 7) - 0x00071 - Symmetry & Dots & Eraser
+158059 - 0x009B8 (Scenery Outlines 1) - True - Symmetry
+158060 - 0x003E8 (Scenery Outlines 2) - 0x009B8 - Symmetry
+158061 - 0x00A15 (Scenery Outlines 3) - 0x003E8 - Symmetry
+158062 - 0x00B53 (Scenery Outlines 4) - 0x00A15 - Symmetry
+158063 - 0x00B8D (Scenery Outlines 5) - 0x00B53 - Symmetry
+158064 - 0x1C349 (Upper Panel) - 0x00076 - Symmetry & Dots
+Door - 0x18269 (Upper) - 0x1C349
+159000 - 0x0332B (Glass Factory Black Line Reflection EP) - True - True
+
+Symmetry Island Upper (Symmetry Island):
+158065 - 0x00A52 (Laser Yellow 1) - True - Symmetry & Colored Dots
+158066 - 0x00A57 (Laser Yellow 2) - 0x00A52 - Symmetry & Colored Dots
+158067 - 0x00A5B (Laser Yellow 3) - 0x00A57 - Symmetry & Colored Dots
+158068 - 0x00A61 (Laser Blue 1) - 0x00A52 - Symmetry & Colored Dots
+158069 - 0x00A64 (Laser Blue 2) - 0x00A61 & 0x00A57 - Symmetry & Colored Dots
+158070 - 0x00A68 (Laser Blue 3) - 0x00A64 & 0x00A5B - Symmetry & Colored Dots
+158700 - 0x0360D (Laser Panel) - 0x00A68 - True
+Laser - 0x00509 (Laser) - 0x0360D
+159001 - 0x03367 (Glass Factory Black Line EP) - True - True
+
+==Desert==
+
+Desert Obelisk (Desert) - Entry - True:
+159700 - 0xFFE00 (Obelisk Side 1) - 0x0332B & 0x03367 & 0x28B8A - True
+159701 - 0xFFE01 (Obelisk Side 2) - 0x037B6 & 0x037B2 & 0x000F7 - True
+159702 - 0xFFE02 (Obelisk Side 3) - 0x3351D - True
+159703 - 0xFFE03 (Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True
+159704 - 0xFFE04 (Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True
+159709 - 0x00359 (Obelisk) - True - True
+
+Desert Outside (Desert) - Main Island - True - Desert Light Room - 0x09FEE - Desert Vault - 0x03444:
+158652 - 0x0CC7B (Vault Panel) - True - Dots & Full Dots & Rotated Shapers & Negative Shapers & Stars & Stars + Same Colored Symbol
+Door - 0x03444 (Vault Door) - 0x0CC7B
+158602 - 0x17CE7 (Discard) - True - Arrows & Triangles
+158076 - 0x00698 (Surface 1) - True - True
+158077 - 0x0048F (Surface 2) - 0x00698 - True
+158078 - 0x09F92 (Surface 3) - 0x0048F & 0x09FA0 - True
+158079 - 0x09FA0 (Surface 3 Control) - 0x0048F - True
+158080 - 0x0A036 (Surface 4) - 0x09F92 - True
+158081 - 0x09DA6 (Surface 5) - 0x09F92 - True
+158082 - 0x0A049 (Surface 6) - 0x09F92 - True
+158083 - 0x0A053 (Surface 7) - 0x0A036 & 0x09DA6 & 0x0A049 - True
+158084 - 0x09F94 (Surface 8) - 0x0A053 & 0x09F86 - True
+158085 - 0x09F86 (Surface 8 Control) - 0x0A053 - True
+158086 - 0x0C339 (Light Room Entry Panel) - 0x09F94 - True
+Door - 0x09FEE (Light Room Entry) - 0x0C339
+158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True
+Laser - 0x012FB (Laser) - 0x03608
+159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True
+159020 - 0x3351D (Sand Snake EP) - True - True
+159030 - 0x0053C (Facade Right EP) - True - True
+159031 - 0x00771 (Facade Left EP) - True - True
+159032 - 0x335C8 (Stairs Left EP) - True - True
+159033 - 0x335C9 (Stairs Right EP) - True - True
+159036 - 0x220E4 (Broken Wall Straight EP) - True - True
+159037 - 0x220E5 (Broken Wall Bend EP) - True - True
+159040 - 0x334B9 (Shore EP) - True - True
+159041 - 0x334BC (Island EP) - True - True
+
+Desert Vault (Desert):
+158653 - 0x0339E (Vault Box) - True - True
+
+Desert Light Room (Desert) - Desert Pond Room - 0x0C2C3:
+158087 - 0x09FAA (Light Control) - True - True
+158088 - 0x00422 (Light Room 1) - 0x09FAA - True
+158089 - 0x006E3 (Light Room 2) - 0x09FAA - True
+158090 - 0x0A02D (Light Room 3) - 0x09FAA & 0x00422 & 0x006E3 - True
+Door - 0x0C2C3 (Pond Room Entry) - 0x0A02D
+
+Desert Pond Room (Desert) - Desert Flood Room - 0x0A24B:
+158091 - 0x00C72 (Pond Room 1) - True - True
+158092 - 0x0129D (Pond Room 2) - 0x00C72 - True
+158093 - 0x008BB (Pond Room 3) - 0x0129D - True
+158094 - 0x0078D (Pond Room 4) - 0x008BB - True
+158095 - 0x18313 (Pond Room 5) - 0x0078D - True
+158096 - 0x0A249 (Flood Room Entry Panel) - 0x18313 - True
+Door - 0x0A24B (Flood Room Entry) - 0x0A249
+159043 - 0x0A14C (Pond Room Near Reflection EP) - True - True
+159044 - 0x0A14D (Pond Room Far Reflection EP) - True - True
+
+Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316:
+158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True
+158098 - 0x1831E (Reduce Water Level Far Right) - True - True
+158099 - 0x1C260 (Reduce Water Level Near Left) - True - True
+158100 - 0x1831C (Reduce Water Level Near Right) - True - True
+158101 - 0x1C2F3 (Raise Water Level Far Left) - True - True
+158102 - 0x1831D (Raise Water Level Far Right) - True - True
+158103 - 0x1C2B1 (Raise Water Level Near Left) - True - True
+158104 - 0x1831B (Raise Water Level Near Right) - True - True
+158105 - 0x04D18 (Flood Room 1) - 0x1C260 & 0x1831C - True
+158106 - 0x01205 (Flood Room 2) - 0x04D18 & 0x1C260 & 0x1831C - True
+158107 - 0x181AB (Flood Room 3) - 0x01205 & 0x1C260 & 0x1831C - True
+158108 - 0x0117A (Flood Room 4) - 0x181AB & 0x1C260 & 0x1831C - True
+158109 - 0x17ECA (Flood Room 5) - 0x0117A & 0x1C260 & 0x1831C - True
+158110 - 0x18076 (Flood Room 6) - 0x17ECA & 0x1C260 & 0x1831C - True
+Door - 0x0C316 (Elevator Room Entry) - 0x18076
+159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True
+
+Desert Elevator Room (Desert) - Desert Behind Elevator - 0x01317:
+158111 - 0x17C31 (Elevator Room Transparent) - True - True
+158113 - 0x012D7 (Elevator Room Hexagonal) - 0x17C31 & 0x0A015 - True
+158114 - 0x0A015 (Elevator Room Hexagonal Control) - 0x17C31 - True
+158115 - 0x0A15C (Elevator Room Bent 1) - True - True
+158116 - 0x09FFF (Elevator Room Bent 2) - 0x0A15C - True
+158117 - 0x0A15F (Elevator Room Bent 3) - 0x09FFF - True
+159035 - 0x037BB (Elevator EP) - 0x01317 - True
+Door - 0x01317 (Elevator) - 0x03608
+
+Desert Behind Elevator (Desert):
+
+==Quarry==
+
+Quarry Obelisk (Quarry) - Entry - True:
+159740 - 0xFFE40 (Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True
+159741 - 0xFFE41 (Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True
+159742 - 0xFFE42 (Obelisk Side 3) - 0x289CF & 0x289D1 - True
+159743 - 0xFFE43 (Obelisk Side 4) - 0x33692 - True
+159744 - 0xFFE44 (Obelisk Side 5) - 0x03E77 & 0x03E7C - True
+159749 - 0x22073 (Obelisk) - True - True
+
+Outside Quarry (Quarry) - Main Island - True - Quarry Between Entry Doors - 0x09D6F - Quarry Elevator - 0xFFD00 & 0xFFD01:
+158118 - 0x09E57 (Entry 1 Panel) - True - Black/White Squares & Dots
+158603 - 0x17CF0 (Discard) - True - Arrows & Triangles
+158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Shapers
+Laser - 0x01539 (Laser) - 0x03612
+Door - 0x09D6F (Entry 1) - 0x09E57
+159404 - 0x28A4A (Shore EP) - True - True
+159410 - 0x334B6 (Entrance Pipe EP) - True - True
+159412 - 0x28A4C (Sand Pile EP) - True - True
+159420 - 0x289CF (Rock Line EP) - True - True
+159421 - 0x289D1 (Rock Line Reflection EP) - True - True
+
+Quarry Elevator (Quarry) - Outside Quarry - 0x17CC4 - Quarry - 0x17CC4:
+158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser
+159403 - 0x17CB9 (Railroad EP) - 0x17CC4 - True
+
+Quarry Between Entry Doors (Quarry) - Quarry - 0x17C07:
+158119 - 0x17C09 (Entry 2 Panel) - True - Shapers & Eraser
+Door - 0x17C07 (Entry 2) - 0x17C09
+
+Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010:
+159802 - 0xFFD01 (Inside Reached Independently) - True - True
+158121 - 0x01E5A (Stoneworks Entry Left Panel) - True - Stars & Stars + Same Colored Symbol & Eraser
+158122 - 0x01E59 (Stoneworks Entry Right Panel) - True - Triangles
+Door - 0x02010 (Stoneworks Entry) - 0x01E59 & 0x01E5A
+
+Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8 - Quarry Stoneworks Lift - TrueOneWay:
+158123 - 0x275ED (Side Exit Panel) - True - True
+Door - 0x275FF (Side Exit) - 0x275ED
+158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser
+158145 - 0x17CAC (Roof Exit Panel) - True - True
+Door - 0x17CE8 (Roof Exit) - 0x17CAC
+
+Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - TrueOneWay:
+158125 - 0x00E0C (Lower Row 1) - True - Dots & Eraser
+158126 - 0x01489 (Lower Row 2) - 0x00E0C - Dots & Eraser
+158127 - 0x0148A (Lower Row 3) - 0x01489 - Dots & Eraser
+158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Eraser
+158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Dots & Eraser
+158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Dots & Eraser
+
+Quarry Stoneworks Lift (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03679 - Quarry Stoneworks Ground Floor - 0x03679 - Quarry Stoneworks Upper Floor - 0x03679:
+158131 - 0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser
+
+Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - 0x03675 - Quarry Stoneworks Ground Floor - 0x0368A:
+158132 - 0x03676 (Upper Ramp Control) - True - Dots & Eraser
+158133 - 0x03675 (Upper Lift Control) - True - Dots & Eraser
+158134 - 0x00557 (Upper Row 1) - True - Colored Squares & Eraser
+158135 - 0x005F1 (Upper Row 2) - 0x00557 - Colored Squares & Eraser
+158136 - 0x00620 (Upper Row 3) - 0x005F1 - Colored Squares & Eraser
+158137 - 0x009F5 (Upper Row 4) - 0x00620 - Colored Squares & Eraser
+158138 - 0x0146C (Upper Row 5) - 0x009F5 - Stars & Stars + Same Colored Symbol & Eraser
+158139 - 0x3C12D (Upper Row 6) - 0x0146C - Stars & Stars + Same Colored Symbol & Eraser
+158140 - 0x03686 (Upper Row 7) - 0x3C12D - Stars & Stars + Same Colored Symbol & Eraser
+158141 - 0x014E9 (Upper Row 8) - 0x03686 - Stars & Stars + Same Colored Symbol & Eraser
+158142 - 0x03677 (Stairs Panel) - True - Colored Squares & Eraser
+Door - 0x0368A (Stairs) - 0x03677
+158143 - 0x3C125 (Control Room Left) - 0x014E9 - Black/White Squares & Dots & Eraser & Symmetry
+158144 - 0x0367C (Control Room Right) - 0x014E9 - Colored Squares & Dots & Eraser & Stars & Stars + Same Colored Symbol
+159411 - 0x0069D (Ramp EP) - 0x03676 & 0x275FF - True
+159413 - 0x00614 (Lift EP) - 0x275FF & 0x03675 - True
+
+Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Front - 0x03852 - Quarry Boathouse Behind Staircase - 0x2769B:
+158146 - 0x034D4 (Intro Left) - True - Stars
+158147 - 0x021D5 (Intro Right) - True - Shapers & Rotated Shapers & Triangles
+158148 - 0x03852 (Ramp Height Control) - 0x034D4 & 0x021D5 - Rotated Shapers
+158166 - 0x17CA6 (Boat Spawn) - True - Boat
+Door - 0x2769B (Dock) - 0x17CA6
+Door - 0x27163 (Dock Invis Barrier) - 0x17CA6
+
+Quarry Boathouse Behind Staircase (Quarry Boathouse) - The Ocean - 0x17CA6:
+
+Quarry Boathouse Upper Front (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x17C50:
+158149 - 0x021B3 (Front Row 1) - True - Shapers & Eraser
+158150 - 0x021B4 (Front Row 2) - 0x021B3 - Shapers & Eraser
+158151 - 0x021B0 (Front Row 3) - 0x021B4 - Shapers & Eraser
+158152 - 0x021AF (Front Row 4) - 0x021B0 - Shapers & Eraser
+158153 - 0x021AE (Front Row 5) - 0x021AF - Shapers & Eraser
+Door - 0x17C50 (First Barrier) - 0x021AE
+
+Quarry Boathouse Upper Middle (Quarry Boathouse) - Quarry Boathouse Upper Back - 0x03858:
+158154 - 0x03858 (Ramp Horizontal Control) - True - Shapers & Eraser
+159402 - 0x00859 (Moving Ramp EP) - 0x03858 & 0x03852 & 0x3865F - True
+
+Quarry Boathouse Upper Back (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x3865F:
+158155 - 0x38663 (Second Barrier Panel) - True - True
+Door - 0x3865F (Second Barrier) - 0x38663
+158156 - 0x021B5 (Back First Row 1) - True - Shapers & Negative Shapers & Eraser
+158157 - 0x021B6 (Back First Row 2) - 0x021B5 - Shapers & Negative Shapers & Eraser
+158158 - 0x021B7 (Back First Row 3) - 0x021B6 - Shapers & Negative Shapers & Eraser
+158159 - 0x021BB (Back First Row 4) - 0x021B7 - Shapers & Negative Shapers & Eraser
+158160 - 0x09DB5 (Back First Row 5) - 0x021BB - Shapers & Negative Shapers & Eraser
+158161 - 0x09DB1 (Back First Row 6) - 0x09DB5 - Stars & Stars + Same Colored Symbol & Eraser & Colored Squares
+158162 - 0x3C124 (Back First Row 7) - 0x09DB1 - Stars & Stars + Same Colored Symbol & Eraser & Colored Squares
+158163 - 0x09DB3 (Back First Row 8) - 0x3C124 - Stars & Stars + Same Colored Symbol & Eraser & Colored Squares & Triangles
+158164 - 0x09DB4 (Back First Row 9) - 0x09DB3 - Stars & Stars + Same Colored Symbol & Eraser & Colored Squares & Triangles
+158165 - 0x275FA (Hook Control) - True - Shapers & Eraser
+158167 - 0x0A3CB (Back Second Row 1) - 0x09DB4 - Black/White Squares & Colored Squares & Eraser & Shapers
+158168 - 0x0A3CC (Back Second Row 2) - 0x0A3CB - Stars & Stars + Same Colored Symbol & Eraser & Shapers
+158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Triangles & Eraser & Shapers
+159401 - 0x005F6 (Hook EP) - 0x275FA & 0x03852 & 0x3865F - True
+
+==Shadows==
+
+Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 | 0x19665:
+158170 - 0x334DB (Door Timer Outside) - True - True
+Door - 0x19B24 (Timed Door) - 0x334DB | 0x334DC
+158171 - 0x0AC74 (Intro 6) - 0x0A8DC - True
+158172 - 0x0AC7A (Intro 7) - 0x0AC74 - True
+158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - True
+158174 - 0x386FA (Far 1) - 0x0A8E0 - True
+158175 - 0x1C33F (Far 2) - 0x386FA - True
+158176 - 0x196E2 (Far 3) - 0x1C33F - True
+158177 - 0x1972A (Far 4) - 0x196E2 - True
+158178 - 0x19809 (Far 5) - 0x1972A - True
+158179 - 0x19806 (Far 6) - 0x19809 - True
+158180 - 0x196F8 (Far 7) - 0x19806 - True
+158181 - 0x1972F (Far 8) - 0x196F8 - True
+Door - 0x194B2 (Laser Entry Right) - 0x1972F
+158182 - 0x19797 (Near 1) - 0x0A8E0 - True
+158183 - 0x1979A (Near 2) - 0x19797 - True
+158184 - 0x197E0 (Near 3) - 0x1979A - True
+158185 - 0x197E8 (Near 4) - 0x197E0 - True
+158186 - 0x197E5 (Near 5) - 0x197E8 - True
+Door - 0x19665 (Laser Entry Left) - 0x197E5
+159400 - 0x28A7B (Quarry Stoneworks Rooftop Vent EP) - True - True
+
+Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF:
+158187 - 0x334DC (Door Timer Inside) - True - True
+158188 - 0x198B5 (Intro 1) - True - True
+158189 - 0x198BD (Intro 2) - 0x198B5 - True
+158190 - 0x198BF (Intro 3) - 0x198BD & 0x19B24 - True
+Door - 0x19865 (Quarry Barrier) - 0x198BF
+Door - 0x0A2DF (Quarry Barrier 2) - 0x198BF
+158191 - 0x19771 (Intro 4) - 0x198BF - True
+158192 - 0x0A8DC (Intro 5) - 0x19771 - True
+Door - 0x1855B (Ledge Barrier) - 0x0A8DC
+Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC
+
+Shadows Laser Room (Shadows):
+158703 - 0x19650 (Laser Panel) - 0x194B2 & 0x19665 - True
+Laser - 0x181B3 (Laser) - 0x19650
+
+==Keep==
+
+Outside Keep (Keep) - Main Island - True:
+159430 - 0x03E77 (Red Flowers EP) - True - True
+159431 - 0x03E7C (Purple Flowers EP) - True - True
+
+Keep (Keep) - Outside Keep - True - Keep 2nd Maze - 0x01954 - Keep 2nd Pressure Plate - 0x01BEC:
+158193 - 0x00139 (Hedge Maze 1) - True - True
+158197 - 0x0A3A8 (Reset Pressure Plates 1) - True - True
+158198 - 0x033EA (Pressure Plates 1) - 0x0A3A8 - Dots & Triangles
+Door - 0x01954 (Hedge Maze 1 Exit) - 0x00139
+Door - 0x01BEC (Pressure Plates 1 Exit) - 0x033EA
+
+Keep 2nd Maze (Keep) - Keep - 0x018CE - Keep 3rd Maze - 0x019D8:
+Door - 0x018CE (Hedge Maze 2 Shortcut) - 0x00139
+158194 - 0x019DC (Hedge Maze 2) - True - True
+Door - 0x019D8 (Hedge Maze 2 Exit) - 0x019DC
+
+Keep 3rd Maze (Keep) - Keep - 0x019B5 - Keep 4th Maze - 0x019E6:
+Door - 0x019B5 (Hedge Maze 3 Shortcut) - 0x019DC
+158195 - 0x019E7 (Hedge Maze 3) - True - True
+Door - 0x019E6 (Hedge Maze 3 Exit) - 0x019E7
+
+Keep 4th Maze (Keep) - Keep - 0x0199A - Keep Tower - 0x01A0E:
+Door - 0x0199A (Hedge Maze 4 Shortcut) - 0x019E7
+158196 - 0x01A0F (Hedge Maze 4) - True - True
+Door - 0x01A0E (Hedge Maze 4 Exit) - 0x01A0F
+
+Keep 2nd Pressure Plate (Keep) - Keep 3rd Pressure Plate - True:
+158199 - 0x0A3B9 (Reset Pressure Plates 2) - True - True
+158200 - 0x01BE9 (Pressure Plates 2) - 0x0A3B9 - Stars & Stars + Same Colored Symbol & Triangles
+Door - 0x01BEA (Pressure Plates 2 Exit) - 0x01BE9
+
+Keep 3rd Pressure Plate (Keep) - Keep 4th Pressure Plate - 0x01CD5:
+158201 - 0x0A3BB (Reset Pressure Plates 3) - True - True
+158202 - 0x01CD3 (Pressure Plates 3) - 0x0A3BB - Shapers & Stars
+Door - 0x01CD5 (Pressure Plates 3 Exit) - 0x01CD3
+
+Keep 4th Pressure Plate (Keep) - Shadows - 0x09E3D - Keep Tower - 0x01D40:
+158203 - 0x0A3AD (Reset Pressure Plates 4) - True - True
+158204 - 0x01D3F (Pressure Plates 4) - 0x0A3AD - Shapers & Rotated Shapers & Negative Shapers & Symmetry & Dots
+Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F
+158604 - 0x17D27 (Discard) - True - Arrows & Triangles
+158205 - 0x09E49 (Shadows Shortcut Panel) - True - True
+Door - 0x09E3D (Shadows Shortcut) - 0x09E49
+
+Keep Tower (Keep) - Keep - 0x04F8F:
+158206 - 0x0361B (Tower Shortcut Panel) - True - True
+Door - 0x04F8F (Tower Shortcut) - 0x0361B
+158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - True
+158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Dots & Shapers & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol
+Laser - 0x014BB (Laser) - 0x0360E | 0x03317
+159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True
+159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True
+159242 - 0x033DD (Pressure Plates 3 EP) - 0x01CD3 & 0x01CD5 - True
+159243 - 0x033E5 (Pressure Plates 4 Left Exit EP) - 0x01D3F - True
+159244 - 0x018B6 (Pressure Plates 4 Right Exit EP) - 0x01D3F - True
+159250 - 0x28AE9 (Path EP) - True - True
+159251 - 0x3348F (Hedges EP) - True - True
+
+==Shipwreck==
+
+Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True - Shipwreck Vault - 0x17BB4:
+158654 - 0x00AFB (Vault Panel) - True - Symmetry & Sound Dots & Colored Dots
+Door - 0x17BB4 (Vault Door) - 0x00AFB
+158605 - 0x17D28 (Discard) - True - Arrows & Triangles
+159220 - 0x03B22 (Circle Far EP) - True - True
+159221 - 0x03B23 (Circle Left EP) - True - True
+159222 - 0x03B24 (Circle Near EP) - True - True
+159224 - 0x03A79 (Stern EP) - True - True
+159225 - 0x28ABD (Rope Inner EP) - True - True
+159226 - 0x28ABE (Rope Outer EP) - True - True
+159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True
+
+Shipwreck Vault (Shipwreck):
+158655 - 0x03535 (Vault Box) - True - True
+
+==Monastery==
+
+Monastery Obelisk (Monastery) - Entry - True:
+159710 - 0xFFE10 (Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True
+159711 - 0xFFE11 (Obelisk Side 2) - 0x03AC5 - True
+159712 - 0xFFE12 (Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True
+159713 - 0xFFE13 (Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True
+159714 - 0xFFE14 (Obelisk Side 5) - 0x03E01 - True
+159715 - 0xFFE15 (Obelisk Side 6) - 0x289F4 & 0x289F5 - True
+159719 - 0x00263 (Obelisk) - True - True
+
+Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750:
+158207 - 0x03713 (Laser Shortcut Panel) - True - True
+Door - 0x0364E (Laser Shortcut) - 0x03713
+158208 - 0x00B10 (Entry Left) - True - True
+158209 - 0x00C92 (Entry Right) - 0x00B10 - True
+Door - 0x0C128 (Entry Inner) - 0x00B10
+Door - 0x0C153 (Entry Outer) - 0x00C92
+158210 - 0x00290 (Outside 1) - 0x09D9B - True
+158211 - 0x00038 (Outside 2) - 0x09D9B & 0x00290 - True
+158212 - 0x00037 (Outside 3) - 0x09D9B & 0x00038 - True
+Door - 0x03750 (Garden Entry) - 0x00037
+158706 - 0x17CA4 (Laser Panel) - 0x193A6 - True
+Laser - 0x17C65 (Laser) - 0x17CA4
+159130 - 0x006E5 (Facade Left Near EP) - True - True
+159131 - 0x006E6 (Facade Left Far Short EP) - True - True
+159132 - 0x006E7 (Facade Left Far Long EP) - True - True
+159136 - 0x03DAB (Facade Right Near EP) - True - True
+159137 - 0x03DAC (Facade Left Stairs EP) - True - True
+159138 - 0x03DAD (Facade Right Stairs EP) - True - True
+159140 - 0x03E01 (Grass Stairs EP) - True - True
+159120 - 0x03BE2 (Garden Left EP) - 0x03750 - True
+159121 - 0x03BE3 (Garden Right EP) - True - True
+159122 - 0x0A409 (Wall EP) - True - True
+
+Inside Monastery (Monastery):
+158213 - 0x09D9B (Shutters Control) - True - Dots
+158214 - 0x193A7 (Inside 1) - 0x00037 - True
+158215 - 0x193AA (Inside 2) - 0x193A7 - True
+158216 - 0x193AB (Inside 3) - 0x193AA - True
+158217 - 0x193A6 (Inside 4) - 0x193AB - True
+159133 - 0x034A7 (Left Shutter EP) - 0x09D9B - True
+159134 - 0x034AD (Middle Shutter EP) - 0x09D9B - True
+159135 - 0x034AF (Right Shutter EP) - 0x09D9B - True
+
+Monastery Garden (Monastery):
+
+==Town==
+
+Town Obelisk (Town) - Entry - True:
+159750 - 0xFFE50 (Obelisk Side 1) - 0x035C7 - True
+159751 - 0xFFE51 (Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True
+159752 - 0xFFE52 (Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True
+159753 - 0xFFE53 (Obelisk Side 4) - 0x28B30 & 0x035C9 - True
+159754 - 0xFFE54 (Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True
+159755 - 0xFFE55 (Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True
+159759 - 0x0A16C (Obelisk) - True - True
+
+Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - Town RGB House - 0x28A61 - Town Inside Cargo Box - 0x0A0C9 - Outside Windmill - True:
+158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat
+158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Triangles
+Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8
+158707 - 0x09F98 (Desert Laser Redirect Control) - True - True
+158220 - 0x18590 (Transparent) - True - Symmetry
+158221 - 0x28AE3 (Vines) - 0x18590 - True
+158222 - 0x28938 (Apple Tree) - 0x28AE3 - True
+158223 - 0x079DF (Triple Exit) - 0x28938 - True
+158235 - 0x2899C (Wooden Roof Lower Row 1) - True - Rotated Shapers & Dots & Full Dots & Colored Squares
+158236 - 0x28A33 (Wooden Roof Lower Row 2) - 0x2899C - Rotated Shapers & Dots & Full Dots & Stars
+158237 - 0x28ABF (Wooden Roof Lower Row 3) - 0x28A33 - Shapers & Negative Shapers & Dots & Full Dots
+158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Rotated Shapers & Dots & Full Dots
+158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Rotated Shapers & Dots & Full Dots & Triangles
+Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1
+158225 - 0x28998 (RGB House Entry Panel) - True - Stars & Rotated Shapers
+Door - 0x28A61 (RGB House Entry) - 0x28998
+158226 - 0x28A0D (Church Entry Panel) - 0x28A61 - Stars
+Door - 0x03BB0 (Church Entry) - 0x28A0D
+158228 - 0x28A79 (Maze Panel) - True - True
+Door - 0x28AA2 (Maze Stairs) - 0x28A79
+159540 - 0x03335 (Tower Underside Third EP) - True - True
+159541 - 0x03412 (Tower Underside Fourth EP) - True - True
+159542 - 0x038A6 (Tower Underside First EP) - True - True
+159543 - 0x038AA (Tower Underside Second EP) - True - True
+159545 - 0x03E40 (RGB House Green EP) - 0x334D8 - True
+159546 - 0x28B8E (Maze Bridge Underside EP) - 0x2896A - True
+159552 - 0x03BCF (Black Line Redirect EP) - True - True
+159800 - 0xFFF80 (Pet the Dog) - True - True
+
+Town Inside Cargo Box (Town):
+158606 - 0x17D01 (Cargo Box Discard) - True - Arrows & Triangles
+
+Town Maze Rooftop (Town) - Town Red Rooftop - 0x2896A:
+158229 - 0x2896A (Maze Rooftop Bridge Control) - True - Shapers
+159544 - 0x03E3F (RGB House Red EP) - 0x334D8 - True
+
+Town Red Rooftop (Town):
+158607 - 0x17C71 (Rooftop Discard) - True - Arrows & Triangles
+158230 - 0x28AC7 (Red Rooftop 1) - True - Symmetry & Dots
+158231 - 0x28AC8 (Red Rooftop 2) - 0x28AC7 - Symmetry & Black/White Squares
+158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Stars
+158233 - 0x28ACB (Red Rooftop 4) - 0x28ACA - Symmetry & Shapers
+158234 - 0x28ACC (Red Rooftop 5) - 0x28ACB - Symmetry & Triangles
+158224 - 0x28B39 (Tall Hexagonal) - 0x079DF - True
+
+Town Wooden Rooftop (Town):
+158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Rotated Shapers & Dots & Eraser & Full Dots
+
+Town Church (Town):
+158227 - 0x28A69 (Church Lattice) - 0x03BB0 - True
+159553 - 0x03BD1 (Black Line Church EP) - True - True
+
+Town RGB House (Town RGB House) - Town RGB House Upstairs - 0x2897B:
+158242 - 0x034E4 (Sound Room Left) - True - True
+158243 - 0x034E3 (Sound Room Right) - True - Sound Dots
+Door - 0x2897B (Stairs) - 0x034E4 & 0x034E3
+
+Town RGB House Upstairs (Town RGB House Upstairs):
+158244 - 0x334D8 (RGB Control) - True - Rotated Shapers & Colored Squares
+158245 - 0x03C0C (Left) - 0x334D8 - Stars
+158246 - 0x03C08 (Right) - 0x334D8 - Dots & Symmetry & Colored Dots
+
+Town Tower Bottom (Town Tower) - Town - True - Town Tower After First Door - 0x27799:
+Door - 0x27799 (First Door) - 0x28A69
+
+Town Tower After First Door (Town Tower) - Town Tower After Second Door - 0x27798:
+Door - 0x27798 (Second Door) - 0x28ACC
+
+Town Tower After Second Door (Town Tower) - Town Tower After Third Door - 0x2779C:
+Door - 0x2779C (Third Door) - 0x28AD9
+
+Town Tower After Third Door (Town Tower) - Town Tower Top - 0x2779A:
+Door - 0x2779A (Fourth Door) - 0x28B39
+
+Town Tower Top (Town):
+158708 - 0x032F5 (Laser Panel) - True - True
+Laser - 0x032F9 (Laser) - 0x032F5
+159422 - 0x33692 (Brown Bridge EP) - True - True
+159551 - 0x03BCE (Black Line Tower EP) - True - True
+
+==Windmill & Theater==
+
+Outside Windmill (Windmill) - Windmill Interior - 0x1845B:
+159010 - 0x037B6 (First Blade EP) - 0x17D02 - True
+159011 - 0x037B2 (Second Blade EP) - 0x17D02 - True
+159012 - 0x000F7 (Third Blade EP) - 0x17D02 - True
+158241 - 0x17F5F (Entry Panel) - True - Dots
+Door - 0x1845B (Entry) - 0x17F5F
+
+Windmill Interior (Windmill) - Theater - 0x17F88:
+158247 - 0x17D02 (Turn Control) - True - Dots
+158248 - 0x17F89 (Theater Entry Panel) - True - Black/White Squares & Triangles
+Door - 0x17F88 (Theater Entry) - 0x17F89
+
+Theater (Theater) - Town - 0x0A16D | 0x3CCDF:
+158656 - 0x00815 (Video Input) - True - True
+158657 - 0x03553 (Tutorial Video) - 0x00815 & 0x03481 - True
+158658 - 0x03552 (Desert Video) - 0x00815 & 0x0339E - True
+158659 - 0x0354E (Jungle Video) - 0x00815 & 0x03702 - True
+158660 - 0x03549 (Challenge Video) - 0x00815 & 0x0356B - True
+158661 - 0x0354F (Shipwreck Video) - 0x00815 & 0x03535 - True
+158662 - 0x03545 (Mountain Video) - 0x00815 & 0x03542 - True
+158249 - 0x0A168 (Exit Left Panel) - True - Black/White Squares & Stars & Stars + Same Colored Symbol & Shapers
+158250 - 0x33AB2 (Exit Right Panel) - True - Black/White Squares & Stars & Stars + Same Colored Symbol & Shapers
+Door - 0x0A16D (Exit Left) - 0x0A168
+Door - 0x3CCDF (Exit Right) - 0x33AB2
+158608 - 0x17CF7 (Discard) - True - Arrows & Triangles
+159554 - 0x339B6 (Eclipse EP) - 0x03549 & 0x0A16D & 0x3CCDF - True
+159555 - 0x33A29 (Window EP) - 0x03553 - True
+159556 - 0x33A2A (Door EP) - 0x03553 - True
+159558 - 0x33B06 (Church EP) - 0x0354E - True
+
+==Jungle==
+
+Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF:
+158251 - 0x17CDF (Shore Boat Spawn) - True - Boat
+158609 - 0x17F9B (Discard) - True - Arrows & Triangles
+158252 - 0x002C4 (First Row 1) - True - True
+158253 - 0x00767 (First Row 2) - 0x002C4 - True
+158254 - 0x002C6 (First Row 3) - 0x00767 - True
+158255 - 0x0070E (Second Row 1) - 0x002C6 - True
+158256 - 0x0070F (Second Row 2) - 0x0070E - True
+158257 - 0x0087D (Second Row 3) - 0x0070F - True
+158258 - 0x002C7 (Second Row 4) - 0x0087D - True
+158259 - 0x17CAB (Popup Wall Control) - 0x002C7 - True
+Door - 0x1475B (Popup Wall) - 0x17CAB
+158260 - 0x0026D (Popup Wall 1) - 0x1475B - Sound Dots
+158261 - 0x0026E (Popup Wall 2) - 0x0026D - Sound Dots
+158262 - 0x0026F (Popup Wall 3) - 0x0026E - Sound Dots
+158263 - 0x00C3F (Popup Wall 4) - 0x0026F - Sound Dots
+158264 - 0x00C41 (Popup Wall 5) - 0x00C3F - Sound Dots
+158265 - 0x014B2 (Popup Wall 6) - 0x00C41 - Sound Dots
+158709 - 0x03616 (Laser Panel) - 0x014B2 - True
+Laser - 0x00274 (Laser) - 0x03616
+158266 - 0x337FA (Laser Shortcut Panel) - True - True
+Door - 0x3873B (Laser Shortcut) - 0x337FA
+159100 - 0x03ABC (Long Arch Moss EP) - True - True
+159101 - 0x03ABE (Straight Left Moss EP) - True - True
+159102 - 0x03AC0 (Pop-up Wall Moss EP) - True - True
+159103 - 0x03AC4 (Short Arch Moss EP) - True - True
+159150 - 0x289F4 (Entrance EP) - True - True
+159151 - 0x289F5 (Tree Halo EP) - True - True
+159350 - 0x035CB (Bamboo CCW EP) - True - True
+159351 - 0x035CF (Bamboo CW EP) - True - True
+
+Outside Jungle River (Jungle) - Main Island - True - Monastery Garden - 0x0CF2A - Jungle Vault - 0x15287:
+158267 - 0x17CAA (Monastery Garden Shortcut Panel) - True - True
+Door - 0x0CF2A (Monastery Garden Shortcut) - 0x17CAA
+158663 - 0x15ADD (Vault Panel) - True - Black/White Squares & Dots
+Door - 0x15287 (Vault Door) - 0x15ADD
+159110 - 0x03AC5 (Green Leaf Moss EP) - True - True
+
+Jungle Vault (Jungle):
+158664 - 0x03702 (Vault Box) - True - True
+
+==Bunker==
+
+Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4:
+158268 - 0x17C2E (Entry Panel) - True - Black/White Squares
+Door - 0x0C2A4 (Entry) - 0x17C2E
+
+Bunker (Bunker) - Bunker Glass Room - 0x17C79:
+158269 - 0x09F7D (Intro Left 1) - True - Colored Squares
+158270 - 0x09FDC (Intro Left 2) - 0x09F7D - Colored Squares & Black/White Squares
+158271 - 0x09FF7 (Intro Left 3) - 0x09FDC - Colored Squares & Black/White Squares
+158272 - 0x09F82 (Intro Left 4) - 0x09FF7 - Colored Squares & Black/White Squares
+158273 - 0x09FF8 (Intro Left 5) - 0x09F82 - Colored Squares & Black/White Squares
+158274 - 0x09D9F (Intro Back 1) - 0x09FF8 - Colored Squares & Black/White Squares
+158275 - 0x09DA1 (Intro Back 2) - 0x09D9F - Colored Squares
+158276 - 0x09DA2 (Intro Back 3) - 0x09DA1 - Colored Squares
+158277 - 0x09DAF (Intro Back 4) - 0x09DA2 - Colored Squares
+158278 - 0x0A099 (Tinted Glass Door Panel) - 0x09DAF - True
+Door - 0x17C79 (Tinted Glass Door) - 0x0A099
+
+Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3:
+158279 - 0x0A010 (Glass Room 1) - 0x17C79 - Colored Squares
+158280 - 0x0A01B (Glass Room 2) - 0x17C79 & 0x0A010 - Colored Squares & Black/White Squares
+158281 - 0x0A01F (Glass Room 3) - 0x17C79 & 0x0A01B - Colored Squares & Black/White Squares
+Door - 0x0C2A3 (UV Room Entry) - 0x0A01F
+
+Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D:
+158282 - 0x34BC5 (Drop-Down Door Open) - True - True
+158283 - 0x34BC6 (Drop-Down Door Close) - 0x34BC5 - True
+158284 - 0x17E63 (UV Room 1) - 0x34BC5 - Colored Squares
+158285 - 0x17E67 (UV Room 2) - 0x17E63 & 0x34BC6 - Colored Squares & Black/White Squares
+Door - 0x0A08D (Elevator Room Entry) - 0x17E67
+
+Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay:
+159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True
+
+Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Cyan Room - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079:
+158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares
+
+Bunker Cyan Room (Bunker) - Bunker Elevator - TrueOneWay:
+
+Bunker Green Room (Bunker) - Bunker Elevator - TrueOneWay:
+159310 - 0x000D3 (Green Room Flowers EP) - True - True
+
+Bunker Laser Platform (Bunker) - Bunker Elevator - TrueOneWay:
+158710 - 0x09DE0 (Laser Panel) - True - True
+Laser - 0x0C2B2 (Laser) - 0x09DE0
+
+==Swamp==
+
+Outside Swamp (Swamp) - Swamp Entry Area - 0x00C1C - Main Island - True:
+158287 - 0x0056E (Entry Panel) - True - Shapers & Black/White Squares
+Door - 0x00C1C (Entry) - 0x0056E
+159321 - 0x03603 (Purple Sand Middle EP) - 0x17E2B - True
+159322 - 0x03601 (Purple Sand Top EP) - 0x17E2B - True
+159327 - 0x035DE (Purple Sand Bottom EP) - True - True
+
+Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay:
+158288 - 0x00469 (Intro Front 1) - True - Shapers
+158289 - 0x00472 (Intro Front 2) - 0x00469 - Shapers
+158290 - 0x00262 (Intro Front 3) - 0x00472 - Shapers
+158291 - 0x00474 (Intro Front 4) - 0x00262 - Shapers
+158292 - 0x00553 (Intro Front 5) - 0x00474 - Shapers
+158293 - 0x0056F (Intro Front 6) - 0x00553 - Shapers
+158294 - 0x00390 (Intro Back 1) - 0x0056F - Shapers & Black/White Squares
+158295 - 0x010CA (Intro Back 2) - 0x00390 - Shapers & Black/White Squares
+158296 - 0x00983 (Intro Back 3) - 0x010CA - Shapers & Rotated Shapers & Black/White Squares
+158297 - 0x00984 (Intro Back 4) - 0x00983 - Shapers & Rotated Shapers & Black/White Squares
+158298 - 0x00986 (Intro Back 5) - 0x00984 - Shapers & Triangles
+158299 - 0x00985 (Intro Back 6) - 0x00986 - Shapers & Triangles
+158300 - 0x00987 (Intro Back 7) - 0x00985 - Rotated Shapers & Triangles
+158301 - 0x181A9 (Intro Back 8) - 0x00987 - Shapers & Triangles
+
+Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609:
+158302 - 0x00609 (Sliding Bridge) - True - Shapers
+159342 - 0x0105D (Sliding Bridge Left EP) - 0x00609 - True
+159343 - 0x0A304 (Sliding Bridge Right EP) - 0x00609 - True
+
+Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay:
+158313 - 0x00982 (Platform Row 1) - True - Shapers
+158314 - 0x0097F (Platform Row 2) - 0x00982 - Shapers
+158315 - 0x0098F (Platform Row 3) - 0x0097F - Shapers
+158316 - 0x00990 (Platform Row 4) - 0x0098F - Shapers
+Door - 0x184B7 (Between Bridges First Door) - 0x00990
+158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Shapers
+158318 - 0x17C0E (Platform Shortcut Right Panel) - 0x17C0D - Shapers
+Door - 0x38AE6 (Platform Shortcut) - 0x17C0E
+Door - 0x04B7F (Cyan Water Pump) - 0x00006
+
+Swamp Cyan Underwater (Swamp):
+158307 - 0x00002 (Cyan Underwater 1) - True - Shapers & Negative Shapers & Black/White Squares
+158308 - 0x00004 (Cyan Underwater 2) - 0x00002 - Shapers & Negative Shapers & Black/White Squares
+158309 - 0x00005 (Cyan Underwater 3) - 0x00004 - Shapers & Negative Shapers & Stars
+158310 - 0x013E6 (Cyan Underwater 4) - 0x00005 - Shapers & Negative Shapers & Stars
+158311 - 0x00596 (Cyan Underwater 5) - 0x013E6 - Shapers & Negative Shapers & Dots
+158312 - 0x18488 (Cyan Underwater Sliding Bridge Control) - True - Shapers
+159340 - 0x03AA6 (Cyan Underwater Sliding Bridge EP) - 0x18488 - True
+
+Swamp Between Bridges Near (Swamp) - Swamp Between Bridges Far - 0x18507:
+158303 - 0x00999 (Between Bridges Near Row 1) - 0x00990 - Shapers
+158304 - 0x0099D (Between Bridges Near Row 2) - 0x00999 - Shapers
+158305 - 0x009A0 (Between Bridges Near Row 3) - 0x0099D - Shapers
+158306 - 0x009A1 (Between Bridges Near Row 4) - 0x009A0 - Shapers
+Door - 0x18507 (Between Bridges Second Door) - 0x009A1
+
+Swamp Between Bridges Far (Swamp) - Swamp Red Underwater - 0x183F2 - Swamp Rotating Bridge - TrueOneWay:
+158319 - 0x00007 (Between Bridges Far Row 1) - 0x009A1 - Rotated Shapers & Dots
+158320 - 0x00008 (Between Bridges Far Row 2) - 0x00007 - Rotated Shapers & Dots
+158321 - 0x00009 (Between Bridges Far Row 3) - 0x00008 - Rotated Shapers & Dots
+158322 - 0x0000A (Between Bridges Far Row 4) - 0x00009 - Rotated Shapers & Dots
+Door - 0x183F2 (Red Water Pump) - 0x00596
+
+Swamp Red Underwater (Swamp) - Swamp Maze - 0x305D5:
+158323 - 0x00001 (Red Underwater 1) - True - Symmetry & Shapers & Negative Shapers
+158324 - 0x014D2 (Red Underwater 2) - True - Symmetry & Shapers & Negative Shapers
+158325 - 0x014D4 (Red Underwater 3) - True - Symmetry & Shapers & Negative Shapers & Eraser
+158326 - 0x014D1 (Red Underwater 4) - True - Symmetry & Shapers & Negative Shapers & Eraser
+Door - 0x305D5 (Red Underwater Exit) - 0x014D1
+
+Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near Boat - 0x181F5 - Swamp Purple Area - 0x181F5:
+158327 - 0x181F5 (Rotating Bridge) - True - Rotated Shapers & Shapers
+159331 - 0x016B2 (Rotating Bridge CCW EP) - 0x181F5 - True
+159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True
+
+Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8:
+159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True
+158328 - 0x09DB8 (Boat Spawn) - True - Boat
+158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers & Dots
+158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers & Dots
+158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Rotated Shapers & Dots
+158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers & Dots
+Door - 0x18482 (Blue Water Pump) - 0x00E3A
+159332 - 0x3365F (Boat EP) - 0x09DB8 - True
+159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True
+
+Swamp Long Bridge (Swamp) - Swamp Near Boat - 0x17E2B - Outside Swamp - 0x17E2B:
+158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers
+
+Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6 - Swamp Near Boat - TrueOneWay:
+Door - 0x0A1D6 (Purple Water Pump) - 0x00E3A
+
+Swamp Purple Underwater (Swamp):
+158333 - 0x009A6 (Purple Underwater) - True - Shapers & Dots
+159330 - 0x03A9E (Purple Underwater Right EP) - True - True
+159336 - 0x03A93 (Purple Underwater Left EP) - True - True
+
+Swamp Blue Underwater (Swamp):
+158334 - 0x009AB (Blue Underwater 1) - True - Shapers & Negative Shapers
+158335 - 0x009AD (Blue Underwater 2) - 0x009AB - Shapers & Negative Shapers
+158336 - 0x009AE (Blue Underwater 3) - 0x009AD - Shapers & Negative Shapers
+158337 - 0x009AF (Blue Underwater 4) - 0x009AE - Shapers & Negative Shapers
+158338 - 0x00006 (Blue Underwater 5) - 0x009AF - Shapers & Negative Shapers
+
+Swamp Maze (Swamp) - Swamp Laser Area - 0x17C0A & 0x17E07:
+158340 - 0x17C0A (Maze Control) - True - Shapers & Negative Shapers & Rotated Shapers
+158112 - 0x17E07 (Maze Control Other Side) - True - Shapers & Negative Shapers & Rotated Shapers
+
+Swamp Laser Area (Swamp) - Outside Swamp - 0x2D880:
+158711 - 0x03615 (Laser Panel) - True - True
+Laser - 0x00BF6 (Laser) - 0x03615
+158341 - 0x17C05 (Laser Shortcut Left Panel) - True - Shapers & Colored Squares & Stars & Stars + Same Colored Symbol
+158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Colored Squares & Stars & Stars + Same Colored Symbol
+Door - 0x2D880 (Laser Shortcut) - 0x17C02
+
+==Treehouse==
+
+Treehouse Obelisk (Treehouse) - Entry - True:
+159720 - 0xFFE20 (Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True
+159721 - 0xFFE21 (Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True
+159722 - 0xFFE22 (Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True
+159723 - 0xFFE23 (Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True
+159724 - 0xFFE24 (Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True
+159725 - 0xFFE25 (Obelisk Side 6) - 0x28AE9 & 0x3348F - True
+159729 - 0x00097 (Obelisk) - True - True
+
+Treehouse Beach (Treehouse Beach) - Main Island - True:
+159200 - 0x0053D (Rock Shadow EP) - True - True
+159201 - 0x0053E (Sand Shadow EP) - True - True
+159212 - 0x220BD (Both Orange Bridges EP) - 0x17DA2 & 0x17DDB - True
+
+Treehouse Entry Area (Treehouse) - Treehouse Between Entry Doors - 0x0C309 - The Ocean - 0x17C95:
+158343 - 0x17C95 (Boat Spawn) - True - Boat
+158344 - 0x0288C (First Door Panel) - True - Stars
+Door - 0x0C309 (First Door) - 0x0288C
+159210 - 0x33721 (Buoy EP) - 0x17C95 - True
+
+Treehouse Between Entry Doors (Treehouse) - Treehouse Yellow Bridge - 0x0C310:
+158345 - 0x02886 (Second Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles
+Door - 0x0C310 (Second Door) - 0x02886
+
+Treehouse Yellow Bridge (Treehouse) - Treehouse After Yellow Bridge - 0x17DC4:
+158346 - 0x17D72 (Yellow Bridge 1) - True - Stars
+158347 - 0x17D8F (Yellow Bridge 2) - 0x17D72 - Stars
+158348 - 0x17D74 (Yellow Bridge 3) - 0x17D8F - Stars
+158349 - 0x17DAC (Yellow Bridge 4) - 0x17D74 - Stars
+158350 - 0x17D9E (Yellow Bridge 5) - 0x17DAC - Stars
+158351 - 0x17DB9 (Yellow Bridge 6) - 0x17D9E - Stars
+158352 - 0x17D9C (Yellow Bridge 7) - 0x17DB9 - Stars
+158353 - 0x17DC2 (Yellow Bridge 8) - 0x17D9C - Stars
+158354 - 0x17DC4 (Yellow Bridge 9) - 0x17DC2 - Stars
+
+Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181:
+158355 - 0x0A182 (Third Door Panel) - True - Stars
+Door - 0x0A181 (Third Door) - 0x0A182
+
+Treehouse Junction (Treehouse) - Treehouse Right Orange Bridge - True - Treehouse First Purple Bridge - True - Treehouse Green Bridge - True:
+158356 - 0x2700B (Laser House Door Timer Outside) - True - True
+
+Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x17D6C:
+158357 - 0x17DC8 (First Purple Bridge 1) - True - Stars & Dots & Triangles
+158358 - 0x17DC7 (First Purple Bridge 2) - 0x17DC8 - Stars & Dots & Triangles
+158359 - 0x17CE4 (First Purple Bridge 3) - 0x17DC7 - Stars & Dots & Triangles
+158360 - 0x17D2D (First Purple Bridge 4) - 0x17CE4 - Stars & Dots & Triangles
+158361 - 0x17D6C (First Purple Bridge 5) - 0x17D2D - Stars & Dots & Triangles
+
+Treehouse Right Orange Bridge (Treehouse) - Treehouse Drawbridge Platform - 0x17DA2:
+158391 - 0x17D88 (Right Orange Bridge 1) - True - Stars & Stars + Same Colored Symbol & Colored Squares
+158392 - 0x17DB4 (Right Orange Bridge 2) - 0x17D88 - Stars & Stars + Same Colored Symbol & Colored Squares
+158393 - 0x17D8C (Right Orange Bridge 3) - 0x17DB4 - Stars & Stars + Same Colored Symbol & Colored Squares
+158394 - 0x17CE3 (Right Orange Bridge 4 & Directional) - 0x17D8C - Stars & Stars + Same Colored Symbol & Colored Squares
+158395 - 0x17DCD (Right Orange Bridge 5) - 0x17CE3 - Stars & Stars + Same Colored Symbol & Colored Squares
+158396 - 0x17DB2 (Right Orange Bridge 6) - 0x17DCD - Stars & Stars + Same Colored Symbol & Colored Squares
+158397 - 0x17DCC (Right Orange Bridge 7) - 0x17DB2 - Stars & Stars + Same Colored Symbol & Colored Squares
+158398 - 0x17DCA (Right Orange Bridge 8) - 0x17DCC - Stars & Stars + Same Colored Symbol & Colored Squares
+158399 - 0x17D8E (Right Orange Bridge 9) - 0x17DCA - Stars & Stars + Same Colored Symbol & Colored Squares
+158400 - 0x17DB7 (Right Orange Bridge 10 & Directional) - 0x17D8E - Stars & Stars + Same Colored Symbol & Colored Squares
+158401 - 0x17DB1 (Right Orange Bridge 11) - 0x17DB7 - Stars & Stars + Same Colored Symbol & Colored Squares
+158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars & Stars + Same Colored Symbol & Colored Squares
+
+Treehouse Drawbridge Platform (Treehouse) - Main Island - 0x0C32D:
+158404 - 0x037FF (Drawbridge Panel) - True - Stars
+Door - 0x0C32D (Drawbridge) - 0x037FF
+
+Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6:
+158362 - 0x17D9B (Second Purple Bridge 1) - True - Stars & Stars + Same Colored Symbol & Black/White Squares & Colored Squares
+158363 - 0x17D99 (Second Purple Bridge 2) - 0x17D9B - Stars & Stars + Same Colored Symbol & Black/White Squares & Colored Squares
+158364 - 0x17DAA (Second Purple Bridge 3) - 0x17D99 - Stars & Stars + Same Colored Symbol & Black/White Squares & Colored Squares
+158365 - 0x17D97 (Second Purple Bridge 4) - 0x17DAA - Stars & Stars + Same Colored Symbol & Black/White Squares & Colored Squares
+158366 - 0x17BDF (Second Purple Bridge 5) - 0x17D97 - Stars & Stars + Same Colored Symbol & Colored Squares
+158367 - 0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Stars + Same Colored Symbol & Black/White Squares & Colored Squares
+158368 - 0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Stars + Same Colored Symbol & Colored Squares
+
+Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room Front Platform - 0x17DDB - Treehouse Laser Room Back Platform - 0x17DDB - Treehouse Burned House - 0x17DDB:
+158376 - 0x17DB3 (Left Orange Bridge 1) - True - Stars & Stars + Same Colored Symbol & Triangles
+158377 - 0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Stars + Same Colored Symbol & Triangles
+158378 - 0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Stars + Same Colored Symbol & Triangles
+158379 - 0x17DC0 (Left Orange Bridge 4) - 0x17DB6 - Stars & Stars + Same Colored Symbol & Triangles
+158380 - 0x17DD7 (Left Orange Bridge 5) - 0x17DC0 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers
+158381 - 0x17DD9 (Left Orange Bridge 6) - 0x17DD7 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers
+158382 - 0x17DB8 (Left Orange Bridge 7) - 0x17DD9 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers
+158383 - 0x17DDC (Left Orange Bridge 8) - 0x17DB8 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers
+158384 - 0x17DD1 (Left Orange Bridge 9 & Directional) - 0x17DDC - Stars & Black/White Squares & Stars + Same Colored Symbol
+158385 - 0x17DDE (Left Orange Bridge 10) - 0x17DD1 - Stars & Stars + Same Colored Symbol & Shapers
+158386 - 0x17DE3 (Left Orange Bridge 11) - 0x17DDE - Stars & Stars + Same Colored Symbol & Shapers
+158387 - 0x17DEC (Left Orange Bridge 12) - 0x17DE3 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers
+158388 - 0x17DAE (Left Orange Bridge 13) - 0x17DEC - Stars & Stars + Same Colored Symbol & Shapers & Triangles
+158389 - 0x17DB0 (Left Orange Bridge 14) - 0x17DAE - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers
+158390 - 0x17DDB (Left Orange Bridge 15) - 0x17DB0 - Stars & Stars + Same Colored Symbol & Shapers & Triangles
+
+Treehouse Green Bridge (Treehouse) - Treehouse Green Bridge Front House - 0x17E61 - Treehouse Green Bridge Left House - 0x17E61:
+158369 - 0x17E3C (Green Bridge 1) - True - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
+158370 - 0x17E4D (Green Bridge 2) - 0x17E3C - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
+158371 - 0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
+158372 - 0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Rotated Shapers & Negative Shapers & Stars + Same Colored Symbol
+158373 - 0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
+158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
+158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
+
+Treehouse Green Bridge Front House (Treehouse):
+158610 - 0x17FA9 (Green Bridge Discard) - True - Arrows & Triangles
+
+Treehouse Green Bridge Left House (Treehouse):
+159211 - 0x220A7 (Right Orange Bridge EP) - 0x17DA2 - True
+
+Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323:
+Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DDB | 0x17CBC
+
+Treehouse Laser Room Back Platform (Treehouse):
+158611 - 0x17FA0 (Laser Discard) - True - Arrows & Triangles
+
+Treehouse Burned House (Treehouse):
+159202 - 0x00769 (Burned House Beach EP) - True - True
+
+Treehouse Laser Room (Treehouse):
+158712 - 0x03613 (Laser Panel) - True - True
+158403 - 0x17CBC (Laser House Door Timer Inside) - True - True
+Laser - 0x028A4 (Laser) - 0x03613
+
+==Mountain (Outside)==
+
+Mountainside Obelisk (Mountainside) - Entry - True:
+159730 - 0xFFE30 (Obelisk Side 1) - 0x001A3 & 0x335AE - True
+159731 - 0xFFE31 (Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True
+159732 - 0xFFE32 (Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True
+159733 - 0xFFE33 (Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True
+159734 - 0xFFE34 (Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True
+159735 - 0xFFE35 (Obelisk Side 6) - 0x035CB & 0x035CF - True
+159739 - 0x00367 (Obelisk) - True - True
+
+Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
+159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True
+158612 - 0x17C42 (Discard) - True - Arrows & Triangles
+158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Triangles
+Door - 0x00085 (Vault Door) - 0x002A6
+159301 - 0x335AE (Cloud Cycle EP) - True - True
+159325 - 0x33505 (Bush EP) - True - True
+159335 - 0x03C07 (Apparent River EP) - True - True
+
+Mountainside Vault (Mountainside):
+158666 - 0x03542 (Vault Box) - True - True
+
+Mountaintop (Mountaintop) - Mountain Floor 1 - 0x17C34:
+158405 - 0x0042D (River Shape) - True - True
+158406 - 0x09F7F (Box Short) - 7 Lasers + Redirect - True
+158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Triangles
+158800 - 0xFFF00 (Box Long) - 11 Lasers + Redirect & 0x17C34 - True
+159300 - 0x001A3 (River Shape EP) - True - True
+159320 - 0x3370E (Arch Black EP) - True - True
+159324 - 0x336C8 (Arch White Right EP) - True - True
+159326 - 0x3369A (Arch White Left EP) - True - True
+
+==Mountain (Inside)==
+
+Mountain Floor 1 (Mountain Floor 1) - Mountain Floor 1 Bridge - 0x09E39:
+158408 - 0x09E39 (Light Bridge Controller) - True - Black/White Squares & Colored Squares & Eraser
+
+Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneWay:
+158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots
+158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Dots & Stars & Stars + Same Colored Symbol
+158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers & Stars & Stars + Same Colored Symbol
+158412 - 0x09E69 (Right Row 4) - 0x09E72 - Black/White Squares & Eraser & Stars & Stars + Same Colored Symbol
+158413 - 0x09E7B (Right Row 5) - 0x09E69 - Dots & Full Dots & Triangles
+158414 - 0x09E73 (Left Row 1) - True - Dots & Black/White Squares
+158415 - 0x09E75 (Left Row 2) - 0x09E73 - Arrows & Black/White Squares
+158416 - 0x09E78 (Left Row 3) - 0x09E75 - Arrows & Stars
+158417 - 0x09E79 (Left Row 4) - 0x09E78 - Arrows & Shapers & Rotated Shapers
+158418 - 0x09E6C (Left Row 5) - 0x09E79 - Arrows & Black/White Squares & Stars & Stars + Same Colored Symbol
+158419 - 0x09E6F (Left Row 6) - 0x09E6C - Arrows & Dots & Full Dots
+158420 - 0x09E6B (Left Row 7) - 0x09E6F - Arrows & Dots & Full Dots
+158421 - 0x33AF5 (Back Row 1) - True - Symmetry & Triangles
+158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Triangles
+158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Triangles
+158424 - 0x09EAD (Trash Pillar 1) - True - Triangles & Arrows
+158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Triangles & Arrows
+
+Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
+Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B
+
+Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 Above The Abyss - True - Mountain Pink Bridge EP - TrueOneWay:
+158426 - 0x09FD3 (Near Row 1) - True - Stars & Stars + Same Colored Symbol & Colored Squares
+158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Stars & Stars + Same Colored Symbol & Triangles
+158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Stars + Same Colored Symbol & Colored Squares & Eraser
+158429 - 0x09FD7 (Near Row 4) - 0x09FD6 - Stars & Stars + Same Colored Symbol & Shapers & Eraser
+158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Symmetry & Triangles
+Door - 0x09FFB (Staircase Near) - 0x09FD8
+
+Mountain Floor 2 Above The Abyss (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD & 0x09ED8 & 0x09E86:
+Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86
+
+Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2):
+158431 - 0x09E86 (Light Bridge Controller Near) - True - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser
+
+Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay - Mountain Floor 2 - 0x09ED8:
+158432 - 0x09FCC (Far Row 1) - True - Black/White Squares
+158433 - 0x09FCE (Far Row 2) - 0x09FCC - Triangles
+158434 - 0x09FCF (Far Row 3) - 0x09FCE - Stars
+158435 - 0x09FD0 (Far Row 4) - 0x09FCF - Stars & Stars + Same Colored Symbol & Colored Squares
+158436 - 0x09FD1 (Far Row 5) - 0x09FD0 - Dots
+158437 - 0x09FD2 (Far Row 6) - 0x09FD1 - Shapers
+Door - 0x09E07 (Staircase Far) - 0x09FD2
+
+Mountain Floor 2 Light Bridge Room Far (Mountain Floor 2):
+158438 - 0x09ED8 (Light Bridge Controller Far) - True - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser
+
+Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - TrueOneWay:
+158613 - 0x17F93 (Elevator Discard) - True - Arrows & Triangles
+
+Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Floor 3 - 0x09EEB:
+158439 - 0x09EEB (Elevator Control Panel) - True - Dots
+
+Mountain Floor 3 (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89 - Mountain Pink Bridge EP - TrueOneWay:
+158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Eraser
+158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser
+158442 - 0x09F01 (Giant Puzzle Top Right) - True - Shapers & Eraser
+158443 - 0x09EFF (Giant Puzzle Top Left) - True - Rotated Shapers
+158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry
+159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True
+159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True
+Door - 0x09F89 (Exit) - 0x09FDA
+
+Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Path to Caves - 0x17F33 - Mountain Bottom Floor Pillars Room - 0x0C141:
+158614 - 0x17FA2 (Discard) - 0xFFF00 - Arrows & Triangles
+158445 - 0x01983 (Pillars Room Entry Left) - True - Shapers & Stars
+158446 - 0x01987 (Pillars Room Entry Right) - True - Colored Squares & Dots
+Door - 0x0C141 (Pillars Room Entry) - 0x01983 & 0x01987
+Door - 0x17F33 (Rock Open) - 0x17FA2 | 0x334E1
+
+Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB & 0x33961:
+158522 - 0x0383A (Right Pillar 1) - True - Stars
+158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Stars & Dots
+158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Full Dots & Triangles
+158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Dots & Symmetry & Triangles
+158526 - 0x0383D (Left Pillar 1) - True - Triangles
+158527 - 0x0383F (Left Pillar 2) - 0x0383D - Black/White Squares & Stars & Stars + Same Colored Symbol
+158528 - 0x03859 (Left Pillar 3) - 0x0383F - Shapers
+158529 - 0x339BB (Left Pillar 4) - 0x03859 - Triangles & Symmetry
+
+Elevator (Mountain Bottom Floor):
+158530 - 0x3D9A6 (Elevator Door Close Left) - True - True
+158531 - 0x3D9A7 (Elevator Door Close Right) - True - True
+158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True
+158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True
+158534 - 0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True
+158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True
+158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True
+
+Mountain Pink Bridge EP (Mountain Floor 2):
+159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True
+
+Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D:
+158447 - 0x00FF8 (Caves Entry Panel) - True - Black/White Squares & Arrows & Triangles
+Door - 0x2D77D (Caves Entry) - 0x00FF8
+158448 - 0x334E1 (Rock Control) - True - True
+
+==Caves==
+
+Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Caves Path to Challenge - 0x019A5:
+158451 - 0x335AB (Elevator Inside Control) - True - Dots & Black/White Squares
+158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Black/White Squares
+158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Black/White Squares & Dots
+158454 - 0x00190 (Blue Tunnel Right First 1) - True - Dots & Full Dots & Triangles & Arrows
+158455 - 0x00558 (Blue Tunnel Right First 2) - 0x00190 - Dots & Full Dots & Triangles & Arrows
+158456 - 0x00567 (Blue Tunnel Right First 3) - 0x00558 - Dots & Full Dots & Triangles & Arrows
+158457 - 0x006FE (Blue Tunnel Right First 4) - 0x00567 - Dots & Full Dots & Triangles & Arrows
+158458 - 0x01A0D (Blue Tunnel Left First 1) - True - Symmetry & Triangles & Arrows
+158459 - 0x008B8 (Blue Tunnel Left Second 1) - True - Triangles & Colored Squares & Arrows
+158460 - 0x00973 (Blue Tunnel Left Second 2) - 0x008B8 - Triangles & Colored Squares & Arrows
+158461 - 0x0097B (Blue Tunnel Left Second 3) - 0x00973 - Triangles & Colored Squares & Arrows & Stars & Stars + Same Colored Symbol
+158462 - 0x0097D (Blue Tunnel Left Second 4) - 0x0097B - Triangles & Colored Squares & Arrows & Stars & Stars + Same Colored Symbol
+158463 - 0x0097E (Blue Tunnel Left Second 5) - 0x0097D - Triangles & Colored Squares & Arrows & Stars & Stars + Same Colored Symbol
+158464 - 0x00994 (Blue Tunnel Right Second 1) - True - Rotated Shapers & Triangles
+158465 - 0x334D5 (Blue Tunnel Right Second 2) - 0x00994 - Rotated Shapers & Triangles
+158466 - 0x00995 (Blue Tunnel Right Second 3) - 0x334D5 - Rotated Shapers & Triangles
+158467 - 0x00996 (Blue Tunnel Right Second 4) - 0x00995 - Rotated Shapers & Triangles
+158468 - 0x00998 (Blue Tunnel Right Second 5) - 0x00996 - Shapers & Triangles
+158469 - 0x009A4 (Blue Tunnel Left Third 1) - True - Shapers & Triangles
+158470 - 0x018A0 (Blue Tunnel Right Third 1) - True - Shapers & Symmetry & Eraser
+158471 - 0x00A72 (Blue Tunnel Left Fourth 1) - True - Shapers & Negative Shapers & Triangles
+158472 - 0x32962 (First Floor Left) - True - Rotated Shapers & Dots
+158473 - 0x32966 (First Floor Grounded) - True - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Triangles
+158474 - 0x01A31 (First Floor Middle) - True - Colored Squares
+158475 - 0x00B71 (First Floor Right) - True - Colored Squares & Stars & Stars + Same Colored Symbol & Eraser & Shapers & Negative Shapers & Dots
+158478 - 0x288EA (First Wooden Beam) - True - Colored Squares & Black/White Squares & Eraser
+158479 - 0x288FC (Second Wooden Beam) - True - Black/White Squares & Stars & Stars + Same Colored Symbol & Eraser
+158480 - 0x289E7 (Third Wooden Beam) - True - Black/White Squares & Stars & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Eraser
+158481 - 0x288AA (Fourth Wooden Beam) - True - Stars & Shapers & Eraser
+158482 - 0x17FB9 (Left Upstairs Single) - True - Stars & Dots & Full Dots
+158483 - 0x0A16B (Left Upstairs Left Row 1) - True - Dots & Full Dots & Black/White Squares
+158484 - 0x0A2CE (Left Upstairs Left Row 2) - 0x0A16B - Dots & Full Dots & Stars
+158485 - 0x0A2D7 (Left Upstairs Left Row 3) - 0x0A2CE - Dots & Full Dots & Shapers
+158486 - 0x0A2DD (Left Upstairs Left Row 4) - 0x0A2D7 - Dots & Full Dots & Triangles
+158487 - 0x0A2EA (Left Upstairs Left Row 5) - 0x0A2DD - Dots & Full Dots & Triangles & Eraser
+158488 - 0x0008F (Right Upstairs Left Row 1) - True - Dots
+158489 - 0x0006B (Right Upstairs Left Row 2) - 0x0008F - Black/White Squares & Colored Squares
+158490 - 0x0008B (Right Upstairs Left Row 3) - 0x0006B - Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol
+158491 - 0x0008C (Right Upstairs Left Row 4) - 0x0008B - Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Shapers
+158492 - 0x0008A (Right Upstairs Left Row 5) - 0x0008C - Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol
+158493 - 0x00089 (Right Upstairs Left Row 6) - 0x0008A - Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Rotated Shapers
+158494 - 0x0006A (Right Upstairs Left Row 7) - 0x00089 - Stars & Stars + Same Colored Symbol & Shapers & Negative Shapers
+158495 - 0x0006C (Right Upstairs Left Row 8) - 0x0006A - Dots & Shapers & Negative Shapers & Eraser
+158496 - 0x00027 (Right Upstairs Right Row 1) - True - Black/White Squares & Colored Squares & Eraser & Symmetry
+158497 - 0x00028 (Right Upstairs Right Row 2) - 0x00027 - Black/White Squares & Colored Squares & Eraser & Symmetry
+158498 - 0x00029 (Right Upstairs Right Row 3) - 0x00028 - Stars & Stars + Same Colored Symbol & Eraser & Symmetry
+158476 - 0x09DD5 (Lone Pillar) - True - Triangles & Dots
+Door - 0x019A5 (Pillar Door) - 0x09DD5
+158449 - 0x021D7 (Mountain Shortcut Panel) - True - Triangles
+Door - 0x2D73F (Mountain Shortcut Door) - 0x021D7
+158450 - 0x17CF2 (Swamp Shortcut Panel) - True - Triangles
+Door - 0x2D859 (Swamp Shortcut Door) - 0x17CF2
+159341 - 0x3397C (Skylight EP) - True - True
+
+Caves Path to Challenge (Caves) - Challenge - 0x0A19A:
+158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Shapers & Stars + Same Colored Symbol & Triangles
+Door - 0x0A19A (Challenge Entry) - 0x0A16E
+
+==Challenge==
+
+Challenge (Challenge) - Tunnels - 0x0348A - Challenge Vault - 0x04D75:
+158499 - 0x0A332 (Start Timer) - 11 Lasers - True
+158500 - 0x0088E (Small Basic) - 0x0A332 - True
+158501 - 0x00BAF (Big Basic) - 0x0088E - True
+158502 - 0x00BF3 (Square) - 0x00BAF - Black/White Squares
+158503 - 0x00C09 (Maze Map) - 0x00BF3 - Dots
+158504 - 0x00CDB (Stars and Dots) - 0x00C09 - Stars & Dots
+158505 - 0x0051F (Symmetry) - 0x00CDB - Symmetry & Colored Dots & Dots
+158506 - 0x00524 (Stars and Shapers) - 0x0051F - Stars & Shapers
+158507 - 0x00CD4 (Big Basic 2) - 0x00524 - True
+158508 - 0x00CB9 (Choice Squares Right) - 0x00CD4 - Black/White Squares
+158509 - 0x00CA1 (Choice Squares Middle) - 0x00CD4 - Black/White Squares
+158510 - 0x00C80 (Choice Squares Left) - 0x00CD4 - Black/White Squares
+158511 - 0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares
+158512 - 0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares
+158513 - 0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares
+158514 - 0x034F4 (Maze Hidden 1) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles
+158515 - 0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles
+158516 - 0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry
+158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Black/White Squares & Symmetry
+Door - 0x04D75 (Vault Door) - 0x1C31A & 0x1C319
+158518 - 0x039B4 (Tunnels Entry Panel) - True - Triangles & Dots
+Door - 0x0348A (Tunnels Entry) - 0x039B4
+159530 - 0x28B30 (Water EP) - True - True
+
+Challenge Vault (Challenge):
+158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True
+
+==Tunnels==
+
+Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Behind Elevator - 0x27263 - Town - 0x09E87:
+158668 - 0x2FAF6 (Vault Box) - True - True
+158519 - 0x27732 (Theater Shortcut Panel) - True - True
+Door - 0x27739 (Theater Shortcut) - 0x27732
+158520 - 0x2773D (Desert Shortcut Panel) - True - True
+Door - 0x27263 (Desert Shortcut) - 0x2773D
+158521 - 0x09E85 (Town Shortcut Panel) - True - Triangles & Dots
+Door - 0x09E87 (Town Shortcut) - 0x09E85
+159557 - 0x33A20 (Theater Flowers EP) - 0x03553 & Theater to Tunnels - True
+
+==Boat==
+
+The Ocean (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay:
+159042 - 0x22106 (Desert EP) - True - True
+159223 - 0x03B25 (Shipwreck CCW Underside EP) - True - True
+159231 - 0x28B29 (Shipwreck Green EP) - True - True
+159232 - 0x28B2A (Shipwreck CW Underside EP) - True - True
+159323 - 0x03D0D (Bunker Yellow Line EP) - True - True
+159515 - 0x28A37 (Town Long Sewer EP) - True - True
+159520 - 0x33857 (Tutorial EP) - True - True
+159521 - 0x33879 (Tutorial Reflection EP) - True - True
+159522 - 0x03C19 (Tutorial Moss EP) - True - True
+159531 - 0x035C9 (Cargo Box EP) - 0x0A0C9 - True
diff --git a/worlds/witness/data/static_items.py b/worlds/witness/data/static_items.py
index b0d8fc3c4f..e5103ef380 100644
--- a/worlds/witness/data/static_items.py
+++ b/worlds/witness/data/static_items.py
@@ -13,6 +13,22 @@ ITEM_GROUPS: Dict[str, Set[str]] = {}
# item list during get_progression_items.
_special_usefuls: List[str] = ["Puzzle Skip"]
+ALWAYS_GOOD_SYMBOL_ITEMS: Set[str] = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
+
+MODE_SPECIFIC_GOOD_ITEMS: Dict[str, Set[str]] = {
+ "none": set(),
+ "sigma_normal": set(),
+ "sigma_expert": {"Triangles"},
+ "umbra_variety": {"Triangles"}
+}
+
+MODE_SPECIFIC_GOOD_DISCARD_ITEMS: Dict[str, Set[str]] = {
+ "none": {"Triangles"},
+ "sigma_normal": {"Triangles"},
+ "sigma_expert": {"Arrows"},
+ "umbra_variety": set() # Variety Discards use both Arrows and Triangles, so neither of them are that useful alone
+}
+
def populate_items() -> None:
for item_name, definition in static_witness_logic.ALL_ITEMS.items():
diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py
index 87e1015257..58f2e894e8 100644
--- a/worlds/witness/data/static_logic.py
+++ b/worlds/witness/data/static_logic.py
@@ -17,6 +17,7 @@ from .utils import (
get_items,
get_sigma_expert_logic,
get_sigma_normal_logic,
+ get_umbra_variety_logic,
get_vanilla_logic,
logical_or_witness_rules,
parse_lambda,
@@ -292,6 +293,11 @@ def get_sigma_expert() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_sigma_expert_logic())
+@cache_argsless
+def get_umbra_variety() -> StaticWitnessLogicObj:
+ return StaticWitnessLogicObj(get_umbra_variety_logic())
+
+
def __getattr__(name: str) -> StaticWitnessLogicObj:
if name == "vanilla":
return get_vanilla()
@@ -299,6 +305,8 @@ def __getattr__(name: str) -> StaticWitnessLogicObj:
return get_sigma_normal()
if name == "sigma_expert":
return get_sigma_expert()
+ if name == "umbra_variety":
+ return get_umbra_variety()
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
diff --git a/worlds/witness/data/utils.py b/worlds/witness/data/utils.py
index 11f905b18a..84eca5afc4 100644
--- a/worlds/witness/data/utils.py
+++ b/worlds/witness/data/utils.py
@@ -215,6 +215,10 @@ def get_sigma_expert_logic() -> List[str]:
return get_adjustment_file("WitnessLogicExpert.txt")
+def get_umbra_variety_logic() -> List[str]:
+ return get_adjustment_file("WitnessLogicVariety.txt")
+
+
def get_vanilla_logic() -> List[str]:
return get_adjustment_file("WitnessLogicVanilla.txt")
diff --git a/worlds/witness/entity_hunt.py b/worlds/witness/entity_hunt.py
index 34cf7d3d7f..86881930c3 100644
--- a/worlds/witness/entity_hunt.py
+++ b/worlds/witness/entity_hunt.py
@@ -145,7 +145,7 @@ class EntityHuntPicker:
remaining_entities, remaining_entity_weights = [], []
for area, eligible_entities in self.ELIGIBLE_ENTITIES_PER_AREA.items():
- for panel in eligible_entities - self.HUNT_ENTITIES:
+ for panel in sorted(eligible_entities - self.HUNT_ENTITIES):
remaining_entities.append(panel)
remaining_entity_weights.append(allowance_per_area[area])
diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py
index 09c3f0b101..99e8eea2eb 100644
--- a/worlds/witness/hints.py
+++ b/worlds/witness/hints.py
@@ -53,7 +53,7 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]:
wincon = world.options.victory_condition
if discards:
- if difficulty == "sigma_expert":
+ if difficulty == "sigma_expert" or difficulty == "umbra_variety":
always.append("Arrows")
else:
always.append("Triangles")
@@ -220,7 +220,7 @@ def try_getting_location_group_for_location(world: "WitnessWorld", hint_loc: Loc
def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint:
location_name = hint.location.name
if hint.location.player != world.player:
- location_name += " (" + world.player_name + ")"
+ location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")"
item = hint.location.item
@@ -229,7 +229,7 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes
item_name = item.name
if item.player != world.player:
- item_name += " (" + world.player_name + ")"
+ item_name += " (" + world.multiworld.get_player_name(item.player) + ")"
hint_text = ""
area: Optional[str] = None
diff --git a/worlds/witness/options.py b/worlds/witness/options.py
index f91e5218c3..4de966abe9 100644
--- a/worlds/witness/options.py
+++ b/worlds/witness/options.py
@@ -250,10 +250,15 @@ class PanelHuntDiscourageSameAreaFactor(Range):
class PuzzleRandomization(Choice):
"""
Puzzles in this randomizer are randomly generated. This option changes the difficulty/types of puzzles.
+ "Sigma Normal" randomizes puzzles close to their original mechanics and difficulty.
+ "Sigma Expert" is an entirely new experience with extremely difficult random puzzles. Do not underestimate this mode, it is brutal.
+ "Umbra Variety" focuses on unique symbol combinations not featured in the original game. It is harder than Sigma Normal, but easier than Sigma Expert.
+ "None" means that the puzzles are unchanged from the original game.
"""
display_name = "Puzzle Randomization"
option_sigma_normal = 0
option_sigma_expert = 1
+ option_umbra_variety = 3
option_none = 2
diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py
index 4142ea5e04..72dfc2b7ee 100644
--- a/worlds/witness/player_items.py
+++ b/worlds/witness/player_items.py
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Dict, List, Set, cast
from BaseClasses import Item, ItemClassification, MultiWorld
from .data import static_items as static_witness_items
-from .data import static_logic as static_witness_logic
from .data.item_definition_classes import (
DoorItemDefinition,
ItemCategory,
@@ -155,16 +154,12 @@ class WitnessPlayerItems:
"""
output: Set[str] = set()
if self._world.options.shuffle_symbols:
- output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
+ discards_on = self._world.options.shuffle_discarded_panels
+ mode = self._world.options.puzzle_randomization.current_key
- if self._world.options.shuffle_discarded_panels:
- if self._world.options.puzzle_randomization == "sigma_expert":
- output.add("Arrows")
- else:
- output.add("Triangles")
-
- # Replace progressive items with their parents.
- output = {static_witness_logic.get_parent_progressive_item(item) for item in output}
+ output = static_witness_items.ALWAYS_GOOD_SYMBOL_ITEMS | static_witness_items.MODE_SPECIFIC_GOOD_ITEMS[mode]
+ if discards_on:
+ output |= static_witness_items.MODE_SPECIFIC_GOOD_DISCARD_ITEMS[mode]
# Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
# before create_items so that we'll be able to check placed items instead of just removing all items mentioned
diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py
index 027d1834d9..f8b7db3570 100644
--- a/worlds/witness/player_logic.py
+++ b/worlds/witness/player_logic.py
@@ -87,12 +87,14 @@ class WitnessPlayerLogic:
self.DIFFICULTY = world.options.puzzle_randomization
self.REFERENCE_LOGIC: StaticWitnessLogicObj
- if self.DIFFICULTY == "sigma_expert":
+ if self.DIFFICULTY == "sigma_normal":
+ self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
+ elif self.DIFFICULTY == "sigma_expert":
self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
+ elif self.DIFFICULTY == "umbra_variety":
+ self.REFERENCE_LOGIC = static_witness_logic.umbra_variety
elif self.DIFFICULTY == "none":
self.REFERENCE_LOGIC = static_witness_logic.vanilla
- else:
- self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py
index 7ff8c440ad..1df438f68b 100644
--- a/worlds/witness/regions.py
+++ b/worlds/witness/regions.py
@@ -30,6 +30,8 @@ class WitnessPlayerRegions:
self.reference_logic = static_witness_logic.sigma_normal
elif difficulty == "sigma_expert":
self.reference_logic = static_witness_logic.sigma_expert
+ elif difficulty == "umbra_variety":
+ self.reference_logic = static_witness_logic.umbra_variety
else:
self.reference_logic = static_witness_logic.vanilla
diff --git a/worlds/witness/test/test_lasers.py b/worlds/witness/test/test_lasers.py
index f09897ce40..5e60dfc521 100644
--- a/worlds/witness/test/test_lasers.py
+++ b/worlds/witness/test/test_lasers.py
@@ -96,6 +96,39 @@ class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase):
self.assert_can_beat_with_minimally(exact_requirement)
+class TestSymbolsRequiredToWinElevatorVariety(WitnessTestBase):
+ options = {
+ "shuffle_lasers": True,
+ "mountain_lasers": 1,
+ "victory_condition": "elevator",
+ "early_symbol_item": False,
+ "puzzle_randomization": "umbra_variety",
+ }
+
+ def test_symbols_to_win(self) -> None:
+ """
+ In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain.
+ This requires a very specific set of symbol items per puzzle randomization mode.
+ In this case, we check Variety Puzzles.
+ """
+
+ exact_requirement = {
+ "Monastery Laser": 1,
+ "Progressive Dots": 2,
+ "Progressive Stars": 2,
+ "Progressive Symmetry": 1,
+ "Black/White Squares": 1,
+ "Colored Squares": 1,
+ "Shapers": 1,
+ "Rotated Shapers": 1,
+ "Eraser": 1,
+ "Triangles": 1,
+ "Arrows": 1,
+ }
+
+ self.assert_can_beat_with_minimally(exact_requirement)
+
+
class TestPanelsRequiredToWinElevator(WitnessTestBase):
options = {
"shuffle_lasers": True,
diff --git a/worlds/witness/test/test_roll_other_options.py b/worlds/witness/test/test_roll_other_options.py
index bea278a042..7473716e06 100644
--- a/worlds/witness/test/test_roll_other_options.py
+++ b/worlds/witness/test/test_roll_other_options.py
@@ -54,6 +54,7 @@ class TestMaxEntityShuffle(WitnessTestBase):
class TestPostgameGroupedDoors(WitnessTestBase):
options = {
+ "puzzle_randomization": "umbra_variety",
"shuffle_postgame": True,
"shuffle_discarded_panels": True,
"shuffle_doors": "doors",
diff --git a/worlds/witness/test/test_symbol_shuffle.py b/worlds/witness/test/test_symbol_shuffle.py
index 8012480075..3be874f3c0 100644
--- a/worlds/witness/test/test_symbol_shuffle.py
+++ b/worlds/witness/test/test_symbol_shuffle.py
@@ -46,6 +46,9 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
{
"puzzle_randomization": "none",
},
+ {
+ "puzzle_randomization": "umbra_variety",
+ }
]
common_options = {
@@ -63,12 +66,15 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
self.assertFalse(self.get_items_by_name("Arrows", 1))
self.assertTrue(self.get_items_by_name("Arrows", 2))
self.assertFalse(self.get_items_by_name("Arrows", 3))
+ self.assertTrue(self.get_items_by_name("Arrows", 4))
with self.subTest("Test that Discards ask for Triangles in normal, but Arrows in expert."):
desert_discard = "0x17CE7"
triangles = frozenset({frozenset({"Triangles"})})
arrows = frozenset({frozenset({"Arrows"})})
+ both = frozenset({frozenset({"Triangles", "Arrows"})})
self.assertEqual(self.multiworld.worlds[1].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
self.assertEqual(self.multiworld.worlds[2].player_logic.REQUIREMENTS_BY_HEX[desert_discard], arrows)
self.assertEqual(self.multiworld.worlds[3].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
+ self.assertEqual(self.multiworld.worlds[4].player_logic.REQUIREMENTS_BY_HEX[desert_discard], both)
|