From c984b48149f6933c5b99dcda34957bb767131d7e Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 7 Nov 2023 07:39:36 +0100 Subject: [PATCH 001/104] The Witness: Fix Town Tower 4th Door Logic (#2421) --- worlds/witness/settings/Disable_Unrandomized.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt index f7a0fcb7cbd6..3cd7ec1fb5eb 100644 --- a/worlds/witness/settings/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Disable_Unrandomized.txt @@ -9,7 +9,7 @@ Requirement Changes: 0x181B3 - 0x00021 | 0x17D28 | 0x17C71 0x28B39 - True - Reflection 0x17CAB - True - True -0x2779A - True - 0x17CFB | 0x3C12B | 0x17CF7 +0x2779A - 0x17CFB | 0x3C12B | 0x17CF7 Disabled Locations: 0x03505 (Tutorial Gate Close) @@ -125,4 +125,4 @@ Precompleted Locations: 0x035F5 0x000D3 0x33A20 -0x03BE2 \ No newline at end of file +0x03BE2 From 5a7d69c8b42b03e401390b4d2df32a4961146d66 Mon Sep 17 00:00:00 2001 From: TheLynk <44308308+TheLynk@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:31:06 +0100 Subject: [PATCH 002/104] ChecksFinder: Tweak link in ChecksFinder (#2353) Co-authored-by: Ludovic Marechal Co-authored-by: Marech Co-authored-by: Fabian Dill Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/checksfinder/__init__.py | 4 ++-- worlds/checksfinder/docs/{checksfinder_en.md => setup_en.md} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename worlds/checksfinder/docs/{checksfinder_en.md => setup_en.md} (100%) diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index 4978500da0cb..621e8f5c37b2 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -14,8 +14,8 @@ class ChecksFinderWeb(WebWorld): "A guide to setting up the Archipelago ChecksFinder software on your computer. This guide covers " "single-player, multiworld, and related software.", "English", - "checksfinder_en.md", - "checksfinder/en", + "setup_en.md", + "setup/en", ["Mewlif"] )] diff --git a/worlds/checksfinder/docs/checksfinder_en.md b/worlds/checksfinder/docs/setup_en.md similarity index 100% rename from worlds/checksfinder/docs/checksfinder_en.md rename to worlds/checksfinder/docs/setup_en.md From 72cb8b7d6080a088a3f129f1f3e7cf09e4949daf Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 7 Nov 2023 21:02:28 +0100 Subject: [PATCH 003/104] Factorio: inflate location pool (#2422) --- worlds/factorio/Locations.py | 9 ++------- worlds/factorio/__init__.py | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/worlds/factorio/Locations.py b/worlds/factorio/Locations.py index f9db5f4a2bd8..52f0954cba30 100644 --- a/worlds/factorio/Locations.py +++ b/worlds/factorio/Locations.py @@ -3,18 +3,13 @@ from .Technologies import factorio_base_id from .Options import MaxSciencePack -boundary: int = 0xff -total_locations: int = 0xff - -assert total_locations <= boundary - def make_pools() -> Dict[str, List[str]]: pools: Dict[str, List[str]] = {} for i, pack in enumerate(MaxSciencePack.get_ordered_science_packs(), start=1): - max_needed: int = 0xff + max_needed: int = 999 prefix: str = f"AP-{i}-" - pools[pack] = [prefix + hex(x)[2:].upper().zfill(2) for x in range(1, max_needed + 1)] + pools[pack] = [prefix + str(x).upper().zfill(3) for x in range(1, max_needed + 1)] return pools diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 8308bb2d6559..eb078720c668 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -541,7 +541,7 @@ def __init__(self, player: int, name: str, address: int, parent: Region): super(FactorioScienceLocation, self).__init__(player, name, address, parent) # "AP-{Complexity}-{Cost}" self.complexity = int(self.name[3]) - 1 - self.rel_cost = int(self.name[5:], 16) + self.rel_cost = int(self.name[5:]) self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1} for complexity in range(self.complexity): From 779a31265052d6e660e4cc165e3ab808bc92630a Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Tue, 7 Nov 2023 15:41:13 -0500 Subject: [PATCH 004/104] Docs, Undertale: Added Suggestions Missed in #2285 (#2435) Co-authored-by: jonloveslegos <68133186+jonloveslegos@users.noreply.github.com> Co-authored-by: kindasneaki Co-authored-by: ScootyPuffJr1 <77215594+scootypuffjr1@users.noreply.github.com> --- UndertaleClient.py | 6 +++--- worlds/undertale/docs/en_Undertale.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/UndertaleClient.py b/UndertaleClient.py index 62fbe128bdb9..e1538ce81d2e 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -27,14 +27,14 @@ def _cmd_resync(self): self.ctx.syncing = True def _cmd_patch(self): - """Patch the game.""" + """Patch the game. Only use this command if /auto_patch fails.""" if isinstance(self.ctx, UndertaleContext): os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) self.ctx.patch_game() self.output("Patched.") def _cmd_savepath(self, directory: str): - """Redirect to proper save data folder. (Use before connecting!)""" + """Redirect to proper save data folder. This is necessary for Linux users to use before connecting.""" if isinstance(self.ctx, UndertaleContext): self.ctx.save_game_folder = directory self.output("Changed to the following directory: " + self.ctx.save_game_folder) @@ -67,7 +67,7 @@ def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None): self.output("Patching successful!") def _cmd_online(self): - """Makes you no longer able to see other Undertale players.""" + """Toggles seeing other Undertale players.""" if isinstance(self.ctx, UndertaleContext): self.ctx.update_online_mode(not ("Online" in self.ctx.tags)) if "Online" in self.ctx.tags: diff --git a/worlds/undertale/docs/en_Undertale.md b/worlds/undertale/docs/en_Undertale.md index 87011ee16b4d..7ff5d55edad9 100644 --- a/worlds/undertale/docs/en_Undertale.md +++ b/worlds/undertale/docs/en_Undertale.md @@ -56,8 +56,8 @@ If you press `W` while in the save menu, you will teleport back to the flower ro The following commands are only available when using the UndertaleClient to play with Archipelago. - `/resync` Manually trigger a resync. -- `/patch` Patch the game. -- `/savepath` Redirect to proper save data folder. (Use before connecting!) +- `/savepath` Redirect to proper save data folder. This is necessary for Linux users to use before connecting. - `/auto_patch` Patch the game automatically. -- `/online` Makes you no longer able to see other Undertale players. +- `/patch` Patch the game. Only use this command if `/auto_patch` fails. +- `/online` Toggles seeing other Undertale players. - `/deathlink` Toggles deathlink From ced35c5b78a5d2d23fce8c7938dbe95fe3a0f07d Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:51:35 -0600 Subject: [PATCH 005/104] CommonClient: Add a hints tab (#2392) Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> --- CommonClient.py | 7 +- data/client.kv | 75 +++++++++++++- kvui.py | 263 ++++++++++++++++++++++++++++++++++-------------- 3 files changed, 262 insertions(+), 83 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index a5e9b4553ab4..0952b08a58e7 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -758,6 +758,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict): ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()} ctx.hint_points = args.get("hint_points", 0) ctx.consume_players_package(args["players"]) + ctx.stored_data_notification_keys.add(f"_read_hints_{ctx.team}_{ctx.slot}") msgs = [] if ctx.locations_checked: msgs.append({"cmd": "LocationChecks", @@ -836,10 +837,14 @@ async def process_server_cmd(ctx: CommonContext, args: dict): elif cmd == "Retrieved": ctx.stored_data.update(args["keys"]) + if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" in args["keys"]: + ctx.ui.update_hints() elif cmd == "SetReply": ctx.stored_data[args["key"]] = args["value"] - if args["key"].startswith("EnergyLink"): + if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" == args["key"]: + ctx.ui.update_hints() + elif args["key"].startswith("EnergyLink"): ctx.current_energy_link_value = args["value"] if ctx.ui: ctx.ui.set_new_energy_link_value() diff --git a/data/client.kv b/data/client.kv index f0e36169002a..3b48d216ddb3 100644 --- a/data/client.kv +++ b/data/client.kv @@ -17,6 +17,12 @@ color: "FFFFFF" : tab_width: root.width / app.tab_count +: + text_size: self.width, None + size_hint_y: None + height: self.texture_size[1] + font_size: dp(20) + markup: True : canvas.before: Color: @@ -24,11 +30,6 @@ Rectangle: size: self.size pos: self.pos - text_size: self.width, None - size_hint_y: None - height: self.texture_size[1] - font_size: dp(20) - markup: True : messages: 1000 # amount of messages stored in client logs. cols: 1 @@ -44,6 +45,70 @@ height: self.minimum_height orientation: 'vertical' spacing: dp(3) +: + canvas.before: + Color: + rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1) if self.striped else (0.18, 0.18, 0.18, 1) + Rectangle: + size: self.size + pos: self.pos + height: self.minimum_height + receiving_text: "Receiving Player" + item_text: "Item" + finding_text: "Finding Player" + location_text: "Location" + entrance_text: "Entrance" + found_text: "Found?" + TooltipLabel: + id: receiving + text: root.receiving_text + halign: 'center' + valign: 'center' + pos_hint: {"center_y": 0.5} + TooltipLabel: + id: item + text: root.item_text + halign: 'center' + valign: 'center' + pos_hint: {"center_y": 0.5} + TooltipLabel: + id: finding + text: root.finding_text + halign: 'center' + valign: 'center' + pos_hint: {"center_y": 0.5} + TooltipLabel: + id: location + text: root.location_text + halign: 'center' + valign: 'center' + pos_hint: {"center_y": 0.5} + TooltipLabel: + id: entrance + text: root.entrance_text + halign: 'center' + valign: 'center' + pos_hint: {"center_y": 0.5} + TooltipLabel: + id: found + text: root.found_text + halign: 'center' + valign: 'center' + pos_hint: {"center_y": 0.5} +: + cols: 1 + viewclass: 'HintLabel' + scroll_y: self.height + scroll_type: ["content", "bars"] + bar_width: dp(12) + effect_cls: "ScrollEffect" + SelectableRecycleBoxLayout: + default_size: None, dp(20) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' + spacing: dp(3) : text: "Server:" size_hint_x: None diff --git a/kvui.py b/kvui.py index 71bf80c86d9b..22e179d5be94 100644 --- a/kvui.py +++ b/kvui.py @@ -5,12 +5,13 @@ if sys.platform == "win32": import ctypes + # kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout # by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's try: ctypes.windll.shcore.SetProcessDpiAwareness(0) except FileNotFoundError: # shcore may not be found on <= Windows 7 - pass # TODO: remove silent except when Python 3.8 is phased out. + pass # TODO: remove silent except when Python 3.8 is phased out. os.environ["KIVY_NO_CONSOLELOG"] = "1" os.environ["KIVY_NO_FILELOG"] = "1" @@ -18,14 +19,15 @@ os.environ["KIVY_LOG_ENABLE"] = "0" import Utils + if Utils.is_frozen(): os.environ["KIVY_DATA_DIR"] = Utils.local_path("data") from kivy.config import Config Config.set("input", "mouse", "mouse,disable_multitouch") -Config.set('kivy', 'exit_on_escape', '0') -Config.set('graphics', 'multisamples', '0') # multisamples crash old intel drivers +Config.set("kivy", "exit_on_escape", "0") +Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers from kivy.app import App from kivy.core.window import Window @@ -58,7 +60,6 @@ fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25) - from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType from Utils import async_start @@ -77,8 +78,8 @@ class HoverBehavior(object): border_point = ObjectProperty(None) def __init__(self, **kwargs): - self.register_event_type('on_enter') - self.register_event_type('on_leave') + self.register_event_type("on_enter") + self.register_event_type("on_leave") Window.bind(mouse_pos=self.on_mouse_pos) Window.bind(on_cursor_leave=self.on_cursor_leave) super(HoverBehavior, self).__init__(**kwargs) @@ -106,7 +107,7 @@ def on_cursor_leave(self, *args): self.dispatch("on_leave") -Factory.register('HoverBehavior', HoverBehavior) +Factory.register("HoverBehavior", HoverBehavior) class ToolTip(Label): @@ -121,6 +122,60 @@ class HovererableLabel(HoverBehavior, Label): pass +class TooltipLabel(HovererableLabel): + tooltip = None + + def create_tooltip(self, text, x, y): + text = text.replace("
", "\n").replace("&", "&").replace("&bl;", "[").replace("&br;", "]") + if self.tooltip: + # update + self.tooltip.children[0].text = text + else: + self.tooltip = FloatLayout() + tooltip_label = ToolTip(text=text) + self.tooltip.add_widget(tooltip_label) + fade_in_animation.start(self.tooltip) + App.get_running_app().root.add_widget(self.tooltip) + + # handle left-side boundary to not render off-screen + x = max(x, 3 + self.tooltip.children[0].texture_size[0] / 2) + + # position float layout + self.tooltip.x = x - self.tooltip.width / 2 + self.tooltip.y = y - self.tooltip.height / 2 + 48 + + def remove_tooltip(self): + if self.tooltip: + App.get_running_app().root.remove_widget(self.tooltip) + self.tooltip = None + + def on_mouse_pos(self, window, pos): + if not self.get_root_window(): + return # Abort if not displayed + super().on_mouse_pos(window, pos) + if self.refs and self.hovered: + + tx, ty = self.to_widget(*pos, relative=True) + # Why TF is Y flipped *within* the texture? + ty = self.texture_size[1] - ty + hit = False + for uid, zones in self.refs.items(): + for zone in zones: + x, y, w, h = zone + if x <= tx <= w and y <= ty <= h: + self.create_tooltip(uid.split("|", 1)[1], *pos) + hit = True + break + if not hit: + self.remove_tooltip() + + def on_enter(self): + pass + + def on_leave(self): + self.remove_tooltip() + + class ServerLabel(HovererableLabel): def __init__(self, *args, **kwargs): super(HovererableLabel, self).__init__(*args, **kwargs) @@ -189,11 +244,10 @@ class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior, """ Adds selection and focus behaviour to the view. """ -class SelectableLabel(RecycleDataViewBehavior, HovererableLabel): +class SelectableLabel(RecycleDataViewBehavior, TooltipLabel): """ Add selection support to the Label """ index = None selected = BooleanProperty(False) - tooltip = None def refresh_view_attrs(self, rv, index, data): """ Catch and handle the view changes """ @@ -201,56 +255,6 @@ def refresh_view_attrs(self, rv, index, data): return super(SelectableLabel, self).refresh_view_attrs( rv, index, data) - def create_tooltip(self, text, x, y): - text = text.replace("
", "\n").replace('&', '&').replace('&bl;', '[').replace('&br;', ']') - if self.tooltip: - # update - self.tooltip.children[0].text = text - else: - self.tooltip = FloatLayout() - tooltip_label = ToolTip(text=text) - self.tooltip.add_widget(tooltip_label) - fade_in_animation.start(self.tooltip) - App.get_running_app().root.add_widget(self.tooltip) - - # handle left-side boundary to not render off-screen - x = max(x, 3+self.tooltip.children[0].texture_size[0] / 2) - - # position float layout - self.tooltip.x = x - self.tooltip.width / 2 - self.tooltip.y = y - self.tooltip.height / 2 + 48 - - def remove_tooltip(self): - if self.tooltip: - App.get_running_app().root.remove_widget(self.tooltip) - self.tooltip = None - - def on_mouse_pos(self, window, pos): - if not self.get_root_window(): - return # Abort if not displayed - super().on_mouse_pos(window, pos) - if self.refs and self.hovered: - - tx, ty = self.to_widget(*pos, relative=True) - # Why TF is Y flipped *within* the texture? - ty = self.texture_size[1] - ty - hit = False - for uid, zones in self.refs.items(): - for zone in zones: - x, y, w, h = zone - if x <= tx <= w and y <= ty <= h: - self.create_tooltip(uid.split("|", 1)[1], *pos) - hit = True - break - if not hit: - self.remove_tooltip() - - def on_enter(self): - pass - - def on_leave(self): - self.remove_tooltip() - def on_touch_down(self, touch): """ Add selection on touch down """ if super(SelectableLabel, self).on_touch_down(touch): @@ -274,7 +278,7 @@ def on_touch_down(self, touch): elif not cmdinput.text and text.startswith("Missing: "): cmdinput.text = text.replace("Missing: ", "!hint_location ") - Clipboard.copy(text.replace('&', '&').replace('&bl;', '[').replace('&br;', ']')) + Clipboard.copy(text.replace("&", "&").replace("&bl;", "[").replace("&br;", "]")) return self.parent.select_with_touch(self.index, touch) def apply_selection(self, rv, index, is_selected): @@ -282,9 +286,68 @@ def apply_selection(self, rv, index, is_selected): self.selected = is_selected +class HintLabel(RecycleDataViewBehavior, BoxLayout): + selected = BooleanProperty(False) + striped = BooleanProperty(False) + index = None + no_select = [] + + def __init__(self): + super(HintLabel, self).__init__() + self.receiving_text = "" + self.item_text = "" + self.finding_text = "" + self.location_text = "" + self.entrance_text = "" + self.found_text = "" + for child in self.children: + child.bind(texture_size=self.set_height) + + def set_height(self, instance, value): + self.height = max([child.texture_size[1] for child in self.children]) + + def refresh_view_attrs(self, rv, index, data): + self.index = index + if "select" in data and not data["select"] and index not in self.no_select: + self.no_select.append(index) + self.striped = data["striped"] + self.receiving_text = data["receiving"]["text"] + self.item_text = data["item"]["text"] + self.finding_text = data["finding"]["text"] + self.location_text = data["location"]["text"] + self.entrance_text = data["entrance"]["text"] + self.found_text = data["found"]["text"] + self.height = self.minimum_height + return super(HintLabel, self).refresh_view_attrs(rv, index, data) + + def on_touch_down(self, touch): + """ Add selection on touch down """ + if super(HintLabel, self).on_touch_down(touch): + return True + if self.index not in self.no_select: + if self.collide_point(*touch.pos): + if self.selected: + self.parent.clear_selection() + else: + text = "".join([self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ", + self.finding_text, "\'s World", (" at " + self.entrance_text) + if self.entrance_text != "Vanilla" + else "", ". (", self.found_text.lower(), ")"]) + temp = MarkupLabel(text).markup + text = "".join( + part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]"))) + Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]")) + return self.parent.select_with_touch(self.index, touch) + + def apply_selection(self, rv, index, is_selected): + """ Respond to the selection of items in the view. """ + if self.index not in self.no_select: + self.selected = is_selected + + class ConnectBarTextInput(TextInput): def insert_text(self, substring, from_undo=False): - s = substring.replace('\n', '').replace('\r', '') + s = substring.replace("\n", "").replace("\r", "") return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo) @@ -302,7 +365,7 @@ def __init__(self, **kwargs): def __init__(self, title, text, error=False, **kwargs): label = MessageBox.MessageBoxLabel(text=text) separator_color = [217 / 255, 129 / 255, 122 / 255, 1.] if error else [47 / 255., 167 / 255., 212 / 255, 1.] - super().__init__(title=title, content=label, size_hint=(None, None), width=max(100, int(label.width)+40), + super().__init__(title=title, content=label, size_hint=(None, None), width=max(100, int(label.width) + 40), separator_color=separator_color, **kwargs) self.height += max(0, label.height - 18) @@ -358,11 +421,14 @@ def build(self) -> Layout: # top part server_label = ServerLabel() self.connect_layout.add_widget(server_label) - self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:", size_hint_y=None, + self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:", + size_hint_y=None, height=dp(30), multiline=False, write_tab=False) + def connect_bar_validate(sender): if not self.ctx.server: self.connect_button_action(sender) + self.server_connect_bar.bind(on_text_validate=connect_bar_validate) self.connect_layout.add_widget(self.server_connect_bar) self.server_connect_button = Button(text="Connect", size=(dp(100), dp(30)), size_hint_y=None, size_hint_x=None) @@ -383,20 +449,22 @@ def connect_bar_validate(sender): bridge_logger = logging.getLogger(logger_name) panel = TabbedPanelItem(text=display_name) self.log_panels[display_name] = panel.content = UILog(bridge_logger) - self.tabs.add_widget(panel) + if len(self.logging_pairs) > 1: + # show Archipelago tab if other logging is present + self.tabs.add_widget(panel) + + hint_panel = TabbedPanelItem(text="Hints") + self.log_panels["Hints"] = hint_panel.content = HintLog(self.json_to_kivy_parser) + self.tabs.add_widget(hint_panel) + + if len(self.logging_pairs) == 1: + self.tabs.default_tab_text = "Archipelago" self.main_area_container = GridLayout(size_hint_y=1, rows=1) self.main_area_container.add_widget(self.tabs) self.grid.add_widget(self.main_area_container) - if len(self.logging_pairs) == 1: - # Hide Tab selection if only one tab - self.tabs.clear_tabs() - self.tabs.do_default_tab = False - self.tabs.current_tab.height = 0 - self.tabs.tab_height = 0 - # bottom part bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30)) info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None) @@ -422,7 +490,7 @@ def connect_bar_validate(sender): return self.container def update_texts(self, dt): - if hasattr(self.tabs.content.children[0], 'fix_heights'): + if hasattr(self.tabs.content.children[0], "fix_heights"): self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream if self.ctx.server: self.title = self.base_title + " " + Utils.__version__ + \ @@ -499,6 +567,10 @@ def set_new_energy_link_value(self): if hasattr(self, "energy_link_label"): self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J" + def update_hints(self): + hints = self.ctx.stored_data[f"_read_hints_{self.ctx.team}_{self.ctx.slot}"] + self.log_panels["Hints"].refresh_hints(hints) + # default F1 keybind, opens a settings menu, that seems to break the layout engine once closed def open_settings(self, *largs): pass @@ -513,12 +585,12 @@ def __init__(self, on_log): def format_compact(record: logging.LogRecord) -> str: if isinstance(record.msg, Exception): return str(record.msg) - return (f'{record.exc_info[1]}\n' if record.exc_info else '') + str(record.msg).split("\n")[0] + return (f"{record.exc_info[1]}\n" if record.exc_info else "") + str(record.msg).split("\n")[0] def handle(self, record: logging.LogRecord) -> None: - if getattr(record, 'skip_gui', False): + if getattr(record, "skip_gui", False): pass # skip output - elif getattr(record, 'compact_gui', False): + elif getattr(record, "compact_gui", False): self.on_log(self.format_compact(record)) else: self.on_log(self.format(record)) @@ -552,6 +624,44 @@ def fix_heights(self): element.height = element.texture_size[1] +class HintLog(RecycleView): + header = { + "receiving": {"text": "[u]Receiving Player[/u]"}, + "item": {"text": "[u]Item[/u]"}, + "finding": {"text": "[u]Finding Player[/u]"}, + "location": {"text": "[u]Location[/u]"}, + "entrance": {"text": "[u]Entrance[/u]"}, + "found": {"text": "[u]Status[/u]"}, + "striped": True, + "select": False, + } + + def __init__(self, parser): + super(HintLog, self).__init__() + self.data = [self.header] + self.parser = parser + + def refresh_hints(self, hints): + self.data = [self.header] + striped = False + for hint in hints: + self.data.append({ + "striped": striped, + "receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})}, + "item": {"text": self.parser.handle_node( + {"type": "item_id", "text": hint["item"], "flags": hint["item_flags"]})}, + "finding": {"text": self.parser.handle_node({"type": "player_id", "text": hint["finding_player"]})}, + "location": {"text": self.parser.handle_node({"type": "location_id", "text": hint["location"]})}, + "entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text", + "color": "blue", "text": hint["entrance"] + if hint["entrance"] else "Vanilla"})}, + "found": { + "text": self.parser.handle_node({"type": "color", "color": "green" if hint["found"] else "red", + "text": "Found" if hint["found"] else "Not Found"})}, + }) + striped = not striped + + class E(ExceptionHandler): logger = logging.getLogger("Client") @@ -599,7 +709,7 @@ def _handle_player_id(self, node: JSONMessagePart): f"Type: {SlotType(slot_info.type).name}" if slot_info.group_members: text += f"
Members:
" + \ - '
'.join(self.ctx.player_names[player] for player in slot_info.group_members) + "
".join(self.ctx.player_names[player] for player in slot_info.group_members) node.setdefault("refs", []).append(text) return super(KivyJSONtoTextParser, self)._handle_player_id(node) @@ -627,4 +737,3 @@ def _handle_text(self, node: JSONMessagePart): if os.path.exists(user_file): logging.info("Loading user.kv into builder.") Builder.load_file(user_file) - From 03e1c45d71ebea385db84de14e513799b8c1670c Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 8 Nov 2023 02:15:06 -0600 Subject: [PATCH 006/104] Tests: log the seed fo slot_data failures (#2402) --- test/general/test_implemented.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index b60bcee46784..624be710185d 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -40,8 +40,8 @@ def test_slot_data(self): # has an await for generate_output which isn't being called if game_name in {"Ocarina of Time", "Zillion"}: continue - with self.subTest(game_name): - multiworld = setup_solo_multiworld(world_type) + multiworld = setup_solo_multiworld(world_type) + with self.subTest(game=game_name, seed=multiworld.seed): distribute_items_restrictive(multiworld) call_all(multiworld, "post_fill") for key, data in multiworld.worlds[1].fill_slot_data().items(): From 504d09daf6e4422ca1c332fe921707b1108e5d55 Mon Sep 17 00:00:00 2001 From: Mewlif <68133186+jonloveslegos@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:50:29 -0500 Subject: [PATCH 007/104] Undertale: Logic fixes (#2436) --- worlds/undertale/Regions.py | 4 +++- worlds/undertale/Rules.py | 18 +++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/worlds/undertale/Regions.py b/worlds/undertale/Regions.py index ec13b249fa0e..138a6846537a 100644 --- a/worlds/undertale/Regions.py +++ b/worlds/undertale/Regions.py @@ -24,6 +24,7 @@ def link_undertale_areas(world: MultiWorld, player: int): ("True Lab", []), ("Core", ["Core Exit"]), ("New Home", ["New Home Exit"]), + ("Last Corridor", ["Last Corridor Exit"]), ("Barrier", []), ] @@ -40,7 +41,8 @@ def link_undertale_areas(world: MultiWorld, player: int): ("News Show Entrance", "News Show"), ("Lab Elevator", "True Lab"), ("Core Exit", "New Home"), - ("New Home Exit", "Barrier"), + ("New Home Exit", "Last Corridor"), + ("Last Corridor Exit", "Barrier"), ("Snowdin Hub", "Snowdin Forest"), ("Waterfall Hub", "Waterfall"), ("Hotland Hub", "Hotland"), diff --git a/worlds/undertale/Rules.py b/worlds/undertale/Rules.py index 648152c50414..897484b0508f 100644 --- a/worlds/undertale/Rules.py +++ b/worlds/undertale/Rules.py @@ -81,23 +81,27 @@ def set_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_entrance("New Home Exit", player), lambda state: (state.has("Left Home Key", player) and state.has("Right Home Key", player)) or - state.has("Key Piece", player, state.multiworld.key_pieces[player])) + state.has("Key Piece", player, state.multiworld.key_pieces[player].value)) if _undertale_is_route(multiworld.state, player, 1): set_rule(multiworld.get_entrance("Papyrus\" Home Entrance", player), lambda state: _undertale_has_plot(state, player, "Complete Skeleton")) set_rule(multiworld.get_entrance("Undyne\"s Home Entrance", player), lambda state: _undertale_has_plot(state, player, "Fish") and state.has("Papyrus Date", player)) set_rule(multiworld.get_entrance("Lab Elevator", player), - lambda state: state.has("Alphys Date", player) and _undertale_has_plot(state, player, "DT Extractor")) + lambda state: state.has("Alphys Date", player) and state.has("DT Extractor", player) and + ((state.has("Left Home Key", player) and state.has("Right Home Key", player)) or + state.has("Key Piece", player, state.multiworld.key_pieces[player].value))) set_rule(multiworld.get_location("Alphys Date", player), - lambda state: state.has("Undyne Letter EX", player) and state.has("Undyne Date", player)) + lambda state: state.can_reach("New Home", "Region", player) and state.has("Undyne Letter EX", player) + and state.has("Undyne Date", player)) set_rule(multiworld.get_location("Papyrus Plot", player), lambda state: state.can_reach("Snowdin Town", "Region", player)) set_rule(multiworld.get_location("Undyne Plot", player), lambda state: state.can_reach("Waterfall", "Region", player)) set_rule(multiworld.get_location("True Lab Plot", player), lambda state: state.can_reach("New Home", "Region", player) - and state.can_reach("Letter Quest", "Location", player)) + and state.can_reach("Letter Quest", "Location", player) + and state.can_reach("Alphys Date", "Location", player)) set_rule(multiworld.get_location("Chisps Machine", player), lambda state: state.can_reach("True Lab", "Region", player)) set_rule(multiworld.get_location("Dog Sale 1", player), @@ -113,7 +117,7 @@ def set_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_location("Hush Trade", player), lambda state: state.can_reach("News Show", "Region", player) and state.has("Hot Dog...?", player, 1)) set_rule(multiworld.get_location("Letter Quest", player), - lambda state: state.can_reach("New Home Exit", "Entrance", player) and state.has("Undyne Date", player)) + lambda state: state.can_reach("Last Corridor", "Region", player) and state.has("Undyne Date", player)) if (not _undertale_is_route(multiworld.state, player, 2)) or _undertale_is_route(multiworld.state, player, 3): set_rule(multiworld.get_location("Nicecream Punch Card", player), lambda state: state.has("Punch Card", player, 3) and state.can_reach("Waterfall", "Region", player)) @@ -126,7 +130,7 @@ def set_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_location("Apron Hidden", player), lambda state: state.can_reach("Cooking Show", "Region", player)) if _undertale_is_route(multiworld.state, player, 2) and \ - (multiworld.rando_love[player] or multiworld.rando_stats[player]): + (bool(multiworld.rando_love[player].value) or bool(multiworld.rando_stats[player].value)): maxlv = 1 exp = 190 curarea = "Old Home" @@ -304,7 +308,7 @@ def set_rules(multiworld: MultiWorld, player: int): # Sets rules on completion condition def set_completion_rules(multiworld: MultiWorld, player: int): - completion_requirements = lambda state: state.can_reach("New Home Exit", "Entrance", player) + completion_requirements = lambda state: state.can_reach("Barrier", "Region", player) if _undertale_is_route(multiworld.state, player, 1): completion_requirements = lambda state: state.can_reach("True Lab", "Region", player) From 154e17f4ff161e833816c857939cc7115c20ae10 Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Wed, 8 Nov 2023 19:00:55 +0100 Subject: [PATCH 008/104] SC2: 0.4.3 bugfixes (#2273) Co-authored-by: Matthew --- worlds/sc2wol/Client.py | 9 +++++++-- worlds/sc2wol/Locations.py | 8 ++++---- worlds/sc2wol/Options.py | 8 ++++---- worlds/sc2wol/PoolFilter.py | 35 +++++++++++++++++++++++------------ worlds/sc2wol/Starcraft2.kv | 2 +- worlds/sc2wol/__init__.py | 4 ++-- 6 files changed, 41 insertions(+), 25 deletions(-) diff --git a/worlds/sc2wol/Client.py b/worlds/sc2wol/Client.py index a9bb826b7447..3dbd2047debd 100644 --- a/worlds/sc2wol/Client.py +++ b/worlds/sc2wol/Client.py @@ -9,6 +9,7 @@ import os.path import re import sys +import tempfile import typing import queue import zipfile @@ -286,6 +287,8 @@ async def server_auth(self, password_requested: bool = False): await super(SC2Context, self).server_auth(password_requested) await self.get_username() await self.send_connect() + if self.ui: + self.ui.first_check = True def on_package(self, cmd: str, args: dict): if cmd in {"Connected"}: @@ -1166,10 +1169,12 @@ def download_latest_release_zip(owner: str, repo: str, api_version: str, metadat r2 = requests.get(download_url, headers=headers) if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)): - with open(f"{repo}.zip", "wb") as fh: + tempdir = tempfile.gettempdir() + file = tempdir + os.sep + f"{repo}.zip" + with open(file, "wb") as fh: fh.write(r2.content) sc2_logger.info(f"Successfully downloaded {repo}.zip.") - return f"{repo}.zip", latest_metadata + return file, latest_metadata else: sc2_logger.warning(f"Status code: {r2.status_code}") sc2_logger.warning("Download failed.") diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index ae31fa8eaadd..fba7051337df 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -68,10 +68,10 @@ def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tu lambda state: state._sc2wol_has_common_unit(multiworld, player) and (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player) or state._sc2wol_has_competent_anti_air(multiworld, player))), - LocationData("Evacuation", "Evacuation: First Chrysalis", SC2WOL_LOC_ID_OFFSET + 401, LocationType.BONUS), - LocationData("Evacuation", "Evacuation: Second Chrysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationType.BONUS, + LocationData("Evacuation", "Evacuation: North Chrysalis", SC2WOL_LOC_ID_OFFSET + 401, LocationType.BONUS), + LocationData("Evacuation", "Evacuation: West Chrysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationType.BONUS, lambda state: state._sc2wol_has_common_unit(multiworld, player)), - LocationData("Evacuation", "Evacuation: Third Chrysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationType.BONUS, + LocationData("Evacuation", "Evacuation: East Chrysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationType.BONUS, lambda state: state._sc2wol_has_common_unit(multiworld, player)), LocationData("Evacuation", "Evacuation: Reach Hanson", SC2WOL_LOC_ID_OFFSET + 404, LocationType.MISSION_PROGRESS), LocationData("Evacuation", "Evacuation: Secret Resource Stash", SC2WOL_LOC_ID_OFFSET + 405, LocationType.BONUS), @@ -419,7 +419,7 @@ def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tu lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)), LocationData("A Sinister Turn", "A Sinister Turn: Northeast Base", SC2WOL_LOC_ID_OFFSET + 2304, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)), - LocationData("A Sinister Turn", "A Sinister Turn: Southeast Base", SC2WOL_LOC_ID_OFFSET + 2305, LocationType.MISSION_PROGRESS, + LocationData("A Sinister Turn", "A Sinister Turn: Southwest Base", SC2WOL_LOC_ID_OFFSET + 2305, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)), LocationData("A Sinister Turn", "A Sinister Turn: Maar", SC2WOL_LOC_ID_OFFSET + 2306, LocationType.MISSION_PROGRESS, lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(multiworld, player)), diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py index 13b01c42a22c..e4b6a740669a 100644 --- a/worlds/sc2wol/Options.py +++ b/worlds/sc2wol/Options.py @@ -41,6 +41,10 @@ class FinalMap(Choice): Vanilla mission order always ends with All in mission! + Warning: Using All-in with a short mission order (7 or fewer missions) is not recommended, + as there might not be enough locations to place all the required items, + any excess required items will be placed into the player's starting inventory! + This option is short-lived. It may be changed in the future """ display_name = "Final Map" @@ -265,7 +269,6 @@ class MissionProgressLocations(LocationInclusion): Nothing: No rewards for this type of tasks, effectively disabling such locations Note: Individual locations subject to plando are always enabled, so the plando can be placed properly. - Warning: The generation may fail if too many locations are excluded by this way. See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando) """ display_name = "Mission Progress Locations" @@ -282,7 +285,6 @@ class BonusLocations(LocationInclusion): Nothing: No rewards for this type of tasks, effectively disabling such locations Note: Individual locations subject to plando are always enabled, so the plando can be placed properly. - Warning: The generation may fail if too many locations are excluded by this way. See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando) """ display_name = "Bonus Locations" @@ -300,7 +302,6 @@ class ChallengeLocations(LocationInclusion): Nothing: No rewards for this type of tasks, effectively disabling such locations Note: Individual locations subject to plando are always enabled, so the plando can be placed properly. - Warning: The generation may fail if too many locations are excluded by this way. See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando) """ display_name = "Challenge Locations" @@ -317,7 +318,6 @@ class OptionalBossLocations(LocationInclusion): Nothing: No rewards for this type of tasks, effectively disabling such locations Note: Individual locations subject to plando are always enabled, so the plando can be placed properly. - Warning: The generation may fail if too many locations are excluded by this way. See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando) """ display_name = "Optional Boss Locations" diff --git a/worlds/sc2wol/PoolFilter.py b/worlds/sc2wol/PoolFilter.py index 4a19e2dbb305..23422a3d1ea5 100644 --- a/worlds/sc2wol/PoolFilter.py +++ b/worlds/sc2wol/PoolFilter.py @@ -1,6 +1,7 @@ from typing import Callable, Dict, List, Set from BaseClasses import MultiWorld, ItemClassification, Item, Location -from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, filler_items +from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, filler_items, \ + progressive_if_nco from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\ mission_orders, MissionInfo, alt_final_mission_locations, MissionPools from .Options import get_option_value, MissionOrder, FinalMap, MissionProgressLocations, LocationInclusion @@ -15,7 +16,7 @@ ] BARRACKS_UNITS = {"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre"} -FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine"} +FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine", "Cyclone"} STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven", "Liberator", "Valkyrie"} PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"} @@ -93,7 +94,10 @@ def get_item_upgrades(inventory: List[Item], parent_item: Item or str): ] -def get_item_quantity(item): +def get_item_quantity(item: Item, multiworld: MultiWorld, player: int): + if (not get_option_value(multiworld, player, "nco_items")) \ + and item.name in progressive_if_nco: + return 1 return get_full_item_list()[item.name].quantity @@ -138,13 +142,13 @@ def attempt_removal(item: Item) -> bool: if not all(requirement(self) for requirement in requirements): # If item cannot be removed, lock or revert self.logical_inventory.add(item.name) - for _ in range(get_item_quantity(item)): + for _ in range(get_item_quantity(item, self.multiworld, self.player)): locked_items.append(copy_item(item)) return False return True - + # Limit the maximum number of upgrades - maxUpgrad = get_option_value(self.multiworld, self.player, + maxUpgrad = get_option_value(self.multiworld, self.player, "max_number_of_upgrades") if maxUpgrad != -1: unit_avail_upgrades = {} @@ -197,15 +201,16 @@ def attempt_removal(item: Item) -> bool: # Don't process general upgrades, they may have been pre-locked per-level for item in items_to_lock: if item in inventory: + item_quantity = inventory.count(item) # Unit upgrades, lock all levels - for _ in range(inventory.count(item)): + for _ in range(item_quantity): inventory.remove(item) if item not in locked_items: # Lock all the associated items if not already locked - for _ in range(get_item_quantity(item)): + for _ in range(item_quantity): locked_items.append(copy_item(item)) - if item in existing_items: - existing_items.remove(item) + if item in existing_items: + existing_items.remove(item) if self.min_units_per_structure > 0 and self.has_units_per_structure(): requirements.append(lambda state: state.has_units_per_structure()) @@ -216,7 +221,13 @@ def attempt_removal(item: Item) -> bool: while len(inventory) + len(locked_items) > inventory_size: if len(inventory) == 0: - raise Exception("Reduced item pool generation failed - not enough locations available to place items.") + # There are more items than locations and all of them are already locked due to YAML or logic. + # Random items from locked ones will go to starting items + self.multiworld.random.shuffle(locked_items) + while len(locked_items) > inventory_size: + item: Item = locked_items.pop() + self.multiworld.push_precollected(item) + break # Select random item from removable items item = self.multiworld.random.choice(inventory) # Cascade removals to associated items @@ -245,7 +256,7 @@ def attempt_removal(item: Item) -> bool: for _ in range(inventory.count(transient_item)): inventory.remove(transient_item) if transient_item not in locked_items: - for _ in range(get_item_quantity(transient_item)): + for _ in range(get_item_quantity(transient_item, self.multiworld, self.player)): locked_items.append(copy_item(transient_item)) if transient_item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing): self.logical_inventory.add(transient_item.name) diff --git a/worlds/sc2wol/Starcraft2.kv b/worlds/sc2wol/Starcraft2.kv index 9c52d64c4702..f0785b89e428 100644 --- a/worlds/sc2wol/Starcraft2.kv +++ b/worlds/sc2wol/Starcraft2.kv @@ -11,6 +11,6 @@ markup: True halign: 'center' valign: 'middle' - padding_x: 5 + padding: [5,0,5,0] markup: True outline_width: 1 diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 93aebb7ad15a..5c487f8fee09 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -34,7 +34,7 @@ class SC2WoLWorld(World): game = "Starcraft 2 Wings of Liberty" web = Starcraft2WoLWebWorld() - data_version = 4 + data_version = 5 item_name_to_id = {name: data.code for name, data in get_full_item_list().items()} location_name_to_id = {location.name: location.code for location in get_locations(None, None)} @@ -46,7 +46,7 @@ class SC2WoLWorld(World): mission_req_table = {} final_mission_id: int victory_item: str - required_client_version = 0, 3, 6 + required_client_version = 0, 4, 3 def __init__(self, multiworld: MultiWorld, player: int): super(SC2WoLWorld, self).__init__(multiworld, player) From ea9c31392d822ddda9160d34e65d988c39c7055b Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 8 Nov 2023 18:35:12 -0500 Subject: [PATCH 009/104] Lingo: New game (#1806) Co-authored-by: Aaron Wagener Co-authored-by: Fabian Dill Co-authored-by: Phar --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/lingo/LL1.yaml | 7505 +++++++++++++++++++++++++ worlds/lingo/__init__.py | 112 + worlds/lingo/docs/en_Lingo.md | 42 + worlds/lingo/docs/setup_en.md | 45 + worlds/lingo/ids.yaml | 1449 +++++ worlds/lingo/items.py | 106 + worlds/lingo/locations.py | 80 + worlds/lingo/options.py | 126 + worlds/lingo/player_logic.py | 298 + worlds/lingo/regions.py | 84 + worlds/lingo/rules.py | 104 + worlds/lingo/static_logic.py | 544 ++ worlds/lingo/test/TestDoors.py | 89 + worlds/lingo/test/TestMastery.py | 39 + worlds/lingo/test/TestOptions.py | 31 + worlds/lingo/test/TestOrangeTower.py | 175 + worlds/lingo/test/TestProgressive.py | 191 + worlds/lingo/test/__init__.py | 13 + worlds/lingo/testing.py | 2 + worlds/lingo/utils/assign_ids.rb | 178 + worlds/lingo/utils/validate_config.rb | 329 ++ 23 files changed, 11546 insertions(+) create mode 100644 worlds/lingo/LL1.yaml create mode 100644 worlds/lingo/__init__.py create mode 100644 worlds/lingo/docs/en_Lingo.md create mode 100644 worlds/lingo/docs/setup_en.md create mode 100644 worlds/lingo/ids.yaml create mode 100644 worlds/lingo/items.py create mode 100644 worlds/lingo/locations.py create mode 100644 worlds/lingo/options.py create mode 100644 worlds/lingo/player_logic.py create mode 100644 worlds/lingo/regions.py create mode 100644 worlds/lingo/rules.py create mode 100644 worlds/lingo/static_logic.py create mode 100644 worlds/lingo/test/TestDoors.py create mode 100644 worlds/lingo/test/TestMastery.py create mode 100644 worlds/lingo/test/TestOptions.py create mode 100644 worlds/lingo/test/TestOrangeTower.py create mode 100644 worlds/lingo/test/TestProgressive.py create mode 100644 worlds/lingo/test/__init__.py create mode 100644 worlds/lingo/testing.py create mode 100644 worlds/lingo/utils/assign_ids.rb create mode 100644 worlds/lingo/utils/validate_config.rb diff --git a/README.md b/README.md index 54b659397f1b..bcbc885b4678 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ Currently, the following games are supported: * Muse Dash * DOOM 1993 * Terraria +* Lingo For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index e92bfa42b628..0afc565280f1 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -61,6 +61,9 @@ # Kingdom Hearts 2 /worlds/kh2/ @JaredWeakStrike +# Lingo +/worlds/lingo/ @hatkirby + # Links Awakening DX /worlds/ladx/ @zig-for diff --git a/worlds/lingo/LL1.yaml b/worlds/lingo/LL1.yaml new file mode 100644 index 000000000000..7ae015dc6432 --- /dev/null +++ b/worlds/lingo/LL1.yaml @@ -0,0 +1,7505 @@ +--- + # This file is an associative array where the keys are region names. Rooms + # have four properties: entrances, panels, doors, and paintings. + # + # entrances is an array of regions from which this room can be accessed. The + # key of each entry is the room that can access this one. The value is a list + # of OR'd requirements for being able to access this room from the other one, + # although the list can be elided if there is only one requirement, and the + # value True can be used if there are no requirements (i.e. you always have + # access to this room if you have access to the other). Each requirement + # describes a door that must be opened in order to access this room from the + # other. The door is described by both the door's name and the name of the + # room that the door is in. The room name may be omitted if the door is + # located in the current room. + # + # panels is an array of panels in the room. The key of the array is an + # arbitrary name for the panel. Panels can have the following fields: + # - id: The internal ID of the panel in the LINGO map + # - required_room: In addition to having access to this room, the player must + # also have access to this other room in order to solve this + # panel. + # - required_door: In addition to having access to this room, the player must + # also have this door opened in order to solve this panel. + # - required_panel: In addition to having access to this room, the player must + # also be able to access this other panel in order to solve + # this panel. + # - colors: A list of colors that are required to be unlocked in order + # to solve this panel + # - check: A location check will be created for this individual panel. + # - exclude_reduce: Panel checks are assumed to be INCLUDED when reduce checks + # is on. This option excludes the check anyway. + # - tag: Label that describes how panel randomization should be + # done. In reorder mode, panels with the same tag can be + # shuffled amongst themselves. "forbid" is a special value + # meaning that no randomization should be done. This field is + # mandatory. + # - link: Panels with the same link label are randomized as a group. + # - subtag: Used to identify the separate parts of a linked group. + # - copy_to_sign: When randomizing this panel, the hint should be copied to + # the specified sign(s). + # - achievement: The name of the achievement that is received upon solving + # this panel. + # - non_counting: If True, this panel does not contribute to the total needed + # to unlock Level 2. + # + # doors is an array of doors associated with this room. When door + # randomization is enabled, each of these is an item. The key is a name that + # will be displayed as part of the item's name. Doors can have the following + # fields: + # - id: A string or list of internal door IDs from the LINGO map. + # In door shuffle mode, collecting the item generated for + # this door will open the doors listed here. + # - painting_id: An internal ID of a painting that should be moved upon + # receiving this door. + # - panels: These are the panels that canonically open this door. If + # there is only one panel for the door, then that panel is a + # check. If there is more than one panel, then that entire + # set of panels must be solved for a check. Panels can + # either be a string (representing a panel in this room) or + # a dict containing "room" and "panel". + # - item_name: Overrides the name of the item generated for this door. + # If not specified, the item name will be generated from + # the room name and the door name. + # - location_name: Overrides the name of the location generated for this + # door. If not specified, the location name will be + # generated using the names of the panels. + # - skip_location: If true, no location is generated for this door. + # - skip_item: If true, no item is generated for this door. + # - group: When simple doors is used, all doors with the same group + # will be covered by a single item. + # - include_reduce: Door checks are assumed to be EXCLUDED when reduce checks + # is on. This option includes the check anyway. + # - junk_item: If on, the item for this door will be considered a junk + # item instead of a progression item. Only use this for + # doors that could never gate progression regardless of + # options and state. + # - event: Denotes that the door is event only. This is similar to + # setting both skip_location and skip_item. + # + # paintings is an array of paintings in the room. This is used for painting + # shuffling. + # - id: The internal painting ID from the LINGO map. + # - enter_only: If true, painting shuffling will not place a warp exit on + # this painting. + # - exit_only: If true, painting shuffling will not place a warp entrance + # on this painting. + # - orientation: One of north/south/east/west. This is the direction that + # the player is facing when they are interacting with it, + # not the orientation of the painting itself. "North" is + # the direction the player faces at a new game, with the + # positive X axis to the right. + # - required_door: This door must be open for the painting to be usable as an + # entrance. If required_door is set, enter_only must be + # True. + # - required: Marks a painting as being the only entrance for a room, + # and thus it is required to be an exit when randomized. + # Use "required_when_no_doors" instead if it would be + # possible to enter the room without the painting in door + # shuffle mode. + # - move: Denotes that the painting is able to move. + Starting Room: + entrances: + Menu: True + panels: + HI: + id: Entry Room/Panel_hi_hi + tag: midwhite + HIDDEN: + id: Entry Room/Panel_hidden_hidden + tag: midwhite + TYPE: + id: Entry Room/Panel_type_type + tag: midwhite + THIS: + id: Entry Room/Panel_this_this + tag: midwhite + WRITE: + id: Entry Room/Panel_write_write + tag: midwhite + SAME: + id: Entry Room/Panel_same_same + tag: midwhite + doors: + Main Door: + event: True + panels: + - HI + Back Right Door: + id: Entry Room Area Doors/Door_hidden_hidden + include_reduce: True + panels: + - HIDDEN + Rhyme Room Entrance: + id: + - Palindrome Room Area Doors/Door_level_level_2 + - Palindrome Room Area Doors/Door_racecar_racecar_2 + - Palindrome Room Area Doors/Door_solos_solos_2 + skip_location: True + group: Rhyme Room Doors + panels: + - room: The Tenacious + panel: LEVEL (Black) + - room: The Tenacious + panel: RACECAR (Black) + - room: The Tenacious + panel: SOLOS (Black) + paintings: + - id: arrows_painting + exit_only: True + orientation: south + - id: arrows_painting2 + disable: True + move: True + - id: arrows_painting3 + disable: True + move: True + - id: garden_painting_tower2 + enter_only: True + orientation: north + move: True + required_door: + room: Hedge Maze + door: Painting Shortcut + - id: flower_painting_8 + enter_only: True + orientation: north + move: True + required_door: + room: Courtyard + door: Painting Shortcut + - id: symmetry_painting_a_starter + enter_only: True + orientation: west + move: True + required_door: + room: The Wondrous (Doorknob) + door: Painting Shortcut + - id: pencil_painting6 + enter_only: True + orientation: east + move: True + required_door: + room: Outside The Bold + door: Painting Shortcut + - id: blueman_painting_3 + enter_only: True + orientation: east + move: True + required_door: + room: Outside The Undeterred + door: Painting Shortcut + - id: eyes_yellow_painting2 + enter_only: True + orientation: west + move: True + required_door: + room: Outside The Agreeable + door: Painting Shortcut + Hidden Room: + entrances: + Starting Room: + room: Starting Room + door: Back Right Door + The Seeker: + door: Seeker Entrance + Dead End Area: + door: Dead End Door + Knight Night (Outer Ring): + door: Knight Night Entrance + panels: + DEAD END: + id: Appendix Room/Panel_deadend_deadened + check: True + exclude_reduce: True + tag: topwhite + OPEN: + id: Heteronym Room/Panel_entrance_entrance + tag: midwhite + LIES: + id: Appendix Room/Panel_lies_lies + tag: midwhite + doors: + Dead End Door: + id: Appendix Room Area Doors/Door_rat_tar_2 + skip_location: true + group: Dead End Area Access + panels: + - room: Hub Room + panel: RAT + Knight Night Entrance: + id: Appendix Room Area Doors/Door_rat_tar_4 + skip_location: true + panels: + - room: Hub Room + panel: RAT + Seeker Entrance: + id: Entry Room Area Doors/Door_entrance_entrance + item_name: The Seeker - Entrance + panels: + - OPEN + Rhyme Room Entrance: + id: + - Appendix Room Area Doors/Door_rat_tar_3 + - Double Room Area Doors/Door_room_entry_stairs + skip_location: True + group: Rhyme Room Doors + panels: + - room: The Tenacious + panel: LEVEL (Black) + - room: The Tenacious + panel: RACECAR (Black) + - room: The Tenacious + panel: SOLOS (Black) + - room: Hub Room + panel: RAT + paintings: + - id: owl_painting + orientation: north + The Seeker: + entrances: + Hidden Room: + room: Hidden Room + door: Seeker Entrance + Pilgrim Room: + room: Pilgrim Room + door: Shortcut to The Seeker + panels: + Achievement: + id: Countdown Panels/Panel_seeker_seeker + required_room: Hidden Room + tag: forbid + check: True + achievement: The Seeker + BEAR: + id: Heteronym Room/Panel_bear_bear + tag: midwhite + MINE: + id: Heteronym Room/Panel_mine_mine + tag: double midwhite + subtag: left + link: exact MINE + MINE (2): + id: Heteronym Room/Panel_mine_mine_2 + tag: double midwhite + subtag: right + link: exact MINE + BOW: + id: Heteronym Room/Panel_bow_bow + tag: midwhite + DOES: + id: Heteronym Room/Panel_does_does + tag: midwhite + MOBILE: + id: Heteronym Room/Panel_mobile_mobile + tag: double midwhite + subtag: left + link: exact MOBILE + MOBILE (2): + id: Heteronym Room/Panel_mobile_mobile_2 + tag: double midwhite + subtag: right + link: exact MOBILE + DESERT: + id: Heteronym Room/Panel_desert_desert + tag: topmid white stack + subtag: mid + link: topmid DESERT + DESSERT: + id: Heteronym Room/Panel_desert_dessert + tag: topmid white stack + subtag: top + link: topmid DESERT + SOW: + id: Heteronym Room/Panel_sow_sow + tag: topmid white stack + subtag: mid + link: topmid SOW + SEW: + id: Heteronym Room/Panel_sow_so + tag: topmid white stack + subtag: top + link: topmid SOW + TO: + id: Heteronym Room/Panel_two_to + tag: double topwhite + subtag: left + link: hp TWO + TOO: + id: Heteronym Room/Panel_two_too + tag: double topwhite + subtag: right + link: hp TWO + WRITE: + id: Heteronym Room/Panel_write_right + tag: topwhite + EWE: + id: Heteronym Room/Panel_you_ewe + tag: topwhite + KNOT: + id: Heteronym Room/Panel_not_knot + tag: double topwhite + subtag: left + link: hp NOT + NAUGHT: + id: Heteronym Room/Panel_not_naught + tag: double topwhite + subtag: right + link: hp NOT + BEAR (2): + id: Heteronym Room/Panel_bear_bare + tag: topwhite + Second Room: + entrances: + Starting Room: + room: Starting Room + door: Main Door + Hub Room: + door: Exit Door + panels: + HI: + id: Entry Room/Panel_hi_high + tag: topwhite + LOW: + id: Entry Room/Panel_low_low + tag: forbid # This is a midwhite pretending to be a botwhite + ANOTHER TRY: + id: Entry Room/Panel_advance + tag: topwhite + LEVEL 2: + # We will set up special rules for this in code. + id: EndPanel/Panel_level_2 + tag: forbid + non_counting: True + check: True + required_panel: + - panel: ANOTHER TRY + doors: + Exit Door: + id: Entry Room Area Doors/Door_hi_high + location_name: Second Room - Good Luck + include_reduce: True + panels: + - HI + - LOW + Hub Room: + entrances: + Second Room: + room: Second Room + door: Exit Door + Dead End Area: + door: Near RAT Door + Crossroads: + door: Crossroads Entrance + The Tenacious: + door: Tenacious Entrance + Warts Straw Area: + door: Symmetry Door + Hedge Maze: + door: Shortcut to Hedge Maze + Orange Tower First Floor: + room: Orange Tower First Floor + door: Shortcut to Hub Room + Owl Hallway: + painting: True + Outside The Initiated: + room: Outside The Initiated + door: Shortcut to Hub Room + The Traveled: + door: Traveled Entrance + Roof: True # through the sunwarp + Outside The Undeterred: # (NOTE: used in hardcoded pilgrimage) + room: Outside The Undeterred + door: Green Painting + painting: True + panels: + ORDER: + id: Shuffle Room/Panel_order_chaos + colors: black + tag: botblack + SLAUGHTER: + id: Palindrome Room/Panel_slaughter_laughter + colors: red + tag: midred + NEAR: + id: Symmetry Room/Panel_near_far + colors: black + tag: botblack + FAR: + id: Symmetry Room/Panel_far_near + colors: black + tag: botblack + TRACE: + id: Maze Room/Panel_trace_trace + tag: midwhite + RAT: + id: Appendix Room/Panel_rat_tar + colors: black + check: True + exclude_reduce: True + tag: midblack + OPEN: + id: Synonym Room/Panel_open_open + tag: midwhite + FOUR: + id: Backside Room/Panel_four_four_3 + tag: midwhite + required_door: + room: Outside The Undeterred + door: Fours + LOST: + id: Shuffle Room/Panel_lost_found + colors: black + tag: botblack + FORWARD: + id: Entry Room/Panel_forward_forward + tag: midwhite + BETWEEN: + id: Entry Room/Panel_between_between + tag: midwhite + BACKWARD: + id: Entry Room/Panel_backward_backward + tag: midwhite + doors: + Crossroads Entrance: + id: Shuffle Room Area Doors/Door_chaos + panels: + - ORDER + Tenacious Entrance: + id: Palindrome Room Area Doors/Door_slaughter_laughter + group: Entrances to The Tenacious + panels: + - SLAUGHTER + Symmetry Door: + id: + - Symmetry Room Area Doors/Door_near_far + - Symmetry Room Area Doors/Door_far_near + group: Symmetry Doors + panels: + - NEAR + - FAR + Shortcut to Hedge Maze: + id: Maze Area Doors/Door_trace_trace + group: Hedge Maze Doors + panels: + - TRACE + Near RAT Door: + id: Appendix Room Area Doors/Door_deadend_deadened + skip_location: True + group: Dead End Area Access + panels: + - room: Hidden Room + panel: DEAD END + Traveled Entrance: + id: Appendix Room Area Doors/Door_open_open + item_name: The Traveled - Entrance + group: Entrance to The Traveled + panels: + - OPEN + Lost Door: + id: Shuffle Room Area Doors/Door_lost_found + junk_item: True + panels: + - LOST + paintings: + - id: maze_painting + orientation: west + Dead End Area: + entrances: + Hidden Room: + room: Hidden Room + door: Dead End Door + Hub Room: + room: Hub Room + door: Near RAT Door + panels: + FOUR: + id: Backside Room/Panel_four_four_2 + tag: midwhite + required_door: + room: Outside The Undeterred + door: Fours + EIGHT: + id: Backside Room/Panel_eight_eight_8 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + paintings: + - id: smile_painting_6 + orientation: north + Pilgrim Antechamber: + # Let's not shuffle the paintings yet. + entrances: + # The pilgrimage is hardcoded in rules.py + Starting Room: + door: Sun Painting + panels: + HOT CRUST: + id: Lingo Room/Panel_shortcut + colors: yellow + tag: midyellow + PILGRIMAGE: + id: Lingo Room/Panel_pilgrim + colors: blue + tag: midblue + MASTERY: + id: Master Room/Panel_mastery_mastery14 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + doors: + Sun Painting: + item_name: Pilgrim Room - Sun Painting + location_name: Pilgrim Room - HOT CRUST + painting_id: pilgrim_painting2 + panels: + - HOT CRUST + Exit: + event: True + panels: + - PILGRIMAGE + Pilgrim Room: + entrances: + The Seeker: + door: Shortcut to The Seeker + Pilgrim Antechamber: + room: Pilgrim Antechamber + door: Exit + panels: + THIS: + id: Lingo Room/Panel_lingo_9 + colors: gray + tag: forbid + TIME ROOM: + id: Lingo Room/Panel_lingo_1 + colors: purple + tag: toppurp + SCIENCE ROOM: + id: Lingo Room/Panel_lingo_2 + tag: botwhite + SHINY ROCK ROOM: + id: Lingo Room/Panel_lingo_3 + tag: botwhite + ANGRY POWER: + id: Lingo Room/Panel_lingo_4 + colors: + - purple + tag: forbid + MICRO LEGION: + id: Lingo Room/Panel_lingo_5 + colors: yellow + tag: midyellow + LOSERS RELAX: + id: Lingo Room/Panel_lingo_6 + colors: + - black + tag: forbid + "906234": + id: Lingo Room/Panel_lingo_7 + colors: + - orange + - blue + tag: forbid + MOOR EMORDNILAP: + id: Lingo Room/Panel_lingo_8 + colors: black + tag: midblack + HALL ROOMMATE: + id: Lingo Room/Panel_lingo_10 + colors: + - red + - blue + tag: forbid + ALL GREY: + id: Lingo Room/Panel_lingo_11 + colors: yellow + tag: midyellow + PLUNDER ISLAND: + id: Lingo Room/Panel_lingo_12 + colors: + - purple + - red + tag: forbid + FLOSS PATHS: + id: Lingo Room/Panel_lingo_13 + colors: + - purple + - brown + tag: forbid + doors: + Shortcut to The Seeker: + id: Master Room Doors/Door_pilgrim_shortcut + include_reduce: True + panels: + - THIS + Crossroads: + entrances: + Hub Room: True # The sunwarp means that we never need the ORDER door + Color Hallways: True + The Tenacious: + door: Tenacious Entrance + Orange Tower Fourth Floor: True # through IRK HORN + Amen Name Area: + room: Lost Area + door: Exit + Roof: True # through the sunwarp + panels: + DECAY: + id: Palindrome Room/Panel_decay_day + colors: red + tag: midred + NOPE: + id: Sun Room/Panel_nope_open + colors: yellow + tag: midyellow + EIGHT: + id: Backside Room/Panel_eight_eight_5 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + WE ROT: + id: Shuffle Room/Panel_tower + colors: yellow + tag: midyellow + WORDS: + id: Shuffle Room/Panel_words_sword + colors: yellow + tag: midyellow + SWORD: + id: Shuffle Room/Panel_sword_words + colors: yellow + tag: midyellow + TURN: + id: Shuffle Room/Panel_turn_runt + colors: yellow + tag: midyellow + BEND HI: + id: Shuffle Room/Panel_behind + colors: yellow + tag: midyellow + THE EYES: + id: Shuffle Room/Panel_eyes_see_shuffle + colors: yellow + check: True + exclude_reduce: True + required_door: + door: Hollow Hallway + tag: midyellow + CORNER: + id: Shuffle Room/Panel_corner_corner + required_door: + door: Hollow Hallway + tag: midwhite + HOLLOW: + id: Shuffle Room/Panel_hollow_hollow + required_door: + door: Hollow Hallway + tag: midwhite + SWAP: + id: Shuffle Room/Panel_swap_wasp + colors: yellow + tag: midyellow + GEL: + id: Shuffle Room/Panel_gel + colors: yellow + tag: topyellow + required_door: + door: Tower Entrance + THOUGH: + id: Shuffle Room/Panel_though + colors: yellow + tag: topyellow + required_door: + door: Tower Entrance + CROSSROADS: + id: Shuffle Room/Panel_crossroads_crossroads + tag: midwhite + doors: + Tenacious Entrance: + id: Palindrome Room Area Doors/Door_decay_day + group: Entrances to The Tenacious + panels: + - DECAY + Discerning Entrance: + id: Shuffle Room Area Doors/Door_nope_open + item_name: The Discerning - Entrance + panels: + - NOPE + Tower Entrance: + id: + - Shuffle Room Area Doors/Door_tower + - Shuffle Room Area Doors/Door_tower2 + - Shuffle Room Area Doors/Door_tower3 + - Shuffle Room Area Doors/Door_tower4 + group: Crossroads - Tower Entrances + panels: + - WE ROT + Tower Back Entrance: + id: Shuffle Room Area Doors/Door_runt + location_name: Crossroads - TURN/RUNT + group: Crossroads - Tower Entrances + panels: + - TURN + - room: Orange Tower Fourth Floor + panel: RUNT + Words Sword Door: + id: + - Shuffle Room Area Doors/Door_words_shuffle_3 + - Shuffle Room Area Doors/Door_words_shuffle_4 + group: Crossroads Doors + panels: + - WORDS + - SWORD + Eye Wall: + id: Shuffle Room Area Doors/Door_behind + junk_item: True + group: Crossroads Doors + panels: + - BEND HI + Hollow Hallway: + id: Shuffle Room Area Doors/Door_crossroads6 + skip_location: True + group: Crossroads Doors + panels: + - BEND HI + Roof Access: + id: Tower Room Area Doors/Door_level_6_2 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + - room: Orange Tower Fourth Floor + panel: LEARNS + UNSEW + - room: Orange Tower Fifth Floor + panel: DRAWL + RUNS + - room: Owl Hallway + panel: READS + RUST + paintings: + - id: eye_painting + disable: True + orientation: east + move: True + required_door: + door: Eye Wall + - id: smile_painting_4 + orientation: south + Lost Area: + entrances: + Outside The Agreeable: + door: Exit + Crossroads: + room: Crossroads + door: Words Sword Door + panels: + LOST (1): + id: Shuffle Room/Panel_lost_lots + colors: yellow + tag: midyellow + LOST (2): + id: Shuffle Room/Panel_lost_slot + colors: yellow + tag: midyellow + doors: + Exit: + id: + - Shuffle Room Area Doors/Door_lost_shuffle_1 + - Shuffle Room Area Doors/Door_lost_shuffle_2 + location_name: Crossroads - LOST Pair + panels: + - LOST (1) + - LOST (2) + Amen Name Area: + entrances: + Crossroads: + room: Lost Area + door: Exit + Suits Area: + door: Exit + panels: + AMEN: + id: Shuffle Room/Panel_amen_mean + colors: yellow + tag: double midyellow + subtag: left + link: ana MEAN + NAME: + id: Shuffle Room/Panel_name_mean + colors: yellow + tag: double midyellow + subtag: right + link: ana MEAN + NINE: + id: Backside Room/Panel_nine_nine_3 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + doors: + Exit: + id: Shuffle Room Area Doors/Door_mean + panels: + - AMEN + - NAME + Suits Area: + entrances: + Amen Name Area: + room: Amen Name Area + door: Exit + Roof: True + panels: + SPADES: + id: Cross Room/Panel_spades_spades + tag: midwhite + CLUBS: + id: Cross Room/Panel_clubs_clubs + tag: midwhite + HEARTS: + id: Cross Room/Panel_hearts_hearts + tag: midwhite + paintings: + - id: west_afar + orientation: south + The Tenacious: + entrances: + Hub Room: + - room: Hub Room + door: Tenacious Entrance + - door: Shortcut to Hub Room + Crossroads: + room: Crossroads + door: Tenacious Entrance + Outside The Agreeable: + room: Outside The Agreeable + door: Tenacious Entrance + Dread Hallway: + room: Dread Hallway + door: Tenacious Entrance + panels: + LEVEL (Black): + id: Palindrome Room/Panel_level_level + colors: black + tag: midblack + RACECAR (Black): + id: Palindrome Room/Panel_racecar_racecar + colors: black + tag: palindrome + copy_to_sign: sign4 + SOLOS (Black): + id: Palindrome Room/Panel_solos_solos + colors: black + tag: palindrome + copy_to_sign: + - sign5 + - sign6 + LEVEL (White): + id: Palindrome Room/Panel_level_level_2 + tag: midwhite + RACECAR (White): + id: Palindrome Room/Panel_racecar_racecar_2 + tag: midwhite + copy_to_sign: sign3 + SOLOS (White): + id: Palindrome Room/Panel_solos_solos_2 + tag: midwhite + copy_to_sign: + - sign1 + - sign2 + Achievement: + id: Countdown Panels/Panel_tenacious_tenacious + check: True + tag: forbid + required_panel: + - panel: LEVEL (Black) + - panel: RACECAR (Black) + - panel: SOLOS (Black) + - panel: LEVEL (White) + - panel: RACECAR (White) + - panel: SOLOS (White) + - room: Hub Room + panel: SLAUGHTER + - room: Crossroads + panel: DECAY + - room: Outside The Agreeable + panel: MASSACRED + - room: Dread Hallway + panel: DREAD + achievement: The Tenacious + doors: + Shortcut to Hub Room: + id: + - Palindrome Room Area Doors/Door_level_level_1 + - Palindrome Room Area Doors/Door_racecar_racecar_1 + - Palindrome Room Area Doors/Door_solos_solos_1 + location_name: The Tenacious - Palindromes + group: Entrances to The Tenacious + panels: + - LEVEL (Black) + - RACECAR (Black) + - SOLOS (Black) + White Palindromes: + location_name: The Tenacious - White Palindromes + skip_item: True + panels: + - LEVEL (White) + - RACECAR (White) + - SOLOS (White) + Warts Straw Area: + entrances: + Hub Room: + room: Hub Room + door: Symmetry Door + Leaf Feel Area: + door: Door + panels: + WARTS: + id: Symmetry Room/Panel_warts_straw + colors: black + tag: midblack + STRAW: + id: Symmetry Room/Panel_straw_warts + colors: black + tag: midblack + doors: + Door: + id: + - Symmetry Room Area Doors/Door_warts_straw + - Symmetry Room Area Doors/Door_straw_warts + group: Symmetry Doors + panels: + - WARTS + - STRAW + Leaf Feel Area: + entrances: + Warts Straw Area: + room: Warts Straw Area + door: Door + Outside The Agreeable: + door: Door + panels: + LEAF: + id: Symmetry Room/Panel_leaf_feel + colors: black + tag: topblack + FEEL: + id: Symmetry Room/Panel_feel_leaf + colors: black + tag: topblack + doors: + Door: + id: + - Symmetry Room Area Doors/Door_leaf_feel + - Symmetry Room Area Doors/Door_feel_leaf + group: Symmetry Doors + panels: + - LEAF + - FEEL + Outside The Agreeable: + # Let's ignore the blue warp thing for now because the lookout is a dead + # end. Later on it could be filler checks. + entrances: + # We don't have to list Lost Area because of Crossroads. + Crossroads: True + The Tenacious: + door: Tenacious Entrance + The Agreeable: + door: Agreeable Entrance + Dread Hallway: + door: Black Door + Leaf Feel Area: + room: Leaf Feel Area + door: Door + Starting Room: + door: Painting Shortcut + painting: True + Hallway Room (2): True + Hallway Room (3): True + Hallway Room (4): True + Hedge Maze: True # through the door to the sectioned-off part of the hedge maze + panels: + MASSACRED: + id: Palindrome Room/Panel_massacred_sacred + colors: red + tag: midred + BLACK: + id: Symmetry Room/Panel_black_white + colors: black + tag: botblack + CLOSE: + id: Antonym Room/Panel_close_open + colors: black + tag: botblack + LEFT: + id: Symmetry Room/Panel_left_right + colors: black + tag: botblack + LEFT (2): + id: Symmetry Room/Panel_left_wrong + colors: black + tag: bot black black + RIGHT: + id: Symmetry Room/Panel_right_left + colors: black + tag: botblack + PURPLE: + id: Color Arrow Room/Panel_purple_afar + tag: midwhite + required_door: + door: Purple Barrier + FIVE (1): + id: Backside Room/Panel_five_five_5 + tag: midwhite + required_door: + room: Outside The Undeterred + door: Fives + FIVE (2): + id: Backside Room/Panel_five_five_4 + tag: midwhite + required_door: + room: Outside The Undeterred + door: Fives + OUT: + id: Hallway Room/Panel_out_out + check: True + exclude_reduce: True + tag: midwhite + HIDE: + id: Maze Room/Panel_hide_seek_4 + colors: black + tag: botblack + DAZE: + id: Maze Room/Panel_daze_maze + colors: purple + tag: midpurp + WALL: + id: Hallway Room/Panel_castle_1 + colors: blue + tag: quad bot blue + link: qbb CASTLE + KEEP: + id: Hallway Room/Panel_castle_2 + colors: blue + tag: quad bot blue + link: qbb CASTLE + BAILEY: + id: Hallway Room/Panel_castle_3 + colors: blue + tag: quad bot blue + link: qbb CASTLE + TOWER: + id: Hallway Room/Panel_castle_4 + colors: blue + tag: quad bot blue + link: qbb CASTLE + NORTH: + id: Cross Room/Panel_north_missing + colors: green + tag: forbid + required_room: Outside The Bold + DIAMONDS: + id: Cross Room/Panel_diamonds_missing + colors: green + tag: forbid + required_room: Suits Area + FIRE: + id: Cross Room/Panel_fire_missing + colors: green + tag: forbid + required_room: Elements Area + WINTER: + id: Cross Room/Panel_winter_missing + colors: green + tag: forbid + required_room: Orange Tower Fifth Floor + doors: + Tenacious Entrance: + id: Palindrome Room Area Doors/Door_massacred_sacred + group: Entrances to The Tenacious + panels: + - MASSACRED + Black Door: + id: Symmetry Room Area Doors/Door_black_white + group: Entrances to The Tenacious + panels: + - BLACK + Agreeable Entrance: + id: Symmetry Room Area Doors/Door_close_open + item_name: The Agreeable - Entrance + panels: + - CLOSE + Painting Shortcut: + item_name: Starting Room - Street Painting + painting_id: eyes_yellow_painting2 + panels: + - RIGHT + Purple Barrier: + id: Color Arrow Room Doors/Door_purple_3 + group: Color Hunt Barriers + skip_location: True + panels: + - room: Champion's Rest + panel: PURPLE + Hallway Door: + id: Red Blue Purple Room Area Doors/Door_room_2 + group: Hallway Room Doors + location_name: Hallway Room - First Room + panels: + - WALL + - KEEP + - BAILEY + - TOWER + paintings: + - id: panda_painting + orientation: south + - id: eyes_yellow_painting + orientation: east + progression: + Progressive Hallway Room: + - Hallway Door + - room: Hallway Room (2) + door: Exit + - room: Hallway Room (3) + door: Exit + - room: Hallway Room (4) + door: Exit + Dread Hallway: + entrances: + Outside The Agreeable: + room: Outside The Agreeable + door: Black Door + The Tenacious: + door: Tenacious Entrance + panels: + DREAD: + id: Palindrome Room/Panel_dread_dead + colors: red + tag: midred + doors: + Tenacious Entrance: + id: Palindrome Room Area Doors/Door_dread_dead + group: Entrances to The Tenacious + panels: + - DREAD + The Agreeable: + entrances: + Outside The Agreeable: + room: Outside The Agreeable + door: Agreeable Entrance + Hedge Maze: + door: Shortcut to Hedge Maze + panels: + Achievement: + id: Countdown Panels/Panel_disagreeable_agreeable + colors: black + tag: forbid + required_room: Outside The Agreeable + check: True + achievement: The Agreeable + BYE: + id: Antonym Room/Panel_bye_hi + colors: black + tag: botblack + RETOOL: + id: Antonym Room/Panel_retool_looter + colors: black + tag: midblack + DRAWER: + id: Antonym Room/Panel_drawer_reward + colors: black + tag: midblack + READ: + id: Antonym Room/Panel_read_write + colors: black + tag: botblack + DIFFERENT: + id: Antonym Room/Panel_different_same + colors: black + tag: botblack + LOW: + id: Antonym Room/Panel_low_high + colors: black + tag: botblack + ALIVE: + id: Antonym Room/Panel_alive_dead + colors: black + tag: botblack + THAT: + id: Antonym Room/Panel_that_this + colors: black + tag: botblack + STRESSED: + id: Antonym Room/Panel_stressed_desserts + colors: black + tag: midblack + STAR: + id: Antonym Room/Panel_star_rats + colors: black + tag: midblack + TAME: + id: Antonym Room/Panel_tame_mate + colors: black + tag: topblack + CAT: + id: Antonym Room/Panel_cat_tack + colors: black + tag: topblack + doors: + Shortcut to Hedge Maze: + id: Symmetry Room Area Doors/Door_bye_hi + group: Hedge Maze Doors + panels: + - BYE + Hedge Maze: + entrances: + Hub Room: + room: Hub Room + door: Shortcut to Hedge Maze + Color Hallways: True + The Agreeable: + room: The Agreeable + door: Shortcut to Hedge Maze + The Perceptive: True + The Observant: + door: Observant Entrance + Owl Hallway: + room: Owl Hallway + door: Shortcut to Hedge Maze + Roof: True + panels: + DOWN: + id: Maze Room/Panel_down_up + colors: black + tag: botblack + HIDE (1): + id: Maze Room/Panel_hide_seek + colors: black + tag: botblack + HIDE (2): + id: Maze Room/Panel_hide_seek_2 + colors: black + tag: botblack + HIDE (3): + id: Maze Room/Panel_hide_seek_3 + colors: black + tag: botblack + MASTERY (1): + id: Master Room/Panel_mastery_mastery5 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + MASTERY (2): + id: Master Room/Panel_mastery_mastery9 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + PATH (1): + id: Maze Room/Panel_path_lock + colors: green + tag: forbid + PATH (2): + id: Maze Room/Panel_path_knot + colors: green + tag: forbid + PATH (3): + id: Maze Room/Panel_path_lost + colors: green + tag: forbid + PATH (4): + id: Maze Room/Panel_path_open + colors: green + tag: forbid + PATH (5): + id: Maze Room/Panel_path_help + colors: green + tag: forbid + PATH (6): + id: Maze Room/Panel_path_hunt + colors: green + tag: forbid + PATH (7): + id: Maze Room/Panel_path_nest + colors: green + tag: forbid + PATH (8): + id: Maze Room/Panel_path_look + colors: green + tag: forbid + REFLOW: + id: Maze Room/Panel_reflow_flower + colors: yellow + tag: midyellow + LEAP: + id: Maze Room/Panel_leap_jump + tag: botwhite + doors: + Perceptive Entrance: + id: Maze Area Doors/Door_maze_maze + item_name: The Perceptive - Entrance + group: Hedge Maze Doors + panels: + - DOWN + Painting Shortcut: + painting_id: garden_painting_tower2 + item_name: Starting Room - Hedge Maze Painting + skip_location: True + panels: + - DOWN + Observant Entrance: + id: + - Maze Area Doors/Door_look_room_1 + - Maze Area Doors/Door_look_room_2 + - Maze Area Doors/Door_look_room_3 + skip_location: True + item_name: The Observant - Entrance + group: Observant Doors + panels: + - room: The Perceptive + panel: GAZE + Hide and Seek: + skip_item: True + location_name: Hedge Maze - Hide and Seek + include_reduce: True + panels: + - HIDE (1) + - HIDE (2) + - HIDE (3) + - room: Outside The Agreeable + panel: HIDE + The Perceptive: + entrances: + Starting Room: + room: Hedge Maze + door: Painting Shortcut + painting: True + Hedge Maze: + room: Hedge Maze + door: Perceptive Entrance + panels: + Achievement: + id: Countdown Panels/Panel_perceptive_perceptive + colors: green + tag: forbid + check: True + achievement: The Perceptive + GAZE: + id: Maze Room/Panel_look_look + check: True + exclude_reduce: True + tag: botwhite + paintings: + - id: garden_painting_tower + orientation: north + The Fearless (First Floor): + entrances: + The Perceptive: True + panels: + NAPS: + id: Naps Room/Panel_naps_span + colors: black + tag: midblack + TEAM: + id: Naps Room/Panel_team_meet + colors: black + tag: topblack + TEEM: + id: Naps Room/Panel_teem_meat + colors: black + tag: topblack + IMPATIENT: + id: Naps Room/Panel_impatient_doctor + colors: black + tag: bot black black + EAT: + id: Naps Room/Panel_eat_tea + colors: black + tag: topblack + doors: + Second Floor: + id: Naps Room Doors/Door_hider_5 + location_name: The Fearless - First Floor Puzzles + group: Fearless Doors + panels: + - NAPS + - TEAM + - TEEM + - IMPATIENT + - EAT + progression: + Progressive Fearless: + - Second Floor + - room: The Fearless (Second Floor) + door: Third Floor + The Fearless (Second Floor): + entrances: + The Fearless (First Floor): + room: The Fearless (First Floor) + door: Second Floor + panels: + NONE: + id: Naps Room/Panel_one_many + colors: black + tag: bot black top white + SUM: + id: Naps Room/Panel_one_none + colors: black + tag: top white bot black + FUNNY: + id: Naps Room/Panel_funny_enough + colors: black + tag: topblack + MIGHT: + id: Naps Room/Panel_might_time + colors: black + tag: topblack + SAFE: + id: Naps Room/Panel_safe_face + colors: black + tag: topblack + SAME: + id: Naps Room/Panel_same_mace + colors: black + tag: topblack + CAME: + id: Naps Room/Panel_came_make + colors: black + tag: topblack + doors: + Third Floor: + id: + - Naps Room Doors/Door_hider_1b2 + - Naps Room Doors/Door_hider_new1 + location_name: The Fearless - Second Floor Puzzles + group: Fearless Doors + panels: + - NONE + - SUM + - FUNNY + - MIGHT + - SAFE + - SAME + - CAME + The Fearless: + entrances: + The Fearless (First Floor): + room: The Fearless (Second Floor) + door: Third Floor + panels: + Achievement: + id: Countdown Panels/Panel_fearless_fearless + colors: black + tag: forbid + check: True + achievement: The Fearless + EASY: + id: Naps Room/Panel_easy_soft + colors: black + tag: bot black black + SOMETIMES: + id: Naps Room/Panel_sometimes_always + colors: black + tag: bot black black + DARK: + id: Naps Room/Panel_dark_extinguish + colors: black + tag: bot black black + EVEN: + id: Naps Room/Panel_even_ordinary + colors: black + tag: bot black black + The Observant: + entrances: + Hedge Maze: + room: Hedge Maze + door: Observant Entrance + The Incomparable: True + panels: + Achievement: + id: Countdown Panels/Panel_observant_observant + colors: green + check: True + tag: forbid + required_door: + door: Stairs + achievement: The Observant + BACK: + id: Look Room/Panel_four_back + colors: green + tag: forbid + SIDE: + id: Look Room/Panel_four_side + colors: green + tag: forbid + BACKSIDE: + id: Backside Room/Panel_backside_2 + tag: midwhite + required_door: + door: Backside Door + STAIRS: + id: Look Room/Panel_six_stairs + colors: green + tag: forbid + WAYS: + id: Look Room/Panel_four_ways + colors: green + tag: forbid + "ON": + id: Look Room/Panel_two_on + colors: green + tag: forbid + UP: + id: Look Room/Panel_two_up + colors: green + tag: forbid + SWIMS: + id: Look Room/Panel_five_swims + colors: green + tag: forbid + UPSTAIRS: + id: Look Room/Panel_eight_upstairs + colors: green + tag: forbid + required_door: + door: Stairs + TOIL: + id: Look Room/Panel_blue_toil + colors: green + tag: forbid + required_door: + door: Stairs + STOP: + id: Look Room/Panel_four_stop + colors: green + tag: forbid + required_door: + door: Stairs + TOP: + id: Look Room/Panel_aqua_top + colors: green + tag: forbid + required_door: + door: Stairs + HI: + id: Look Room/Panel_blue_hi + colors: green + tag: forbid + required_door: + door: Stairs + HI (2): + id: Look Room/Panel_blue_hi2 + colors: green + tag: forbid + required_door: + door: Stairs + "31": + id: Look Room/Panel_numbers_31 + colors: green + tag: forbid + required_door: + door: Stairs + "52": + id: Look Room/Panel_numbers_52 + colors: green + tag: forbid + required_door: + door: Stairs + OIL: + id: Look Room/Panel_aqua_oil + colors: green + tag: forbid + required_door: + door: Stairs + BACKSIDE (GREEN): + id: Look Room/Panel_eight_backside + colors: green + tag: forbid + required_door: + door: Stairs + SIDEWAYS: + id: Look Room/Panel_eight_sideways + colors: green + tag: forbid + required_door: + door: Stairs + doors: + Backside Door: + id: Maze Area Doors/Door_backside + group: Backside Doors + panels: + - BACK + - SIDE + Stairs: + id: Maze Area Doors/Door_stairs + group: Observant Doors + panels: + - STAIRS + The Incomparable: + entrances: + The Observant: True # Assuming that access to The Observant includes access to the right entrance + Eight Room: True + Eight Alcove: + door: Eight Painting + panels: + Achievement: + id: Countdown Panels/Panel_incomparable_incomparable + colors: blue + check: True + tag: forbid + required_room: + - Elements Area + - Courtyard + - Eight Room + achievement: The Incomparable + A (One): + id: Strand Room/Panel_blank_a + colors: blue + tag: forbid + A (Two): + id: Strand Room/Panel_a_an + colors: blue + tag: forbid + A (Three): + id: Strand Room/Panel_a_and + colors: blue + tag: forbid + A (Four): + id: Strand Room/Panel_a_sand + colors: blue + tag: forbid + A (Five): + id: Strand Room/Panel_a_stand + colors: blue + tag: forbid + A (Six): + id: Strand Room/Panel_a_strand + colors: blue + tag: forbid + I (One): + id: Strand Room/Panel_blank_i + colors: blue + tag: forbid + I (Two): + id: Strand Room/Panel_i_in + colors: blue + tag: forbid + I (Three): + id: Strand Room/Panel_i_sin + colors: blue + tag: forbid + I (Four): + id: Strand Room/Panel_i_sing + colors: blue + tag: forbid + I (Five): + id: Strand Room/Panel_i_sting + colors: blue + tag: forbid + I (Six): + id: Strand Room/Panel_i_string + colors: blue + tag: forbid + I (Seven): + id: Strand Room/Panel_i_strings + colors: blue + tag: forbid + doors: + Eight Painting: + id: Red Blue Purple Room Area Doors/Door_a_strands + location_name: Giant Sevens + group: Observant Doors + panels: + - I (Seven) + - room: Courtyard + panel: I + - room: Elements Area + panel: A + Eight Alcove: + entrances: + The Incomparable: + room: The Incomparable + door: Eight Painting + paintings: + - id: eight_painting2 + orientation: north + Eight Room: + entrances: + Eight Alcove: + painting: True + panels: + Eight Back: + id: Strand Room/Panel_i_starling + colors: blue + tag: forbid + Eight Front: + id: Strand Room/Panel_i_starting + colors: blue + tag: forbid + Nine: + id: Strand Room/Panel_i_startling + colors: blue + tag: forbid + paintings: + - id: eight_painting + orientation: south + exit_only: True + required: True + Orange Tower: + # This is a special, meta-ish room. + entrances: + Menu: True + doors: + Second Floor: + id: Tower Room Area Doors/Door_level_1 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + Third Floor: + id: Tower Room Area Doors/Door_level_2 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + Fourth Floor: + id: Tower Room Area Doors/Door_level_3 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + Fifth Floor: + id: Tower Room Area Doors/Door_level_4 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + - room: Orange Tower Fourth Floor + panel: LEARNS + UNSEW + Sixth Floor: + id: Tower Room Area Doors/Door_level_5 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + - room: Orange Tower Fourth Floor + panel: LEARNS + UNSEW + - room: Orange Tower Fifth Floor + panel: DRAWL + RUNS + Seventh Floor: + id: Tower Room Area Doors/Door_level_6 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + - room: Orange Tower Fourth Floor + panel: LEARNS + UNSEW + - room: Orange Tower Fifth Floor + panel: DRAWL + RUNS + - room: Owl Hallway + panel: READS + RUST + progression: + Progressive Orange Tower: + - Second Floor + - Third Floor + - Fourth Floor + - Fifth Floor + - Sixth Floor + - Seventh Floor + Orange Tower First Floor: + entrances: + Hub Room: + door: Shortcut to Hub Room + Outside The Wanderer: + room: Outside The Wanderer + door: Tower Entrance + Orange Tower Second Floor: + room: Orange Tower + door: Second Floor + Directional Gallery: + door: Salt Pepper Door + Roof: True # through the sunwarp + panels: + SECRET: + id: Shuffle Room/Panel_secret_secret + tag: midwhite + DADS + ALE: + id: Tower Room/Panel_dads_ale_dead_1 + colors: orange + check: True + tag: midorange + SALT: + id: Backside Room/Panel_salt_pepper + colors: black + tag: botblack + doors: + Shortcut to Hub Room: + id: Shuffle Room Area Doors/Door_secret_secret + group: Orange Tower First Floor - Shortcuts + panels: + - SECRET + Salt Pepper Door: + id: Count Up Room Area Doors/Door_salt_pepper + location_name: Orange Tower First Floor - Salt Pepper Door + group: Orange Tower First Floor - Shortcuts + panels: + - SALT + - room: Directional Gallery + panel: PEPPER + Orange Tower Second Floor: + entrances: + Orange Tower First Floor: + room: Orange Tower + door: Second Floor + Orange Tower Third Floor: + room: Orange Tower + door: Third Floor + Outside The Undeterred: True + Orange Tower Third Floor: + entrances: + Knight Night Exit: + room: Knight Night (Final) + door: Exit + Orange Tower Second Floor: + room: Orange Tower + door: Third Floor + Orange Tower Fourth Floor: + room: Orange Tower + door: Fourth Floor + Hot Crusts Area: True # sunwarp + Bearer Side Area: # This is complicated because of The Bearer's topology + room: Bearer Side Area + door: Shortcut to Tower + Rhyme Room (Smiley): + door: Rhyme Room Entrance + panels: + RED: + id: Color Arrow Room/Panel_red_afar + tag: midwhite + required_door: + door: Red Barrier + DEER + WREN: + id: Tower Room/Panel_deer_wren_rats_3 + colors: orange + check: True + tag: midorange + doors: + Red Barrier: + id: Color Arrow Room Doors/Door_red_6 + group: Color Hunt Barriers + skip_location: True + panels: + - room: Champion's Rest + panel: RED + Rhyme Room Entrance: + id: Double Room Area Doors/Door_room_entry_stairs2 + skip_location: True + group: Rhyme Room Doors + panels: + - room: The Tenacious + panel: LEVEL (Black) + - room: The Tenacious + panel: RACECAR (Black) + - room: The Tenacious + panel: SOLOS (Black) + Orange Barrier: # see note in Outside The Initiated + id: + - Color Arrow Room Doors/Door_orange_hider_1 + - Color Arrow Room Doors/Door_orange_hider_2 + - Color Arrow Room Doors/Door_orange_hider_3 + location_name: Color Hunt - RED and YELLOW + group: Champion's Rest - Color Barriers + item_name: Champion's Rest - Orange Barrier + panels: + - RED + - room: Directional Gallery + panel: YELLOW + paintings: + - id: arrows_painting_6 + orientation: east + - id: flower_painting_5 + orientation: south + Orange Tower Fourth Floor: + entrances: + Orange Tower Third Floor: + room: Orange Tower + door: Fourth Floor + Orange Tower Fifth Floor: + room: Orange Tower + door: Fifth Floor + Hot Crusts Area: + door: Hot Crusts Door + Crossroads: + - room: Crossroads + door: Tower Entrance + - room: Crossroads + door: Tower Back Entrance + Courtyard: True + Roof: True # through the sunwarp + panels: + RUNT: + id: Shuffle Room/Panel_turn_runt2 + colors: yellow + tag: midyellow + RUNT (2): + id: Shuffle Room/Panel_runt3 + colors: + - yellow + - blue + tag: mid yellow blue + LEARNS + UNSEW: + id: Tower Room/Panel_learns_unsew_unrest_4 + colors: orange + check: True + tag: midorange + HOT CRUSTS: + id: Shuffle Room/Panel_shortcuts + colors: yellow + tag: midyellow + IRK HORN: + id: Shuffle Room/Panel_corner + colors: yellow + check: True + exclude_reduce: True + tag: topyellow + doors: + Hot Crusts Door: + id: Shuffle Room Area Doors/Door_hotcrust_shortcuts + panels: + - HOT CRUSTS + Hot Crusts Area: + entrances: + Orange Tower Fourth Floor: + room: Orange Tower Fourth Floor + door: Hot Crusts Door + Roof: True # through the sunwarp + panels: + EIGHT: + id: Backside Room/Panel_eight_eight_3 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + paintings: + - id: smile_painting_8 + orientation: north + Orange Tower Fifth Floor: + entrances: + Orange Tower Fourth Floor: + room: Orange Tower + door: Fifth Floor + Orange Tower Sixth Floor: + room: Orange Tower + door: Sixth Floor + Cellar: + room: Room Room + door: Shortcut to Fifth Floor + Welcome Back Area: + door: Welcome Back + Art Gallery: + room: Art Gallery + door: Exit + The Bearer: + room: Art Gallery + door: Exit + Outside The Initiated: + room: Art Gallery + door: Exit + panels: + SIZE (Small): + id: Entry Room/Panel_size_small + colors: gray + tag: forbid + SIZE (Big): + id: Entry Room/Panel_size_big + colors: gray + tag: forbid + DRAWL + RUNS: + id: Tower Room/Panel_drawl_runs_enter_5 + colors: orange + check: True + tag: midorange + NINE: + id: Backside Room/Panel_nine_nine_2 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + SUMMER: + id: Entry Room/Panel_summer_summer + tag: midwhite + AUTUMN: + id: Entry Room/Panel_autumn_autumn + tag: midwhite + SPRING: + id: Entry Room/Panel_spring_spring + tag: midwhite + PAINTING (1): + id: Panel Room/Panel_painting_flower + colors: green + tag: forbid + required_room: Cellar + PAINTING (2): + id: Panel Room/Panel_painting_eye + colors: green + tag: forbid + required_room: Cellar + PAINTING (3): + id: Panel Room/Panel_painting_snowman + colors: green + tag: forbid + required_room: Cellar + PAINTING (4): + id: Panel Room/Panel_painting_owl + colors: green + tag: forbid + required_room: Cellar + PAINTING (5): + id: Panel Room/Panel_painting_panda + colors: green + tag: forbid + required_room: Cellar + ROOM: + id: Panel Room/Panel_room_stairs + colors: gray + tag: forbid + required_room: Cellar + doors: + Welcome Back: + id: Entry Room Area Doors/Door_sizes + group: Welcome Back Doors + panels: + - SIZE (Small) + - SIZE (Big) + paintings: + - id: hi_solved_painting3 + orientation: south + - id: hi_solved_painting2 + orientation: south + - id: east_afar + orientation: north + Orange Tower Sixth Floor: + entrances: + Orange Tower Fifth Floor: + room: Orange Tower + door: Sixth Floor + The Scientific: + painting: True + paintings: + - id: arrows_painting_10 + orientation: east + - id: owl_painting_3 + orientation: north + - id: clock_painting + orientation: west + - id: scenery_painting_5d_2 + orientation: south + - id: symmetry_painting_b_7 + orientation: north + - id: panda_painting_2 + orientation: south + - id: pencil_painting + orientation: north + - id: colors_painting2 + orientation: south + - id: cherry_painting2 + orientation: east + - id: hi_solved_painting + orientation: west + Orange Tower Seventh Floor: + entrances: + Orange Tower Sixth Floor: + room: Orange Tower + door: Seventh Floor + panels: + THE END: + id: EndPanel/Panel_end_end + check: True + tag: forbid + non_counting: True + THE MASTER: + # We will set up special rules for this in code. + id: Countdown Panels/Panel_master_master + check: True + tag: forbid + MASTERY: + # This is the MASTERY on the other side of THE FEARLESS. It can only be + # accessed by jumping from the top of the tower. + id: Master Room/Panel_mastery_mastery8 + tag: midwhite + required_door: + door: Mastery + doors: + Mastery: + id: + - Master Room Doors/Door_tower_down + - Master Room Doors/Door_master_master + - Master Room Doors/Door_master_master_2 + - Master Room Doors/Door_master_master_3 + - Master Room Doors/Door_master_master_4 + - Master Room Doors/Door_master_master_5 + - Master Room Doors/Door_master_master_6 + - Master Room Doors/Door_master_master_10 + - Master Room Doors/Door_master_master_11 + - Master Room Doors/Door_master_master_12 + - Master Room Doors/Door_master_master_13 + - Master Room Doors/Door_master_master_14 + - Master Room Doors/Door_master_master_15 + - Master Room Doors/Door_master_down + - Master Room Doors/Door_master_down2 + skip_location: True + panels: + - THE MASTER + Mastery Panels: + skip_item: True + location_name: Mastery Panels + panels: + - room: Room Room + panel: MASTERY + - room: The Steady (Topaz) + panel: MASTERY + - room: Orange Tower Basement + panel: MASTERY + - room: Arrow Garden + panel: MASTERY + - room: Hedge Maze + panel: MASTERY (1) + - room: Roof + panel: MASTERY (1) + - room: Roof + panel: MASTERY (2) + - MASTERY + - room: Hedge Maze + panel: MASTERY (2) + - room: Roof + panel: MASTERY (3) + - room: Roof + panel: MASTERY (4) + - room: Roof + panel: MASTERY (5) + - room: Elements Area + panel: MASTERY + - room: Pilgrim Antechamber + panel: MASTERY + - room: Roof + panel: MASTERY (6) + paintings: + - id: map_painting2 + orientation: north + enter_only: True # otherwise you might just skip the whole game! + Roof: + entrances: + Orange Tower Seventh Floor: True + Crossroads: + room: Crossroads + door: Roof Access + panels: + MASTERY (1): + id: Master Room/Panel_mastery_mastery6 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + MASTERY (2): + id: Master Room/Panel_mastery_mastery7 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + MASTERY (3): + id: Master Room/Panel_mastery_mastery10 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + MASTERY (4): + id: Master Room/Panel_mastery_mastery11 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + MASTERY (5): + id: Master Room/Panel_mastery_mastery12 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + MASTERY (6): + id: Master Room/Panel_mastery_mastery15 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + STAIRCASE: + id: Open Areas/Panel_staircase + tag: midwhite + Orange Tower Basement: + entrances: + Orange Tower Sixth Floor: + room: Orange Tower Seventh Floor + door: Mastery + panels: + MASTERY: + id: Master Room/Panel_mastery_mastery3 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + THE LIBRARY: + id: EndPanel/Panel_library + check: True + tag: forbid + non_counting: True + paintings: + - id: arrows_painting_11 + orientation: east + Courtyard: + entrances: + Roof: True + Orange Tower Fourth Floor: True + Arrow Garden: + painting: True + Starting Room: + door: Painting Shortcut + painting: True + Yellow Backside Area: + room: First Second Third Fourth + door: Backside Door + The Colorful (White): True + panels: + I: + id: Strand Room/Panel_i_staring + colors: blue + tag: forbid + GREEN: + id: Color Arrow Room/Panel_green_afar + tag: midwhite + required_door: + door: Green Barrier + PINECONE: + id: Shuffle Room/Panel_pinecone_pine + colors: brown + tag: botbrown + ACORN: + id: Shuffle Room/Panel_acorn_oak + colors: brown + tag: botbrown + doors: + Painting Shortcut: + painting_id: flower_painting_8 + item_name: Starting Room - Flower Painting + skip_location: True + panels: + - room: First Second Third Fourth + panel: FIRST + - room: First Second Third Fourth + panel: SECOND + - room: First Second Third Fourth + panel: THIRD + - room: First Second Third Fourth + panel: FOURTH + Green Barrier: + id: Color Arrow Room Doors/Door_green_5 + group: Color Hunt Barriers + skip_location: True + panels: + - room: Champion's Rest + panel: GREEN + paintings: + - id: flower_painting_7 + orientation: north + Yellow Backside Area: + entrances: + Courtyard: + room: First Second Third Fourth + door: Backside Door + Roof: True + panels: + BACKSIDE: + id: Backside Room/Panel_backside_3 + tag: midwhite + NINE: + id: Backside Room/Panel_nine_nine_8 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + paintings: + - id: blueman_painting + orientation: east + First Second Third Fourth: + # We are separating this door + its panels into its own room because they + # are accessible from two distinct regions (Courtyard and Yellow Backside + # Area). We need to do this because painting shuffle makes it possible to + # have access to Yellow Backside Area without having access to Courtyard, + # and we want it to still be in logic to solve these panels. + entrances: + Courtyard: True + Yellow Backside Area: True + panels: + FIRST: + id: Backside Room/Panel_first_first + tag: midwhite + SECOND: + id: Backside Room/Panel_second_second + tag: midwhite + THIRD: + id: Backside Room/Panel_third_third + tag: midwhite + FOURTH: + id: Backside Room/Panel_fourth_fourth + tag: midwhite + doors: + Backside Door: + id: Count Up Room Area Doors/Door_yellow_backside + group: Backside Doors + location_name: Courtyard - FIRST, SECOND, THIRD, FOURTH + item_name: Courtyard - Backside Door + panels: + - FIRST + - SECOND + - THIRD + - FOURTH + The Colorful (White): + entrances: + Courtyard: True + The Colorful (Black): + door: Progress Door + panels: + BEGIN: + id: Doorways Room/Panel_begin_start + tag: botwhite + doors: + Progress Door: + id: Doorway Room Doors/Door_white + item_name: The Colorful - White Door + group: Colorful Doors + location_name: The Colorful - White + panels: + - BEGIN + The Colorful (Black): + entrances: + The Colorful (White): + room: The Colorful (White) + door: Progress Door + The Colorful (Red): + door: Progress Door + panels: + FOUND: + id: Doorways Room/Panel_found_lost + colors: black + tag: botblack + doors: + Progress Door: + id: Doorway Room Doors/Door_black + item_name: The Colorful - Black Door + location_name: The Colorful - Black + group: Colorful Doors + panels: + - FOUND + The Colorful (Red): + entrances: + The Colorful (Black): + room: The Colorful (Black) + door: Progress Door + The Colorful (Yellow): + door: Progress Door + panels: + LOAF: + id: Doorways Room/Panel_loaf_crust + colors: red + tag: botred + doors: + Progress Door: + id: Doorway Room Doors/Door_red + item_name: The Colorful - Red Door + location_name: The Colorful - Red + group: Colorful Doors + panels: + - LOAF + The Colorful (Yellow): + entrances: + The Colorful (Red): + room: The Colorful (Red) + door: Progress Door + The Colorful (Blue): + door: Progress Door + panels: + CREAM: + id: Doorways Room/Panel_eggs_breakfast + colors: yellow + tag: botyellow + doors: + Progress Door: + id: Doorway Room Doors/Door_yellow + item_name: The Colorful - Yellow Door + location_name: The Colorful - Yellow + group: Colorful Doors + panels: + - CREAM + The Colorful (Blue): + entrances: + The Colorful (Yellow): + room: The Colorful (Yellow) + door: Progress Door + The Colorful (Purple): + door: Progress Door + panels: + SUN: + id: Doorways Room/Panel_sun_sky + colors: blue + tag: botblue + doors: + Progress Door: + id: Doorway Room Doors/Door_blue + item_name: The Colorful - Blue Door + location_name: The Colorful - Blue + group: Colorful Doors + panels: + - SUN + The Colorful (Purple): + entrances: + The Colorful (Blue): + room: The Colorful (Blue) + door: Progress Door + The Colorful (Orange): + door: Progress Door + panels: + SPOON: + id: Doorways Room/Panel_teacher_substitute + colors: purple + tag: botpurple + doors: + Progress Door: + id: Doorway Room Doors/Door_purple + item_name: The Colorful - Purple Door + location_name: The Colorful - Purple + group: Colorful Doors + panels: + - SPOON + The Colorful (Orange): + entrances: + The Colorful (Purple): + room: The Colorful (Purple) + door: Progress Door + The Colorful (Green): + door: Progress Door + panels: + LETTERS: + id: Doorways Room/Panel_walnuts_orange + colors: orange + tag: botorange + doors: + Progress Door: + id: Doorway Room Doors/Door_orange + item_name: The Colorful - Orange Door + location_name: The Colorful - Orange + group: Colorful Doors + panels: + - LETTERS + The Colorful (Green): + entrances: + The Colorful (Orange): + room: The Colorful (Orange) + door: Progress Door + The Colorful (Brown): + door: Progress Door + panels: + WALLS: + id: Doorways Room/Panel_path_i + colors: green + tag: forbid + doors: + Progress Door: + id: Doorway Room Doors/Door_green + item_name: The Colorful - Green Door + location_name: The Colorful - Green + group: Colorful Doors + panels: + - WALLS + The Colorful (Brown): + entrances: + The Colorful (Green): + room: The Colorful (Green) + door: Progress Door + The Colorful (Gray): + door: Progress Door + panels: + IRON: + id: Doorways Room/Panel_iron_rust + colors: brown + tag: botbrown + doors: + Progress Door: + id: Doorway Room Doors/Door_brown + item_name: The Colorful - Brown Door + location_name: The Colorful - Brown + group: Colorful Doors + panels: + - IRON + The Colorful (Gray): + entrances: + The Colorful (Brown): + room: The Colorful (Brown) + door: Progress Door + The Colorful: + door: Progress Door + panels: + OBSTACLE: + id: Doorways Room/Panel_obstacle_door + colors: gray + tag: forbid + doors: + Progress Door: + id: + - Doorway Room Doors/Door_gray + - Doorway Room Doors/Door_gray2 # See comment below + item_name: The Colorful - Gray Door + location_name: The Colorful - Gray + group: Colorful Doors + panels: + - OBSTACLE + The Colorful: + # The set of required_doors in the achievement panel should prevent + # generation from asking you to solve The Colorful before opening all of the + # doors. Access from the roof is included so that the painting here could be + # an entrance. The client will have to be hardcoded to not open the door to + # the achievement until all of the doors are open, whether by solving the + # panels or through receiving items. + entrances: + The Colorful (Gray): + room: The Colorful (Gray) + door: Progress Door + Roof: True + panels: + Achievement: + id: Countdown Panels/Panel_colorful_colorful + check: True + tag: forbid + required_door: + - room: The Colorful (White) + door: Progress Door + - room: The Colorful (Black) + door: Progress Door + - room: The Colorful (Red) + door: Progress Door + - room: The Colorful (Yellow) + door: Progress Door + - room: The Colorful (Blue) + door: Progress Door + - room: The Colorful (Purple) + door: Progress Door + - room: The Colorful (Orange) + door: Progress Door + - room: The Colorful (Green) + door: Progress Door + - room: The Colorful (Brown) + door: Progress Door + - room: The Colorful (Gray) + door: Progress Door + achievement: The Colorful + paintings: + - id: arrows_painting_12 + orientation: north + Welcome Back Area: + entrances: + Starting Room: + door: Shortcut to Starting Room + Hub Room: True + Outside The Wondrous: True + Outside The Undeterred: True + Outside The Initiated: True + Outside The Agreeable: True + Outside The Wanderer: True + Eight Alcove: True + Orange Tower Fifth Floor: + room: Orange Tower Fifth Floor + door: Welcome Back + Challenge Room: + room: Challenge Room + door: Welcome Door + panels: + WELCOME BACK: + id: Entry Room/Panel_return_return + tag: midwhite + SECRET: + id: Entry Room/Panel_secret_secret + tag: midwhite + CLOCKWISE: + id: Shuffle Room/Panel_clockwise_counterclockwise + colors: black + check: True + exclude_reduce: True + tag: botblack + doors: + Shortcut to Starting Room: + id: Entry Room Area Doors/Door_return_return + group: Welcome Back Doors + include_reduce: True + panels: + - WELCOME BACK + Owl Hallway: + entrances: + Hidden Room: + painting: True + Hedge Maze: + door: Shortcut to Hedge Maze + Orange Tower Sixth Floor: + painting: True + panels: + STRAYS: + id: Maze Room/Panel_strays_maze + colors: purple + tag: toppurp + READS + RUST: + id: Tower Room/Panel_reads_rust_lawns_6 + colors: orange + check: True + tag: midorange + doors: + Shortcut to Hedge Maze: + id: Maze Area Doors/Door_strays_maze + group: Hedge Maze Doors + panels: + - STRAYS + paintings: + - id: arrows_painting_8 + orientation: south + - id: maze_painting_2 + orientation: north + - id: owl_painting_2 + orientation: south + required_when_no_doors: True + - id: clock_painting_4 + orientation: north + Outside The Initiated: + entrances: + Hub Room: + door: Shortcut to Hub Room + Knight Night Exit: + room: Knight Night (Final) + door: Exit + Orange Tower Third Floor: True # sunwarp + Orange Tower Fifth Floor: + room: Art Gallery + door: Exit + panels: + SEVEN (1): + id: Backside Room/Panel_seven_seven_5 + tag: midwhite + required_door: + room: Number Hunt + door: Sevens + SEVEN (2): + id: Backside Room/Panel_seven_seven_6 + tag: midwhite + required_door: + room: Number Hunt + door: Sevens + EIGHT: + id: Backside Room/Panel_eight_eight_7 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + NINE: + id: Backside Room/Panel_nine_nine_4 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + BLUE: + id: Color Arrow Room/Panel_blue_afar + tag: midwhite + required_door: + door: Blue Barrier + ORANGE: + id: Color Arrow Room/Panel_orange_afar + tag: midwhite + required_door: + door: Orange Barrier + UNCOVER: + id: Appendix Room/Panel_discover_recover + colors: purple + tag: midpurp + OXEN: + id: Rhyme Room/Panel_locked_knocked + colors: purple + tag: midpurp + BACKSIDE: + id: Backside Room/Panel_backside_1 + tag: midwhite + The Optimistic: + id: Countdown Panels/Panel_optimistic_optimistic + check: True + tag: forbid + required_door: + door: Backsides + achievement: The Optimistic + PAST: + id: Shuffle Room/Panel_past_present + colors: brown + tag: botbrown + FUTURE: + id: Shuffle Room/Panel_future_present + colors: + - brown + - black + tag: bot brown black + FUTURE (2): + id: Shuffle Room/Panel_future_past + colors: black + tag: botblack + PAST (2): + id: Shuffle Room/Panel_past_future + colors: black + tag: botblack + PRESENT: + id: Shuffle Room/Panel_past_past + colors: + - brown + - black + tag: bot brown black + SMILE: + id: Open Areas/Panel_smile_smile + tag: midwhite + ANGERED: + id: Open Areas/Panel_angered_enraged + colors: + - yellow + tag: syn anagram + copy_to_sign: sign18 + VOTE: + id: Open Areas/Panel_vote_veto + colors: + - yellow + - black + tag: ant anagram + copy_to_sign: sign17 + doors: + Shortcut to Hub Room: + id: Appendix Room Area Doors/Door_recover_discover + panels: + - UNCOVER + Blue Barrier: + id: Color Arrow Room Doors/Door_blue_3 + group: Color Hunt Barriers + skip_location: True + panels: + - room: Champion's Rest + panel: BLUE + Orange Barrier: + id: Color Arrow Room Doors/Door_orange_3 + group: Color Hunt Barriers + skip_location: True + panels: + - room: Champion's Rest + panel: ORANGE + Initiated Entrance: + id: Red Blue Purple Room Area Doors/Door_locked_knocked + item_name: The Initiated - Entrance + panels: + - OXEN + # These would be more appropriate in Champion's Rest, but as currently + # implemented, locations need to include at least one panel from the + # containing region. + Green Barrier: + id: Color Arrow Room Doors/Door_green_hider_1 + location_name: Color Hunt - BLUE and YELLOW + item_name: Champion's Rest - Green Barrier + group: Champion's Rest - Color Barriers + panels: + - BLUE + - room: Directional Gallery + panel: YELLOW + Purple Barrier: + id: + - Color Arrow Room Doors/Door_purple_hider_1 + - Color Arrow Room Doors/Door_purple_hider_2 + - Color Arrow Room Doors/Door_purple_hider_3 + location_name: Color Hunt - RED and BLUE + item_name: Champion's Rest - Purple Barrier + group: Champion's Rest - Color Barriers + panels: + - BLUE + - room: Orange Tower Third Floor + panel: RED + Entrance: + id: + - Color Arrow Room Doors/Door_all_hider_1 + - Color Arrow Room Doors/Door_all_hider_2 + - Color Arrow Room Doors/Door_all_hider_3 + location_name: Color Hunt - GREEN, ORANGE and PURPLE + item_name: Champion's Rest - Entrance + panels: + - ORANGE + - room: Courtyard + panel: GREEN + - room: Outside The Agreeable + panel: PURPLE + Backsides: + event: True + panels: + - room: The Observant + panel: BACKSIDE + - room: Yellow Backside Area + panel: BACKSIDE + - room: Directional Gallery + panel: BACKSIDE + - room: The Bearer + panel: BACKSIDE + paintings: + - id: clock_painting_5 + orientation: east + - id: smile_painting_1 + orientation: north + The Initiated: + entrances: + Outside The Initiated: + room: Outside The Initiated + door: Initiated Entrance + panels: + Achievement: + id: Countdown Panels/Panel_illuminated_initiated + colors: purple + tag: forbid + check: True + achievement: The Initiated + DAUGHTER: + id: Rhyme Room/Panel_daughter_laughter + colors: purple + tag: midpurp + START: + id: Rhyme Room/Panel_move_love + colors: purple + tag: double midpurp + subtag: left + link: change STARS + STARE: + id: Rhyme Room/Panel_stove_love + colors: purple + tag: double midpurp + subtag: right + link: change STARS + HYPE: + id: Rhyme Room/Panel_scope_type + colors: purple + tag: midpurp and rhyme + copy_to_sign: sign16 + ABYSS: + id: Rhyme Room/Panel_abyss_this + colors: purple + tag: toppurp + SWEAT: + id: Rhyme Room/Panel_sweat_great + colors: purple + tag: double midpurp + subtag: left + link: change GREAT + BEAT: + id: Rhyme Room/Panel_beat_great + colors: purple + tag: double midpurp + subtag: right + link: change GREAT + ALUMNI: + id: Rhyme Room/Panel_alumni_hi + colors: purple + tag: midpurp and rhyme + copy_to_sign: sign14 + PATS: + id: Rhyme Room/Panel_wrath_path + colors: purple + tag: midpurp and rhyme + copy_to_sign: sign15 + KNIGHT: + id: Rhyme Room/Panel_knight_write + colors: purple + tag: double toppurp + subtag: left + link: change WRITE + BYTE: + id: Rhyme Room/Panel_byte_write + colors: purple + tag: double toppurp + subtag: right + link: change WRITE + MAIM: + id: Rhyme Room/Panel_maim_same + colors: purple + tag: toppurp + MORGUE: + id: Rhyme Room/Panel_chair_bear + colors: purple + tag: purple rhyme change stack + subtag: top + link: prcs CYBORG + CHAIR: + id: Rhyme Room/Panel_bare_bear + colors: purple + tag: toppurp + HUMAN: + id: Rhyme Room/Panel_cost_most + colors: purple + tag: purple rhyme change stack + subtag: bot + link: prcs CYBORG + BED: + id: Rhyme Room/Panel_bed_dead + colors: purple + tag: toppurp + The Traveled: + entrances: + Hub Room: + room: Hub Room + door: Traveled Entrance + Color Hallways: + door: Color Hallways Entrance + panels: + Achievement: + id: Countdown Panels/Panel_traveled_traveled + required_room: Hub Room + tag: forbid + check: True + achievement: The Traveled + CLOSE: + id: Synonym Room/Panel_close_near + tag: botwhite + COMPOSE: + id: Synonym Room/Panel_compose_write + tag: double botwhite + subtag: left + link: syn WRITE + RECORD: + id: Synonym Room/Panel_record_write + tag: double botwhite + subtag: right + link: syn WRITE + CATEGORY: + id: Synonym Room/Panel_category_type + tag: botwhite + HELLO: + id: Synonym Room/Panel_hello_hi + tag: botwhite + DUPLICATE: + id: Synonym Room/Panel_duplicate_same + tag: double botwhite + subtag: left + link: syn SAME + IDENTICAL: + id: Synonym Room/Panel_identical_same + tag: double botwhite + subtag: right + link: syn SAME + DISTANT: + id: Synonym Room/Panel_distant_far + tag: botwhite + HAY: + id: Synonym Room/Panel_hay_straw + tag: botwhite + GIGGLE: + id: Synonym Room/Panel_giggle_laugh + tag: double botwhite + subtag: left + link: syn LAUGH + CHUCKLE: + id: Synonym Room/Panel_chuckle_laugh + tag: double botwhite + subtag: right + link: syn LAUGH + SNITCH: + id: Synonym Room/Panel_snitch_rat + tag: botwhite + CONCEALED: + id: Synonym Room/Panel_concealed_hidden + tag: botwhite + PLUNGE: + id: Synonym Room/Panel_plunge_fall + tag: double botwhite + subtag: left + link: syn FALL + AUTUMN: + id: Synonym Room/Panel_autumn_fall + tag: double botwhite + subtag: right + link: syn FALL + ROAD: + id: Synonym Room/Panel_growths_warts + tag: botwhite + FOUR: + id: Backside Room/Panel_four_four_4 + tag: midwhite + required_door: + room: Outside The Undeterred + door: Fours + doors: + Color Hallways Entrance: + id: Appendix Room Area Doors/Door_hello_hi + group: Entrance to The Traveled + panels: + - HELLO + Color Hallways: + entrances: + The Traveled: + room: The Traveled + door: Color Hallways Entrance + Outside The Bold: True + Outside The Undeterred: True + Crossroads: True + Hedge Maze: True + Outside The Initiated: True # backside + Directional Gallery: True # backside + Yellow Backside Area: True + The Bearer: + room: The Bearer + door: Backside Door + The Observant: + room: The Observant + door: Backside Door + Outside The Bold: + entrances: + Color Hallways: True + Champion's Rest: + room: Champion's Rest + door: Shortcut to The Steady + The Bearer: + room: The Bearer + door: Shortcut to The Bold + Directional Gallery: + # There is a painting warp here from the Directional Gallery, but it + # only appears when the sixes are revealed. It could be its own item if + # we wanted. + room: Number Hunt + door: Sixes + painting: True + Starting Room: + door: Painting Shortcut + painting: True + Room Room: True # trapdoor + panels: + UNOPEN: + id: Truncate Room/Panel_unopened_open + colors: red + tag: midred + BEGIN: + id: Rock Room/Panel_begin_begin + tag: midwhite + SIX: + id: Backside Room/Panel_six_six_4 + tag: midwhite + required_door: + room: Number Hunt + door: Sixes + NINE: + id: Backside Room/Panel_nine_nine_5 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + LEFT: + id: Shuffle Room/Panel_left_left_2 + tag: midwhite + RIGHT: + id: Shuffle Room/Panel_right_right_2 + tag: midwhite + RISE (Horizon): + id: Open Areas/Panel_rise_horizon + colors: blue + tag: double topblue + subtag: left + link: expand HORIZON + RISE (Sunrise): + id: Open Areas/Panel_rise_sunrise + colors: blue + tag: double topblue + subtag: left + link: expand SUNRISE + ZEN: + id: Open Areas/Panel_son_horizon + colors: blue + tag: double topblue + subtag: right + link: expand HORIZON + SON: + id: Open Areas/Panel_son_sunrise + colors: blue + tag: double topblue + subtag: right + link: expand SUNRISE + STARGAZER: + id: Open Areas/Panel_stargazer_stargazer + tag: midwhite + required_door: + door: Stargazer Door + MOUTH: + id: Cross Room/Panel_mouth_south + colors: purple + tag: midpurp + YEAST: + id: Cross Room/Panel_yeast_east + colors: red + tag: midred + WET: + id: Cross Room/Panel_wet_west + colors: blue + tag: midblue + doors: + Bold Entrance: + id: Red Blue Purple Room Area Doors/Door_unopened_open + item_name: The Bold - Entrance + panels: + - UNOPEN + Painting Shortcut: + painting_id: pencil_painting6 + skip_location: True + item_name: Starting Room - Pencil Painting + panels: + - UNOPEN + Steady Entrance: + id: Rock Room Doors/Door_2 + item_name: The Steady - Entrance + panels: + - BEGIN + Lilac Entrance: + event: True + panels: + - room: The Steady (Rose) + panel: SOAR + Stargazer Door: + event: True + panels: + - RISE (Horizon) + - RISE (Sunrise) + - ZEN + - SON + paintings: + - id: pencil_painting2 + orientation: west + - id: north_missing2 + orientation: north + The Bold: + entrances: + Outside The Bold: + room: Outside The Bold + door: Bold Entrance + panels: + Achievement: + id: Countdown Panels/Panel_emboldened_bold + colors: red + tag: forbid + check: True + achievement: The Bold + FOOT: + id: Truncate Room/Panel_foot_toe + colors: red + tag: botred + NEEDLE: + id: Truncate Room/Panel_needle_eye + colors: red + tag: double botred + subtag: left + link: mero EYE + FACE: + id: Truncate Room/Panel_face_eye + colors: red + tag: double botred + subtag: right + link: mero EYE + SIGN: + id: Truncate Room/Panel_sign_sigh + colors: red + tag: topred + HEARTBREAK: + id: Truncate Room/Panel_heartbreak_brake + colors: red + tag: topred + UNDEAD: + id: Truncate Room/Panel_undead_dead + colors: red + tag: double midred + subtag: left + link: trunc DEAD + DEADLINE: + id: Truncate Room/Panel_deadline_dead + colors: red + tag: double midred + subtag: right + link: trunc DEAD + SUSHI: + id: Truncate Room/Panel_sushi_hi + colors: red + tag: midred + THISTLE: + id: Truncate Room/Panel_thistle_this + colors: red + tag: midred + LANDMASS: + id: Truncate Room/Panel_landmass_mass + colors: red + tag: double midred + subtag: left + link: trunc MASS + MASSACRED: + id: Truncate Room/Panel_massacred_mass + colors: red + tag: double midred + subtag: right + link: trunc MASS + AIRPLANE: + id: Truncate Room/Panel_airplane_plain + colors: red + tag: topred + NIGHTMARE: + id: Truncate Room/Panel_nightmare_knight + colors: red + tag: topred + MOUTH: + id: Truncate Room/Panel_mouth_teeth + colors: red + tag: double botred + subtag: left + link: mero TEETH + SAW: + id: Truncate Room/Panel_saw_teeth + colors: red + tag: double botred + subtag: right + link: mero TEETH + HAND: + id: Truncate Room/Panel_hand_finger + colors: red + tag: botred + Outside The Undeterred: + entrances: + Color Hallways: True + Orange Tower First Floor: True # sunwarp + Orange Tower Second Floor: True + The Artistic (Smiley): True + The Artistic (Panda): True + The Artistic (Apple): True + The Artistic (Lattice): True + Yellow Backside Area: + painting: True + Number Hunt: + door: Number Hunt + Directional Gallery: + room: Directional Gallery + door: Shortcut to The Undeterred + Starting Room: + door: Painting Shortcut + painting: True + panels: + HOLLOW: + id: Hallway Room/Panel_hollow_hollow + tag: midwhite + ART + ART: + id: Tower Room/Panel_art_art_eat_2 + colors: orange + check: True + tag: midorange + PEN: + id: Blue Room/Panel_pen_open + colors: blue + tag: midblue + HUSTLING: + id: Open Areas/Panel_hustling_sunlight + colors: yellow + tag: midyellow + SUNLIGHT: + id: Open Areas/Panel_sunlight_light + colors: red + tag: midred + required_panel: + panel: HUSTLING + LIGHT: + id: Open Areas/Panel_light_bright + colors: purple + tag: midpurp + required_panel: + panel: SUNLIGHT + BRIGHT: + id: Open Areas/Panel_bright_sunny + tag: botwhite + required_panel: + panel: LIGHT + SUNNY: + id: Open Areas/Panel_sunny_rainy + colors: black + tag: botblack + required_panel: + panel: BRIGHT + RAINY: + id: Open Areas/Panel_rainy_rainbow + colors: brown + tag: botbrown + required_panel: + panel: SUNNY + check: True + ZERO: + id: Backside Room/Panel_zero_zero + tag: midwhite + required_door: + room: Number Hunt + door: Zero Door + ONE: + id: Backside Room/Panel_one_one + tag: midwhite + TWO (1): + id: Backside Room/Panel_two_two + tag: midwhite + required_door: + door: Twos + TWO (2): + id: Backside Room/Panel_two_two_2 + tag: midwhite + required_door: + door: Twos + THREE (1): + id: Backside Room/Panel_three_three + tag: midwhite + required_door: + door: Threes + THREE (2): + id: Backside Room/Panel_three_three_2 + tag: midwhite + required_door: + door: Threes + THREE (3): + id: Backside Room/Panel_three_three_3 + tag: midwhite + required_door: + door: Threes + FOUR: + id: Backside Room/Panel_four_four + tag: midwhite + required_door: + door: Fours + doors: + Undeterred Entrance: + id: Red Blue Purple Room Area Doors/Door_pen_open + item_name: The Undeterred - Entrance + panels: + - PEN + Painting Shortcut: + painting_id: + - blueman_painting_3 + - arrows_painting3 + skip_location: True + item_name: Starting Room - Blue Painting + panels: + - PEN + Green Painting: + painting_id: maze_painting_3 + skip_location: True + panels: + - FOUR + Twos: + id: + - Count Up Room Area Doors/Door_two_hider + - Count Up Room Area Doors/Door_two_hider_2 + include_reduce: True + panels: + - ONE + Threes: + id: + - Count Up Room Area Doors/Door_three_hider + - Count Up Room Area Doors/Door_three_hider_2 + - Count Up Room Area Doors/Door_three_hider_3 + location_name: Twos + include_reduce: True + panels: + - TWO (1) + - TWO (2) + Number Hunt: + id: Count Up Room Area Doors/Door_three_unlocked + location_name: Threes + include_reduce: True + panels: + - THREE (1) + - THREE (2) + - THREE (3) + Fours: + id: + - Count Up Room Area Doors/Door_four_hider + - Count Up Room Area Doors/Door_four_hider_2 + - Count Up Room Area Doors/Door_four_hider_3 + - Count Up Room Area Doors/Door_four_hider_4 + skip_location: True + panels: + - THREE (1) + - THREE (2) + - THREE (3) + Fives: + id: + - Count Up Room Area Doors/Door_five_hider + - Count Up Room Area Doors/Door_five_hider_4 + - Count Up Room Area Doors/Door_five_hider_5 + location_name: Fours + item_name: Number Hunt - Fives + include_reduce: True + panels: + - FOUR + - room: Hub Room + panel: FOUR + - room: Dead End Area + panel: FOUR + - room: The Traveled + panel: FOUR + Challenge Entrance: + id: Count Up Room Area Doors/Door_zero_unlocked + item_name: Number Hunt - Challenge Entrance + panels: + - ZERO + paintings: + - id: maze_painting_3 + enter_only: True + orientation: north + move: True + required_door: + door: Green Painting + - id: blueman_painting_2 + orientation: east + The Undeterred: + entrances: + Outside The Undeterred: + room: Outside The Undeterred + door: Undeterred Entrance + panels: + Achievement: + id: Countdown Panels/Panel_deterred_undeterred + colors: blue + tag: forbid + check: True + achievement: The Undeterred + BONE: + id: Blue Room/Panel_bone_skeleton + colors: blue + tag: botblue + EYE: + id: Blue Room/Panel_mouth_face + colors: blue + tag: double botblue + subtag: left + link: holo FACE + MOUTH: + id: Blue Room/Panel_eye_face + colors: blue + tag: double botblue + subtag: right + link: holo FACE + IRIS: + id: Blue Room/Panel_toucan_bird + colors: blue + tag: botblue + EYE (2): + id: Blue Room/Panel_two_toucan + colors: blue + tag: topblue + ICE: + id: Blue Room/Panel_ice_eyesight + colors: blue + tag: double topblue + subtag: left + link: hex EYESIGHT + HEIGHT: + id: Blue Room/Panel_height_eyesight + colors: blue + tag: double topblue + subtag: right + link: hex EYESIGHT + EYE (3): + id: Blue Room/Panel_eye_hi + colors: blue + tag: topblue + NOT: + id: Blue Room/Panel_not_notice + colors: blue + tag: midblue + JUST: + id: Blue Room/Panel_just_readjust + colors: blue + tag: double midblue + subtag: left + link: exp READJUST + READ: + id: Blue Room/Panel_read_readjust + colors: blue + tag: double midblue + subtag: right + link: exp READJUST + FATHER: + id: Blue Room/Panel_ate_primate + colors: blue + tag: midblue + FEATHER: + id: Blue Room/Panel_primate_mammal + colors: blue + tag: botblue + CONTINENT: + id: Blue Room/Panel_continent_planet + colors: blue + tag: double botblue + subtag: left + link: holo PLANET + OCEAN: + id: Blue Room/Panel_ocean_planet + colors: blue + tag: double botblue + subtag: right + link: holo PLANET + WALL: + id: Blue Room/Panel_wall_room + colors: blue + tag: botblue + Number Hunt: + # This works a little differently than in the base game. The door to the + # initial number in each set opens at the same time as the rest of the doors + # in that set. + entrances: + Outside The Undeterred: + room: Outside The Undeterred + door: Number Hunt + Directional Gallery: + door: Door to Directional Gallery + Challenge Room: + room: Outside The Undeterred + door: Challenge Entrance + panels: + FIVE: + id: Backside Room/Panel_five_five + tag: midwhite + required_door: + room: Outside The Undeterred + door: Fives + SIX: + id: Backside Room/Panel_six_six + tag: midwhite + required_door: + door: Sixes + SEVEN: + id: Backside Room/Panel_seven_seven + tag: midwhite + required_door: + door: Sevens + EIGHT: + id: Backside Room/Panel_eight_eight + tag: midwhite + required_door: + door: Eights + NINE: + id: Backside Room/Panel_nine_nine + tag: midwhite + required_door: + door: Nines + doors: + Door to Directional Gallery: + id: Count Up Room Area Doors/Door_five_unlocked + group: Directional Gallery Doors + skip_location: True + panels: + - FIVE + Sixes: + id: + - Count Up Room Area Doors/Door_six_hider + - Count Up Room Area Doors/Door_six_hider_2 + - Count Up Room Area Doors/Door_six_hider_3 + - Count Up Room Area Doors/Door_six_hider_4 + - Count Up Room Area Doors/Door_six_hider_5 + - Count Up Room Area Doors/Door_six_hider_6 + painting_id: pencil_painting3 # See note in Outside The Bold + location_name: Fives + include_reduce: True + panels: + - FIVE + - room: Outside The Agreeable + panel: FIVE (1) + - room: Outside The Agreeable + panel: FIVE (2) + - room: Directional Gallery + panel: FIVE (1) + - room: Directional Gallery + panel: FIVE (2) + Sevens: + id: + - Count Up Room Area Doors/Door_seven_hider + - Count Up Room Area Doors/Door_seven_unlocked + - Count Up Room Area Doors/Door_seven_hider_2 + - Count Up Room Area Doors/Door_seven_hider_3 + - Count Up Room Area Doors/Door_seven_hider_4 + - Count Up Room Area Doors/Door_seven_hider_5 + - Count Up Room Area Doors/Door_seven_hider_6 + - Count Up Room Area Doors/Door_seven_hider_7 + location_name: Sixes + include_reduce: True + panels: + - SIX + - room: Outside The Bold + panel: SIX + - room: Directional Gallery + panel: SIX (1) + - room: Directional Gallery + panel: SIX (2) + - room: The Bearer (East) + panel: SIX + - room: The Bearer (South) + panel: SIX + Eights: + id: + - Count Up Room Area Doors/Door_eight_hider + - Count Up Room Area Doors/Door_eight_unlocked + - Count Up Room Area Doors/Door_eight_hider_2 + - Count Up Room Area Doors/Door_eight_hider_3 + - Count Up Room Area Doors/Door_eight_hider_4 + - Count Up Room Area Doors/Door_eight_hider_5 + - Count Up Room Area Doors/Door_eight_hider_6 + - Count Up Room Area Doors/Door_eight_hider_7 + - Count Up Room Area Doors/Door_eight_hider_8 + location_name: Sevens + include_reduce: True + panels: + - SEVEN + - room: Directional Gallery + panel: SEVEN + - room: Knight Night Exit + panel: SEVEN (1) + - room: Knight Night Exit + panel: SEVEN (2) + - room: Knight Night Exit + panel: SEVEN (3) + - room: Outside The Initiated + panel: SEVEN (1) + - room: Outside The Initiated + panel: SEVEN (2) + Nines: + id: + - Count Up Room Area Doors/Door_nine_hider + - Count Up Room Area Doors/Door_nine_hider_2 + - Count Up Room Area Doors/Door_nine_hider_3 + - Count Up Room Area Doors/Door_nine_hider_4 + - Count Up Room Area Doors/Door_nine_hider_5 + - Count Up Room Area Doors/Door_nine_hider_6 + - Count Up Room Area Doors/Door_nine_hider_7 + - Count Up Room Area Doors/Door_nine_hider_8 + - Count Up Room Area Doors/Door_nine_hider_9 + location_name: Eights + include_reduce: True + panels: + - EIGHT + - room: Directional Gallery + panel: EIGHT + - room: The Eyes They See + panel: EIGHT + - room: Dead End Area + panel: EIGHT + - room: Crossroads + panel: EIGHT + - room: Hot Crusts Area + panel: EIGHT + - room: Art Gallery + panel: EIGHT + - room: Outside The Initiated + panel: EIGHT + Zero Door: + # The black wall isn't a door, so we can't ever hide it. + id: Count Up Room Area Doors/Door_zero_hider_2 + location_name: Nines + item_name: Outside The Undeterred - Zero Door + include_reduce: True + panels: + - NINE + - room: Directional Gallery + panel: NINE + - room: Amen Name Area + panel: NINE + - room: Yellow Backside Area + panel: NINE + - room: Outside The Initiated + panel: NINE + - room: Outside The Bold + panel: NINE + - room: Rhyme Room (Cross) + panel: NINE + - room: Orange Tower Fifth Floor + panel: NINE + - room: Elements Area + panel: NINE + paintings: + - id: smile_painting_5 + enter_only: True + orientation: east + required_door: + door: Eights + Directional Gallery: + entrances: + Outside The Agreeable: True # sunwarp + Orange Tower First Floor: + room: Orange Tower First Floor + door: Salt Pepper Door + Outside The Undeterred: + door: Shortcut to The Undeterred + Number Hunt: + room: Number Hunt + door: Door to Directional Gallery + panels: + PEPPER: + id: Backside Room/Panel_pepper_salt + colors: black + tag: botblack + TURN: + id: Backside Room/Panel_turn_return + colors: blue + tag: midblue + LEARN: + id: Backside Room/Panel_learn_return + colors: purple + tag: midpurp + FIVE (1): + id: Backside Room/Panel_five_five_3 + tag: midwhite + required_panel: + panel: LIGHT + FIVE (2): + id: Backside Room/Panel_five_five_2 + tag: midwhite + required_panel: + panel: WARD + SIX (1): + id: Backside Room/Panel_six_six_3 + tag: midwhite + required_door: + room: Number Hunt + door: Sixes + SIX (2): + id: Backside Room/Panel_six_six_2 + tag: midwhite + required_door: + room: Number Hunt + door: Sixes + SEVEN: + id: Backside Room/Panel_seven_seven_2 + tag: midwhite + required_door: + room: Number Hunt + door: Sevens + EIGHT: + id: Backside Room/Panel_eight_eight_2 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + NINE: + id: Backside Room/Panel_nine_nine_6 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + BACKSIDE: + id: Backside Room/Panel_backside_4 + tag: midwhite + "834283054": + id: Tower Room/Panel_834283054_undaunted + colors: orange + check: True + exclude_reduce: True + tag: midorange + required_door: + room: Number Hunt + door: Sixes + PARANOID: + id: Backside Room/Panel_paranoid_paranoid + tag: midwhite + check: True + exclude_reduce: True + required_door: + room: Number Hunt + door: Sixes + YELLOW: + id: Color Arrow Room/Panel_yellow_afar + tag: midwhite + required_door: + door: Yellow Barrier + WADED + WEE: + id: Tower Room/Panel_waded_wee_warts_7 + colors: orange + check: True + exclude_reduce: True + tag: midorange + THE EYES: + id: Shuffle Room/Panel_theeyes_theeyes + tag: midwhite + LEFT: + id: Shuffle Room/Panel_left_left + tag: midwhite + RIGHT: + id: Shuffle Room/Panel_right_right + tag: midwhite + MIDDLE: + id: Shuffle Room/Panel_middle_middle + tag: midwhite + WARD: + id: Backside Room/Panel_ward_forward + colors: blue + tag: midblue + HIND: + id: Backside Room/Panel_hind_behind + colors: blue + tag: midblue + RIG: + id: Backside Room/Panel_rig_right + colors: blue + tag: midblue + WINDWARD: + id: Backside Room/Panel_windward_forward + colors: purple + tag: midpurp + LIGHT: + id: Backside Room/Panel_light_right + colors: purple + tag: midpurp + REWIND: + id: Backside Room/Panel_rewind_behind + colors: purple + tag: midpurp + doors: + Shortcut to The Undeterred: + id: Count Up Room Area Doors/Door_return_double + group: Directional Gallery Doors + panels: + - TURN + - LEARN + Yellow Barrier: + id: Color Arrow Room Doors/Door_yellow_4 + group: Color Hunt Barriers + skip_location: True + panels: + - room: Champion's Rest + panel: YELLOW + paintings: + - id: smile_painting_7 + orientation: south + - id: flower_painting_4 + orientation: south + - id: pencil_painting3 + enter_only: True + orientation: east + move: True + required_door: + room: Number Hunt + door: Sixes + - id: boxes_painting + orientation: south + - id: cherry_painting + orientation: east + Champion's Rest: + entrances: + Outside The Bold: + door: Shortcut to The Steady + Orange Tower Fourth Floor: True # sunwarp + Roof: True # through ceiling of sunwarp + panels: + EXIT: + id: Rock Room/Panel_red_red + tag: midwhite + HUES: + id: Color Arrow Room/Panel_hues_colors + tag: botwhite + RED: + id: Color Arrow Room/Panel_red_near + check: True + tag: midwhite + BLUE: + id: Color Arrow Room/Panel_blue_near + check: True + tag: midwhite + YELLOW: + id: Color Arrow Room/Panel_yellow_near + check: True + tag: midwhite + GREEN: + id: Color Arrow Room/Panel_green_near + check: True + tag: midwhite + required_door: + room: Outside The Initiated + door: Green Barrier + PURPLE: + id: Color Arrow Room/Panel_purple_near + check: True + tag: midwhite + required_door: + room: Outside The Initiated + door: Purple Barrier + ORANGE: + id: Color Arrow Room/Panel_orange_near + check: True + tag: midwhite + required_door: + room: Orange Tower Third Floor + door: Orange Barrier + YOU: + id: Color Arrow Room/Panel_you + required_door: + room: Outside The Initiated + door: Entrance + check: True + colors: gray + tag: forbid + ME: + id: Color Arrow Room/Panel_me + colors: gray + tag: forbid + required_door: + room: Outside The Initiated + door: Entrance + SECRET BLUE: + # Pretend this and the other two are white, because they are snipes. + # TODO: Extract them and randomize them? + id: Color Arrow Room/Panel_secret_blue + tag: forbid + required_door: + room: Outside The Initiated + door: Entrance + SECRET YELLOW: + id: Color Arrow Room/Panel_secret_yellow + tag: forbid + required_door: + room: Outside The Initiated + door: Entrance + SECRET RED: + id: Color Arrow Room/Panel_secret_red + tag: forbid + required_door: + room: Outside The Initiated + door: Entrance + doors: + Shortcut to The Steady: + id: Rock Room Doors/Door_hint + panels: + - EXIT + paintings: + - id: arrows_painting_7 + orientation: east + - id: fruitbowl_painting3 + orientation: west + enter_only: True + required_door: + room: Outside The Initiated + door: Entrance + - id: colors_painting + orientation: south + enter_only: True + required_door: + room: Outside The Initiated + door: Entrance + The Bearer: + entrances: + Outside The Bold: + door: Shortcut to The Bold + Orange Tower Fifth Floor: + room: Art Gallery + door: Exit + The Bearer (East): True + The Bearer (North): True + The Bearer (South): True + The Bearer (West): True + Roof: True + panels: + Achievement: + id: Countdown Panels/Panel_bearer_bearer + check: True + tag: forbid + required_panel: + - panel: PART + - panel: HEART + - room: Cross Tower (East) + panel: WINTER + - room: The Bearer (East) + panel: PEACE + - room: Cross Tower (North) + panel: NORTH + - room: The Bearer (North) + panel: SILENT (1) + - room: The Bearer (North) + panel: SILENT (2) + - room: The Bearer (North) + panel: SPACE + - room: The Bearer (North) + panel: WARTS + - room: Cross Tower (South) + panel: FIRE + - room: The Bearer (South) + panel: TENT + - room: The Bearer (South) + panel: BOWL + - room: Cross Tower (West) + panel: DIAMONDS + - room: The Bearer (West) + panel: SNOW + - room: The Bearer (West) + panel: SMILE + - room: Bearer Side Area + panel: SHORTCUT + - room: Bearer Side Area + panel: POTS + achievement: The Bearer + MIDDLE: + id: Shuffle Room/Panel_middle_middle_2 + tag: midwhite + FARTHER: + id: Backside Room/Panel_farther_far + colors: red + tag: midred + BACKSIDE: + id: Backside Room/Panel_backside_5 + tag: midwhite + required_door: + door: Backside Door + PART: + id: Cross Room/Panel_part_rap + colors: + - red + - yellow + tag: mid red yellow + required_panel: + room: The Bearer (East) + panel: PEACE + HEART: + id: Cross Room/Panel_heart_tar + colors: + - red + - yellow + tag: mid red yellow + doors: + Shortcut to The Bold: + id: Red Blue Purple Room Area Doors/Door_middle_middle + panels: + - MIDDLE + Backside Door: + id: Red Blue Purple Room Area Doors/Door_locked_knocked2 # yeah... + group: Backside Doors + panels: + - FARTHER + East Entrance: + event: True + panels: + - HEART + The Bearer (East): + entrances: + Cross Tower (East): True + Bearer Side Area: + door: Side Area Access + Roof: True + panels: + SIX: + id: Backside Room/Panel_six_six_5 + tag: midwhite + colors: + - red + - yellow + required_door: + room: Number Hunt + door: Sixes + PEACE: + id: Cross Room/Panel_peace_ape + colors: + - red + - yellow + tag: mid red yellow + doors: + North Entrance: + event: True + panels: + - room: The Bearer + panel: PART + Side Area Access: + event: True + panels: + - room: The Bearer (North) + panel: SPACE + The Bearer (North): + entrances: + Cross Tower (East): True + Roof: True + panels: + SILENT (1): + id: Cross Room/Panel_silent_list + colors: + - red + - yellow + tag: mid red yellow + required_panel: + room: The Bearer (West) + panel: SMILE + SILENT (2): + id: Cross Room/Panel_silent_list_2 + colors: + - red + - yellow + tag: mid yellow red + required_panel: + room: The Bearer (West) + panel: SMILE + SPACE: + id: Cross Room/Panel_space_cape + colors: + - red + - yellow + tag: mid red yellow + WARTS: + id: Cross Room/Panel_warts_star + colors: + - red + - yellow + tag: mid red yellow + required_panel: + room: The Bearer (West) + panel: SNOW + doors: + South Entrance: + event: True + panels: + - room: Bearer Side Area + panel: POTS + The Bearer (South): + entrances: + Cross Tower (North): True + Bearer Side Area: + door: Side Area Shortcut + Roof: True + panels: + SIX: + id: Backside Room/Panel_six_six_6 + tag: midwhite + colors: + - red + - yellow + required_door: + room: Number Hunt + door: Sixes + TENT: + id: Cross Room/Panel_tent_net + colors: + - red + - yellow + tag: mid red yellow + BOWL: + id: Cross Room/Panel_bowl_low + colors: + - red + - yellow + tag: mid red yellow + required_panel: + panel: TENT + doors: + Side Area Shortcut: + event: True + panels: + - room: The Bearer (North) + panel: SILENT (1) + The Bearer (West): + entrances: + Cross Tower (West): True + Bearer Side Area: + door: Side Area Shortcut + Roof: True + panels: + SNOW: + id: Cross Room/Panel_smile_lime + colors: + - red + - yellow + tag: mid yellow red + SMILE: + id: Cross Room/Panel_snow_won + colors: + - red + - yellow + tag: mid red yellow + required_panel: + room: The Bearer (North) + panel: WARTS + doors: + Side Area Shortcut: + event: True + panels: + - room: Cross Tower (East) + panel: WINTER + - room: Cross Tower (North) + panel: NORTH + - room: Cross Tower (South) + panel: FIRE + - room: Cross Tower (West) + panel: DIAMONDS + Bearer Side Area: + entrances: + The Bearer (East): + room: The Bearer (East) + door: Side Area Access + The Bearer (South): + room: The Bearer (South) + door: Side Area Shortcut + The Bearer (West): + room: The Bearer (West) + door: Side Area Shortcut + Orange Tower Third Floor: + door: Shortcut to Tower + Roof: True + panels: + SHORTCUT: + id: Cross Room/Panel_shortcut_shortcut + tag: midwhite + POTS: + id: Cross Room/Panel_pots_top + colors: + - red + - yellow + tag: mid yellow red + doors: + Shortcut to Tower: + id: Cross Room Doors/Door_shortcut + item_name: The Bearer - Shortcut to Tower + location_name: The Bearer - SHORTCUT + panels: + - SHORTCUT + West Entrance: + event: True + panels: + - room: The Bearer (South) + panel: BOWL + Cross Tower (East): + entrances: + The Bearer: + room: The Bearer + door: East Entrance + Roof: True + panels: + WINTER: + id: Cross Room/Panel_winter_winter + colors: blue + tag: forbid + required_panel: + room: The Bearer (North) + panel: SPACE + required_room: Orange Tower Fifth Floor + Cross Tower (North): + entrances: + The Bearer (East): + room: The Bearer (East) + door: North Entrance + Roof: True + panels: + NORTH: + id: Cross Room/Panel_north_north + colors: blue + tag: forbid + required_panel: + room: The Bearer (West) + panel: SMILE + required_room: Outside The Bold + Cross Tower (South): + entrances: # No roof access + The Bearer (North): + room: The Bearer (North) + door: South Entrance + panels: + FIRE: + id: Cross Room/Panel_fire_fire + colors: blue + tag: forbid + required_panel: + room: The Bearer (North) + panel: SILENT (1) + required_room: Elements Area + Cross Tower (West): + entrances: + Bearer Side Area: + room: Bearer Side Area + door: West Entrance + Roof: True + panels: + DIAMONDS: + id: Cross Room/Panel_diamonds_diamonds + colors: blue + tag: forbid + required_panel: + room: The Bearer (North) + panel: WARTS + required_room: Suits Area + The Steady (Rose): + entrances: + Outside The Bold: + room: Outside The Bold + door: Steady Entrance + The Steady (Lilac): + room: The Steady + door: Reveal + The Steady (Ruby): + door: Forward Exit + The Steady (Carnation): + door: Right Exit + panels: + SOAR: + id: Rock Room/Panel_soar_rose + colors: black + tag: topblack + doors: + Forward Exit: + event: True + panels: + - SOAR + Right Exit: + event: True + panels: + - room: The Steady (Lilac) + panel: LIE LACK + The Steady (Ruby): + entrances: + The Steady (Rose): + room: The Steady (Rose) + door: Forward Exit + The Steady (Amethyst): + room: The Steady + door: Reveal + The Steady (Cherry): + door: Forward Exit + The Steady (Amber): + door: Right Exit + panels: + BURY: + id: Rock Room/Panel_bury_ruby + colors: yellow + tag: midyellow + doors: + Forward Exit: + event: True + panels: + - room: The Steady (Lime) + panel: LIMELIGHT + Right Exit: + event: True + panels: + - room: The Steady (Carnation) + panel: INCARNATION + The Steady (Carnation): + entrances: + The Steady (Rose): + room: The Steady (Rose) + door: Right Exit + Outside The Bold: + room: The Steady + door: Reveal + The Steady (Amber): + room: The Steady + door: Reveal + The Steady (Sunflower): + door: Right Exit + panels: + INCARNATION: + id: Rock Room/Panel_incarnation_carnation + colors: red + tag: midred + doors: + Right Exit: + event: True + panels: + - room: The Steady (Amethyst) + panel: PACIFIST + The Steady (Sunflower): + entrances: + The Steady (Carnation): + room: The Steady (Carnation) + door: Right Exit + The Steady (Topaz): + room: The Steady (Topaz) + door: Back Exit + panels: + SUN: + id: Rock Room/Panel_sun_sunflower + colors: blue + tag: midblue + doors: + Back Exit: + event: True + panels: + - SUN + The Steady (Plum): + entrances: + The Steady (Amethyst): + room: The Steady + door: Reveal + The Steady (Blueberry): + room: The Steady + door: Reveal + The Steady (Cherry): + room: The Steady (Cherry) + door: Left Exit + panels: + LUMP: + id: Rock Room/Panel_lump_plum + colors: yellow + tag: midyellow + The Steady (Lime): + entrances: + The Steady (Sunflower): True + The Steady (Emerald): + room: The Steady + door: Reveal + The Steady (Blueberry): + door: Right Exit + panels: + LIMELIGHT: + id: Rock Room/Panel_limelight_lime + colors: red + tag: midred + doors: + Right Exit: + event: True + panels: + - room: The Steady (Amber) + panel: ANTECHAMBER + paintings: + - id: pencil_painting5 + orientation: south + The Steady (Lemon): + entrances: + The Steady (Emerald): True + The Steady (Orange): + room: The Steady + door: Reveal + The Steady (Topaz): + door: Back Exit + panels: + MELON: + id: Rock Room/Panel_melon_lemon + colors: yellow + tag: midyellow + doors: + Back Exit: + event: True + panels: + - MELON + paintings: + - id: pencil_painting4 + orientation: south + The Steady (Topaz): + entrances: + The Steady (Lemon): + room: The Steady (Lemon) + door: Back Exit + The Steady (Amber): + room: The Steady + door: Reveal + The Steady (Sunflower): + door: Back Exit + panels: + TOP: + id: Rock Room/Panel_top_topaz + colors: blue + tag: midblue + MASTERY: + id: Master Room/Panel_mastery_mastery2 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + doors: + Back Exit: + event: True + panels: + - TOP + The Steady (Orange): + entrances: + The Steady (Cherry): + room: The Steady + door: Reveal + The Steady (Lemon): + room: The Steady + door: Reveal + The Steady (Amber): + room: The Steady (Amber) + door: Forward Exit + panels: + BLUE: + id: Rock Room/Panel_blue_orange + colors: black + tag: botblack + The Steady (Sapphire): + entrances: + The Steady (Emerald): + door: Left Exit + The Steady (Blueberry): + room: The Steady + door: Reveal + The Steady (Amethyst): + room: The Steady (Amethyst) + door: Left Exit + panels: + SAP: + id: Rock Room/Panel_sap_sapphire + colors: blue + tag: midblue + doors: + Left Exit: + event: True + panels: + - room: The Steady (Plum) + panel: LUMP + - room: The Steady (Orange) + panel: BLUE + The Steady (Blueberry): + entrances: + The Steady (Lime): + room: The Steady (Lime) + door: Right Exit + The Steady (Sapphire): + room: The Steady + door: Reveal + The Steady (Plum): + room: The Steady + door: Reveal + panels: + BLUE: + id: Rock Room/Panel_blue_blueberry + colors: blue + tag: midblue + The Steady (Amber): + entrances: + The Steady (Ruby): + room: The Steady (Ruby) + door: Right Exit + The Steady (Carnation): + room: The Steady + door: Reveal + The Steady (Orange): + door: Forward Exit + The Steady (Topaz): + room: The Steady + door: Reveal + panels: + ANTECHAMBER: + id: Rock Room/Panel_antechamber_amber + colors: red + tag: midred + doors: + Forward Exit: + event: True + panels: + - room: The Steady (Blueberry) + panel: BLUE + The Steady (Emerald): + entrances: + The Steady (Sapphire): + room: The Steady (Sapphire) + door: Left Exit + The Steady (Lime): + room: The Steady + door: Reveal + panels: + HERALD: + id: Rock Room/Panel_herald_emerald + colors: purple + tag: midpurp + The Steady (Amethyst): + entrances: + The Steady (Lilac): + room: The Steady (Lilac) + door: Forward Exit + The Steady (Sapphire): + door: Left Exit + The Steady (Plum): + room: The Steady + door: Reveal + The Steady (Ruby): + room: The Steady + door: Reveal + panels: + PACIFIST: + id: Rock Room/Panel_thistle_amethyst + colors: purple + tag: toppurp + doors: + Left Exit: + event: True + panels: + - room: The Steady (Sunflower) + panel: SUN + The Steady (Lilac): + entrances: + Outside The Bold: + room: Outside The Bold + door: Lilac Entrance + The Steady (Amethyst): + door: Forward Exit + The Steady (Rose): + room: The Steady + door: Reveal + panels: + LIE LACK: + id: Rock Room/Panel_lielack_lilac + tag: topwhite + doors: + Forward Exit: + event: True + panels: + - room: The Steady (Ruby) + panel: BURY + The Steady (Cherry): + entrances: + The Steady (Plum): + door: Left Exit + The Steady (Orange): + room: The Steady + door: Reveal + The Steady (Ruby): + room: The Steady (Ruby) + door: Forward Exit + panels: + HAIRY: + id: Rock Room/Panel_hairy_cherry + colors: blue + tag: topblue + doors: + Left Exit: + event: True + panels: + - room: The Steady (Sapphire) + panel: SAP + The Steady: + entrances: + The Steady (Sunflower): + room: The Steady (Sunflower) + door: Back Exit + panels: + Achievement: + id: Countdown Panels/Panel_steady_steady + required_panel: + - room: The Steady (Rose) + panel: SOAR + - room: The Steady (Carnation) + panel: INCARNATION + - room: The Steady (Sunflower) + panel: SUN + - room: The Steady (Ruby) + panel: BURY + - room: The Steady (Plum) + panel: LUMP + - room: The Steady (Lime) + panel: LIMELIGHT + - room: The Steady (Lemon) + panel: MELON + - room: The Steady (Topaz) + panel: TOP + - room: The Steady (Orange) + panel: BLUE + - room: The Steady (Sapphire) + panel: SAP + - room: The Steady (Blueberry) + panel: BLUE + - room: The Steady (Amber) + panel: ANTECHAMBER + - room: The Steady (Emerald) + panel: HERALD + - room: The Steady (Amethyst) + panel: PACIFIST + - room: The Steady (Lilac) + panel: LIE LACK + - room: The Steady (Cherry) + panel: HAIRY + tag: forbid + check: True + achievement: The Steady + doors: + Reveal: + event: True + panels: + - Achievement + Knight Night (Outer Ring): + entrances: + Hidden Room: + room: Hidden Room + door: Knight Night Entrance + Knight Night Exit: True + panels: + NIGHT: + id: Appendix Room/Panel_night_knight + colors: blue + tag: homophone midblue + copy_to_sign: sign7 + KNIGHT: + id: Appendix Room/Panel_knight_night + colors: red + tag: homophone midred + copy_to_sign: sign8 + BEE: + id: Appendix Room/Panel_bee_be + colors: red + tag: homophone midred + copy_to_sign: sign9 + NEW: + id: Appendix Room/Panel_new_knew + colors: blue + tag: homophone midblue + copy_to_sign: sign11 + FORE: + id: Appendix Room/Panel_fore_for + colors: red + tag: homophone midred + copy_to_sign: sign10 + TRUSTED (1): + id: Appendix Room/Panel_trusted_trust + colors: red + tag: midred + required_panel: + room: Knight Night (Right Lower Segment) + panel: BEFORE + TRUSTED (2): + id: Appendix Room/Panel_trusted_rusted + colors: red + tag: midred + required_panel: + room: Knight Night (Right Lower Segment) + panel: BEFORE + ENCRUSTED: + id: Appendix Room/Panel_encrusted_rust + colors: red + tag: midred + required_panel: + - panel: TRUSTED (1) + - panel: TRUSTED (2) + ADJUST (1): + id: Appendix Room/Panel_adjust_readjust + colors: blue + tag: midblue and phone + required_panel: + room: Knight Night (Right Lower Segment) + panel: BE + ADJUST (2): + id: Appendix Room/Panel_adjust_adjusted + colors: blue + tag: midblue and phone + required_panel: + room: Knight Night (Right Lower Segment) + panel: BE + RIGHT: + id: Appendix Room/Panel_right_right + tag: midwhite + required_panel: + room: Knight Night (Right Lower Segment) + panel: ADJUST + TRUST: + id: Appendix Room/Panel_trust_crust + colors: + - red + - blue + tag: mid red blue + required_panel: + - room: Knight Night (Right Lower Segment) + panel: ADJUST + - room: Knight Night (Right Lower Segment) + panel: LEFT + doors: + Fore Door: + event: True + panels: + - FORE + New Door: + event: True + panels: + - NEW + To End: + event: True + panels: + - RIGHT + - room: Knight Night (Right Lower Segment) + panel: LEFT + Knight Night (Right Upper Segment): + entrances: + Knight Night Exit: True + Knight Night (Outer Ring): + room: Knight Night (Outer Ring) + door: Fore Door + Knight Night (Right Lower Segment): + door: Segment Door + panels: + RUST (1): + id: Appendix Room/Panel_rust_trust + colors: blue + tag: midblue + required_panel: + room: Knight Night (Outer Ring) + panel: BEE + RUST (2): + id: Appendix Room/Panel_rust_crust + colors: blue + tag: midblue + required_panel: + room: Knight Night (Outer Ring) + panel: BEE + doors: + Segment Door: + event: True + panels: + - RUST (2) + - room: Knight Night (Right Lower Segment) + panel: BEFORE + Knight Night (Right Lower Segment): + entrances: + Knight Night Exit: True + Knight Night (Right Upper Segment): + room: Knight Night (Right Upper Segment) + door: Segment Door + Knight Night (Outer Ring): + room: Knight Night (Outer Ring) + door: New Door + panels: + ADJUST: + id: Appendix Room/Panel_adjust_readjusted + colors: blue + tag: midblue + required_panel: + - room: Knight Night (Outer Ring) + panel: ADJUST (1) + - room: Knight Night (Outer Ring) + panel: ADJUST (2) + BEFORE: + id: Appendix Room/Panel_before_fore + colors: red + tag: midred and phone + required_panel: + room: Knight Night (Right Upper Segment) + panel: RUST (1) + BE: + id: Appendix Room/Panel_be_before + colors: blue + tag: midblue and phone + required_panel: + room: Knight Night (Right Upper Segment) + panel: RUST (1) + LEFT: + id: Appendix Room/Panel_left_left + tag: midwhite + required_panel: + room: Knight Night (Outer Ring) + panel: ENCRUSTED + TRUST: + id: Appendix Room/Panel_trust_crust_2 + colors: purple + tag: midpurp + required_panel: + - room: Knight Night (Outer Ring) + panel: ENCRUSTED + - room: Knight Night (Outer Ring) + panel: RIGHT + Knight Night (Final): + entrances: + Knight Night Exit: True + Knight Night (Outer Ring): + room: Knight Night (Outer Ring) + door: To End + Knight Night (Right Upper Segment): + room: Knight Night (Outer Ring) + door: To End + panels: + TRUSTED: + id: Appendix Room/Panel_trusted_readjusted + colors: purple + tag: midpurp + doors: + Exit: + id: + - Appendix Room Area Doors/Door_trusted_readjusted + - Appendix Room Area Doors/Door_trusted_readjusted2 + - Appendix Room Area Doors/Door_trusted_readjusted3 + - Appendix Room Area Doors/Door_trusted_readjusted4 + - Appendix Room Area Doors/Door_trusted_readjusted5 + - Appendix Room Area Doors/Door_trusted_readjusted6 + - Appendix Room Area Doors/Door_trusted_readjusted7 + - Appendix Room Area Doors/Door_trusted_readjusted8 + - Appendix Room Area Doors/Door_trusted_readjusted9 + - Appendix Room Area Doors/Door_trusted_readjusted10 + - Appendix Room Area Doors/Door_trusted_readjusted11 + - Appendix Room Area Doors/Door_trusted_readjusted12 + - Appendix Room Area Doors/Door_trusted_readjusted13 + include_reduce: True + location_name: Knight Night Room - TRUSTED + item_name: Knight Night Room - Exit + panels: + - TRUSTED + Knight Night Exit: + entrances: + Knight Night (Outer Ring): + room: Knight Night (Final) + door: Exit + Orange Tower Third Floor: + room: Knight Night (Final) + door: Exit + Outside The Initiated: + room: Knight Night (Final) + door: Exit + panels: + SEVEN (1): + id: Backside Room/Panel_seven_seven_7 + tag: midwhite + required_door: + - room: Number Hunt + door: Sevens + SEVEN (2): + id: Backside Room/Panel_seven_seven_3 + tag: midwhite + required_door: + - room: Number Hunt + door: Sevens + SEVEN (3): + id: Backside Room/Panel_seven_seven_4 + tag: midwhite + required_door: + - room: Number Hunt + door: Sevens + DEAD END: + id: Appendix Room/Panel_deadend_deadend + tag: midwhite + WARNER: + id: Appendix Room/Panel_warner_corner + colors: purple + tag: toppurp + The Artistic (Smiley): + entrances: + Dead End Area: + painting: True + Crossroads: + painting: True + Hot Crusts Area: + painting: True + Outside The Initiated: + painting: True + Directional Gallery: + painting: True + Number Hunt: + room: Number Hunt + door: Eights + painting: True + Art Gallery: + painting: True + The Eyes They See: + painting: True + The Artistic (Panda): + door: Door to Panda + The Artistic (Apple): + room: The Artistic (Apple) + door: Door to Smiley + Elements Area: + room: Hallway Room (4) + door: Exit + panels: + Achievement: + id: Countdown Panels/Panel_artistic_artistic + colors: + - red + - black + - yellow + - blue + tag: forbid + required_room: + - The Artistic (Panda) + - The Artistic (Apple) + - The Artistic (Lattice) + check: True + achievement: The Artistic + FINE: + id: Ceiling Room/Panel_yellow_top_5 + colors: + - yellow + - blue + tag: yellow top blue bot + subtag: top + link: yxu KNIFE + BLADE: + id: Ceiling Room/Panel_blue_bot_5 + colors: + - blue + - yellow + tag: yellow top blue bot + subtag: bot + link: yxu KNIFE + RED: + id: Ceiling Room/Panel_blue_top_6 + colors: + - blue + - yellow + tag: blue top yellow mid + subtag: top + link: uyx BREAD + BEARD: + id: Ceiling Room/Panel_yellow_mid_6 + colors: + - yellow + - blue + tag: blue top yellow mid + subtag: mid + link: uyx BREAD + ICE: + id: Ceiling Room/Panel_blue_mid_7 + colors: + - blue + - yellow + tag: blue mid yellow bot + subtag: mid + link: xuy SPICE + ROOT: + id: Ceiling Room/Panel_yellow_bot_7 + colors: + - yellow + - blue + tag: blue mid yellow bot + subtag: bot + link: xuy SPICE + doors: + Door to Panda: + id: + - Ceiling Room Doors/Door_blue + - Ceiling Room Doors/Door_blue2 + location_name: The Artistic - Smiley and Panda + group: Artistic Doors + panels: + - FINE + - BLADE + - RED + - BEARD + - ICE + - ROOT + - room: The Artistic (Panda) + panel: EYE (Top) + - room: The Artistic (Panda) + panel: EYE (Bottom) + - room: The Artistic (Panda) + panel: LADYLIKE + - room: The Artistic (Panda) + panel: WATER + - room: The Artistic (Panda) + panel: OURS + - room: The Artistic (Panda) + panel: DAYS + - room: The Artistic (Panda) + panel: NIGHTTIME + - room: The Artistic (Panda) + panel: NIGHT + paintings: + - id: smile_painting_9 + orientation: north + exit_only: True + The Artistic (Panda): + entrances: + Orange Tower Sixth Floor: + painting: True + Outside The Agreeable: + painting: True + The Artistic (Smiley): + room: The Artistic (Smiley) + door: Door to Panda + The Artistic (Lattice): + door: Door to Lattice + panels: + EYE (Top): + id: Ceiling Room/Panel_blue_top_1 + colors: + - blue + - red + tag: blue top red bot + subtag: top + link: uxr IRIS + EYE (Bottom): + id: Ceiling Room/Panel_red_bot_1 + colors: + - red + - blue + tag: blue top red bot + subtag: bot + link: uxr IRIS + LADYLIKE: + id: Ceiling Room/Panel_red_mid_2 + colors: + - red + - blue + tag: red mid blue bot + subtag: mid + link: xru LAKE + WATER: + id: Ceiling Room/Panel_blue_bot_2 + colors: + - blue + - red + tag: red mid blue bot + subtag: bot + link: xru LAKE + OURS: + id: Ceiling Room/Panel_blue_mid_3 + colors: + - blue + - red + tag: blue mid red bot + subtag: mid + link: xur HOURS + DAYS: + id: Ceiling Room/Panel_red_bot_3 + colors: + - red + - blue + tag: blue mid red bot + subtag: bot + link: xur HOURS + NIGHTTIME: + id: Ceiling Room/Panel_red_top_4 + colors: + - red + - blue + tag: red top mid blue + subtag: top + link: rux KNIGHT + NIGHT: + id: Ceiling Room/Panel_blue_mid_4 + colors: + - blue + - red + tag: red top mid blue + subtag: mid + link: rux KNIGHT + doors: + Door to Lattice: + id: + - Ceiling Room Doors/Door_red + - Ceiling Room Doors/Door_red2 + location_name: The Artistic - Panda and Lattice + group: Artistic Doors + panels: + - EYE (Top) + - EYE (Bottom) + - LADYLIKE + - WATER + - OURS + - DAYS + - NIGHTTIME + - NIGHT + - room: The Artistic (Lattice) + panel: POSH + - room: The Artistic (Lattice) + panel: MALL + - room: The Artistic (Lattice) + panel: DEICIDE + - room: The Artistic (Lattice) + panel: WAVER + - room: The Artistic (Lattice) + panel: REPAID + - room: The Artistic (Lattice) + panel: BABY + - room: The Artistic (Lattice) + panel: LOBE + - room: The Artistic (Lattice) + panel: BOWELS + paintings: + - id: panda_painting_3 + exit_only: True + orientation: south + required_when_no_doors: True + The Artistic (Lattice): + entrances: + Directional Gallery: + painting: True + The Artistic (Panda): + room: The Artistic (Panda) + door: Door to Lattice + The Artistic (Apple): + door: Door to Apple + panels: + POSH: + id: Ceiling Room/Panel_black_top_12 + colors: + - black + - red + tag: black top red bot + subtag: top + link: bxr SHOP + MALL: + id: Ceiling Room/Panel_red_bot_12 + colors: + - red + - black + tag: black top red bot + subtag: bot + link: bxr SHOP + DEICIDE: + id: Ceiling Room/Panel_red_top_13 + colors: + - red + - black + tag: red top black bot + subtag: top + link: rxb DECIDE + WAVER: + id: Ceiling Room/Panel_black_bot_13 + colors: + - black + - red + tag: red top black bot + subtag: bot + link: rxb DECIDE + REPAID: + id: Ceiling Room/Panel_black_mid_14 + colors: + - black + - red + tag: black mid red bot + subtag: mid + link: xbr DIAPER + BABY: + id: Ceiling Room/Panel_red_bot_14 + colors: + - red + - black + tag: black mid red bot + subtag: bot + link: xbr DIAPER + LOBE: + id: Ceiling Room/Panel_black_top_15 + colors: + - black + - red + tag: black top red mid + subtag: top + link: brx BOWL + BOWELS: + id: Ceiling Room/Panel_red_mid_15 + colors: + - red + - black + tag: black top red mid + subtag: mid + link: brx BOWL + doors: + Door to Apple: + id: + - Ceiling Room Doors/Door_black + - Ceiling Room Doors/Door_black2 + location_name: The Artistic - Lattice and Apple + group: Artistic Doors + panels: + - POSH + - MALL + - DEICIDE + - WAVER + - REPAID + - BABY + - LOBE + - BOWELS + - room: The Artistic (Apple) + panel: SPRIG + - room: The Artistic (Apple) + panel: RELEASES + - room: The Artistic (Apple) + panel: MUCH + - room: The Artistic (Apple) + panel: FISH + - room: The Artistic (Apple) + panel: MASK + - room: The Artistic (Apple) + panel: HILL + - room: The Artistic (Apple) + panel: TINE + - room: The Artistic (Apple) + panel: THING + paintings: + - id: boxes_painting2 + orientation: south + exit_only: True + required_when_no_doors: True + The Artistic (Apple): + entrances: + Orange Tower Sixth Floor: + painting: True + Directional Gallery: + painting: True + The Artistic (Lattice): + room: The Artistic (Lattice) + door: Door to Apple + The Artistic (Smiley): + door: Door to Smiley + panels: + SPRIG: + id: Ceiling Room/Panel_yellow_mid_8 + colors: + - yellow + - black + tag: yellow mid black bot + subtag: mid + link: xyb GRIPS + RELEASES: + id: Ceiling Room/Panel_black_bot_8 + colors: + - black + - yellow + tag: yellow mid black bot + subtag: bot + link: xyb GRIPS + MUCH: + id: Ceiling Room/Panel_black_top_9 + colors: + - black + - yellow + tag: black top yellow bot + subtag: top + link: bxy CHUM + FISH: + id: Ceiling Room/Panel_yellow_bot_9 + colors: + - yellow + - black + tag: black top yellow bot + subtag: bot + link: bxy CHUM + MASK: + id: Ceiling Room/Panel_yellow_top_10 + colors: + - yellow + - black + tag: yellow top black bot + subtag: top + link: yxb CHASM + HILL: + id: Ceiling Room/Panel_black_bot_10 + colors: + - black + - yellow + tag: yellow top black bot + subtag: bot + link: yxb CHASM + TINE: + id: Ceiling Room/Panel_black_top_11 + colors: + - black + - yellow + tag: black top yellow mid + subtag: top + link: byx NIGHT + THING: + id: Ceiling Room/Panel_yellow_mid_11 + colors: + - yellow + - black + tag: black top yellow mid + subtag: mid + link: byx NIGHT + doors: + Door to Smiley: + id: + - Ceiling Room Doors/Door_yellow + - Ceiling Room Doors/Door_yellow2 + location_name: The Artistic - Apple and Smiley + group: Artistic Doors + panels: + - SPRIG + - RELEASES + - MUCH + - FISH + - MASK + - HILL + - TINE + - THING + - room: The Artistic (Smiley) + panel: FINE + - room: The Artistic (Smiley) + panel: BLADE + - room: The Artistic (Smiley) + panel: RED + - room: The Artistic (Smiley) + panel: BEARD + - room: The Artistic (Smiley) + panel: ICE + - room: The Artistic (Smiley) + panel: ROOT + paintings: + - id: cherry_painting3 + orientation: north + exit_only: True + required_when_no_doors: True + The Artistic (Hint Room): + entrances: + The Artistic (Lattice): + room: The Artistic (Lattice) + door: Door to Apple + panels: + THEME: + id: Ceiling Room/Panel_answer_1 + colors: red + tag: midred + PAINTS: + id: Ceiling Room/Panel_answer_2 + colors: yellow + tag: botyellow + I: + id: Ceiling Room/Panel_answer_3 + colors: blue + tag: midblue + KIT: + id: Ceiling Room/Panel_answer_4 + colors: black + tag: topblack + The Discerning: + entrances: + Crossroads: + room: Crossroads + door: Discerning Entrance + panels: + Achievement: + id: Countdown Panels/Panel_discerning_scramble + colors: yellow + tag: forbid + check: True + achievement: The Discerning + HITS: + id: Sun Room/Panel_hits_this + colors: yellow + tag: midyellow + WARRED: + id: Sun Room/Panel_warred_drawer + colors: yellow + tag: double midyellow + subtag: left + link: ana DRAWER + REDRAW: + id: Sun Room/Panel_redraw_drawer + colors: yellow + tag: double midyellow + subtag: right + link: ana DRAWER + ADDER: + id: Sun Room/Panel_adder_dread + colors: yellow + tag: midyellow + LAUGHTERS: + id: Sun Room/Panel_laughters_slaughter + colors: yellow + tag: midyellow + STONE: + id: Sun Room/Panel_stone_notes + colors: yellow + tag: double midyellow + subtag: left + link: ana NOTES + ONSET: + id: Sun Room/Panel_onset_notes + colors: yellow + tag: double midyellow + subtag: right + link: ana NOTES + RAT: + id: Sun Room/Panel_rat_art + colors: yellow + tag: midyellow + DUSTY: + id: Sun Room/Panel_dusty_study + colors: yellow + tag: midyellow + ARTS: + id: Sun Room/Panel_arts_star + colors: yellow + tag: double midyellow + subtag: left + link: ana STAR + TSAR: + id: Sun Room/Panel_tsar_star + colors: yellow + tag: double midyellow + subtag: right + link: ana STAR + STATE: + id: Sun Room/Panel_state_taste + colors: yellow + tag: midyellow + REACT: + id: Sun Room/Panel_react_trace + colors: yellow + tag: midyellow + DEAR: + id: Sun Room/Panel_dear_read + colors: yellow + tag: double midyellow + subtag: left + link: ana READ + DARE: + id: Sun Room/Panel_dare_read + colors: yellow + tag: double midyellow + subtag: right + link: ana READ + SEAM: + id: Sun Room/Panel_seam_same + colors: yellow + tag: midyellow + The Eyes They See: + entrances: + Crossroads: + room: Crossroads + door: Eye Wall + painting: True + Wondrous Lobby: + door: Exit + Directional Gallery: True + panels: + NEAR: + id: Shuffle Room/Panel_near_near + tag: midwhite + EIGHT: + id: Backside Room/Panel_eight_eight_4 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + doors: + Exit: + id: Count Up Room Area Doors/Door_near_near + group: Crossroads Doors + panels: + - NEAR + paintings: + - id: eye_painting_2 + orientation: west + - id: smile_painting_2 + orientation: north + Far Window: + entrances: + Crossroads: + room: Crossroads + door: Eye Wall + The Eyes They See: True + panels: + FAR: + id: Shuffle Room/Panel_far_far + tag: midwhite + Wondrous Lobby: + entrances: + Directional Gallery: True + The Eyes They See: + room: The Eyes They See + door: Exit + paintings: + - id: arrows_painting_5 + orientation: east + Outside The Wondrous: + entrances: + Wondrous Lobby: True + The Wondrous (Doorknob): + door: Wondrous Entrance + The Wondrous (Window): True + panels: + SHRINK: + id: Wonderland Room/Panel_shrink_shrink + tag: midwhite + doors: + Wondrous Entrance: + id: Red Blue Purple Room Area Doors/Door_wonderland + item_name: The Wondrous - Entrance + panels: + - SHRINK + The Wondrous (Doorknob): + entrances: + Outside The Wondrous: + room: Outside The Wondrous + door: Wondrous Entrance + Starting Room: + door: Painting Shortcut + painting: True + The Wondrous (Chandelier): + painting: True + The Wondrous (Table): True # There is a way that doesn't use the painting + doors: + Painting Shortcut: + painting_id: + - symmetry_painting_a_starter + - arrows_painting2 + skip_location: True + item_name: Starting Room - Symmetry Painting + panels: + - room: Outside The Wondrous + panel: SHRINK + paintings: + - id: symmetry_painting_a_1 + orientation: east + exit_only: True + - id: symmetry_painting_b_1 + orientation: south + The Wondrous (Bookcase): + entrances: + The Wondrous (Doorknob): True + panels: + CASE: + id: Wonderland Room/Panel_case_bookcase + colors: blue + tag: midblue + paintings: + - id: symmetry_painting_a_3 + orientation: west + exit_only: True + - id: symmetry_painting_b_3 + disable: True + The Wondrous (Chandelier): + entrances: + The Wondrous (Bookcase): True + panels: + CANDLE HEIR: + id: Wonderland Room/Panel_candleheir_chandelier + colors: yellow + tag: midyellow + paintings: + - id: symmetry_painting_a_5 + orientation: east + - id: symmetry_painting_a_5 + disable: True + The Wondrous (Window): + entrances: + The Wondrous (Bookcase): True + panels: + GLASS: + id: Wonderland Room/Panel_glass_window + colors: brown + tag: botbrown + paintings: + - id: symmetry_painting_b_4 + orientation: north + exit_only: True + - id: symmetry_painting_a_4 + disable: True + The Wondrous (Table): + entrances: + The Wondrous (Doorknob): + painting: True + The Wondrous: + painting: True + panels: + WOOD: + id: Wonderland Room/Panel_wood_table + colors: brown + tag: botbrown + BROOK NOD: + # This panel, while physically being in the first room, is facing upward + # and is only really solvable while standing on the windowsill, which is + # a location you can only get to from Table. + id: Wonderland Room/Panel_brooknod_doorknob + colors: yellow + tag: midyellow + paintings: + - id: symmetry_painting_a_2 + orientation: west + - id: symmetry_painting_b_2 + orientation: south + exit_only: True + required: True + The Wondrous: + entrances: + The Wondrous (Table): True + Arrow Garden: + door: Exit + panels: + FIREPLACE: + id: Wonderland Room/Panel_fireplace_fire + colors: red + tag: midred + Achievement: + id: Countdown Panels/Panel_wondrous_wondrous + required_panel: + - panel: FIREPLACE + - room: The Wondrous (Table) + panel: BROOK NOD + - room: The Wondrous (Bookcase) + panel: CASE + - room: The Wondrous (Chandelier) + panel: CANDLE HEIR + - room: The Wondrous (Window) + panel: GLASS + - room: The Wondrous (Table) + panel: WOOD + tag: forbid + achievement: The Wondrous + doors: + Exit: + id: Red Blue Purple Room Area Doors/Door_wonderland_exit + painting_id: arrows_painting_9 + include_reduce: True + panels: + - Achievement + paintings: + - id: arrows_painting_9 + enter_only: True + orientation: south + move: True + required_door: + door: Exit + - id: symmetry_painting_a_6 + orientation: west + exit_only: True + - id: symmetry_painting_b_6 + orientation: north + Arrow Garden: + entrances: + The Wondrous: + room: The Wondrous + door: Exit + Roof: True + panels: + MASTERY: + id: Master Room/Panel_mastery_mastery4 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + SHARP: + id: Open Areas/Panel_rainy_rainbow2 + tag: midwhite + paintings: + - id: flower_painting_6 + orientation: south + Hallway Room (2): + entrances: + Outside The Agreeable: + room: Outside The Agreeable + door: Hallway Door + Elements Area: True + panels: + WISE: + id: Hallway Room/Panel_counterclockwise_1 + colors: blue + tag: quad mid blue + link: qmb COUNTERCLOCKWISE + CLOCK: + id: Hallway Room/Panel_counterclockwise_2 + colors: blue + tag: quad mid blue + link: qmb COUNTERCLOCKWISE + ER: + id: Hallway Room/Panel_counterclockwise_3 + colors: blue + tag: quad mid blue + link: qmb COUNTERCLOCKWISE + COUNT: + id: Hallway Room/Panel_counterclockwise_4 + colors: blue + tag: quad mid blue + link: qmb COUNTERCLOCKWISE + doors: + Exit: + id: Red Blue Purple Room Area Doors/Door_room_3 + location_name: Hallway Room - Second Room + group: Hallway Room Doors + panels: + - WISE + - CLOCK + - ER + - COUNT + Hallway Room (3): + entrances: + Hallway Room (2): + room: Hallway Room (2) + door: Exit + # No entrance from Elements Area. The winding hallway does not connect. + panels: + TRANCE: + id: Hallway Room/Panel_transformation_1 + colors: blue + tag: quad top blue + link: qtb TRANSFORMATION + FORM: + id: Hallway Room/Panel_transformation_2 + colors: blue + tag: quad top blue + link: qtb TRANSFORMATION + A: + id: Hallway Room/Panel_transformation_3 + colors: blue + tag: quad top blue + link: qtb TRANSFORMATION + SHUN: + id: Hallway Room/Panel_transformation_4 + colors: blue + tag: quad top blue + link: qtb TRANSFORMATION + doors: + Exit: + id: Red Blue Purple Room Area Doors/Door_room_4 + location_name: Hallway Room - Third Room + group: Hallway Room Doors + panels: + - TRANCE + - FORM + - A + - SHUN + Hallway Room (4): + entrances: + Hallway Room (3): + room: Hallway Room (3) + door: Exit + Elements Area: True + panels: + WHEEL: + id: Hallway Room/Panel_room_5 + colors: blue + tag: full stack blue + doors: + Exit: + id: + - Red Blue Purple Room Area Doors/Door_room_5 + - Red Blue Purple Room Area Doors/Door_room_6 # this is the connection to The Artistic + group: Hallway Room Doors + location_name: Hallway Room - Fourth Room + panels: + - WHEEL + include_reduce: True + Elements Area: + entrances: + Roof: True + Hallway Room (4): + room: Hallway Room (4) + door: Exit + The Artistic (Smiley): + room: Hallway Room (4) + door: Exit + panels: + A: + id: Strand Room/Panel_a_strands + colors: blue + tag: forbid + NINE: + id: Backside Room/Panel_nine_nine_7 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + UNDISTRACTED: + id: Open Areas/Panel_undistracted + check: True + exclude_reduce: True + tag: midwhite + MASTERY: + id: Master Room/Panel_mastery_mastery13 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + EARTH: + id: Cross Room/Panel_earth_earth + tag: midwhite + WATER: + id: Cross Room/Panel_water_water + tag: midwhite + AIR: + id: Cross Room/Panel_air_air + tag: midwhite + paintings: + - id: south_afar + orientation: south + Outside The Wanderer: + entrances: + Orange Tower First Floor: + door: Tower Entrance + Rhyme Room (Cross): + room: Rhyme Room (Cross) + door: Exit + Roof: True + panels: + WANDERLUST: + id: Tower Room/Panel_wanderlust_1234567890 + colors: orange + tag: midorange + doors: + Wanderer Entrance: + id: Tower Room Area Doors/Door_wanderer_entrance + item_name: The Wanderer - Entrance + panels: + - WANDERLUST + Tower Entrance: + id: Tower Room Area Doors/Door_wanderlust_start + skip_location: True + panels: + - room: The Wanderer + panel: Achievement + The Wanderer: + entrances: + Outside The Wanderer: + room: Outside The Wanderer + door: Wanderer Entrance + panels: + Achievement: + id: Countdown Panels/Panel_1234567890_wanderlust + colors: orange + check: True + tag: forbid + achievement: The Wanderer + "7890": + id: Orange Room/Panel_lust + colors: orange + tag: midorange + "6524": + id: Orange Room/Panel_read + colors: orange + tag: midorange + "951": + id: Orange Room/Panel_sew + colors: orange + tag: midorange + "4524": + id: Orange Room/Panel_dead + colors: orange + tag: midorange + LEARN: + id: Orange Room/Panel_learn + colors: orange + tag: midorange + DUST: + id: Orange Room/Panel_dust + colors: orange + tag: midorange + STAR: + id: Orange Room/Panel_star + colors: orange + tag: midorange + WANDER: + id: Orange Room/Panel_wander + colors: orange + tag: midorange + Art Gallery: + entrances: + Orange Tower Third Floor: True + Art Gallery (Second Floor): True + Art Gallery (Third Floor): True + Art Gallery (Fourth Floor): True + Orange Tower Fifth Floor: + door: Exit + panels: + EIGHT: + id: Backside Room/Panel_eight_eight_6 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + EON: + id: Painting Room/Panel_eon_one + colors: yellow + tag: midyellow + TRUSTWORTHY: + id: Painting Room/Panel_to_two + colors: red + tag: midred + FREE: + id: Painting Room/Panel_free_three + colors: purple + tag: midpurp + OUR: + id: Painting Room/Panel_our_four + colors: blue + tag: midblue + ONE ROAD MANY TURNS: + id: Painting Room/Panel_order_onepathmanyturns + tag: forbid + colors: + - yellow + - blue + - gray + - brown + - orange + required_door: + door: Fifth Floor + doors: + Second Floor: + painting_id: + - scenery_painting_2b + - scenery_painting_2c + skip_location: True + panels: + - EON + First Floor Puzzles: + skip_item: True + location_name: Art Gallery - First Floor Puzzles + panels: + - EON + - TRUSTWORTHY + - FREE + - OUR + Third Floor: + painting_id: + - scenery_painting_3b + - scenery_painting_3c + skip_location: True + panels: + - room: Art Gallery (Second Floor) + panel: PATH + Fourth Floor: + painting_id: + - scenery_painting_4b + - scenery_painting_4c + skip_location: True + panels: + - room: Art Gallery (Third Floor) + panel: ANY + Fifth Floor: + id: Tower Room Area Doors/Door_painting_backroom + painting_id: + - scenery_painting_5b + - scenery_painting_5c + skip_location: True + panels: + - room: Art Gallery (Fourth Floor) + panel: SEND - USE + Exit: + id: Tower Room Area Doors/Door_painting_exit + include_reduce: True + panels: + - ONE ROAD MANY TURNS + paintings: + - id: smile_painting_3 + orientation: west + - id: flower_painting_2 + orientation: east + - id: scenery_painting_0a + orientation: north + - id: map_painting + orientation: east + - id: fruitbowl_painting4 + orientation: south + progression: + Progressive Art Gallery: + - Second Floor + - Third Floor + - Fourth Floor + - Fifth Floor + - Exit + Art Gallery (Second Floor): + entrances: + Art Gallery: + room: Art Gallery + door: Second Floor + panels: + HOUSE: + id: Painting Room/Panel_house_neighborhood + colors: blue + tag: botblue + PATH: + id: Painting Room/Panel_path_road + colors: brown + tag: botbrown + PARK: + id: Painting Room/Panel_park_drive + colors: black + tag: botblack + CARRIAGE: + id: Painting Room/Panel_carriage_horse + colors: red + tag: botred + doors: + Puzzles: + skip_item: True + location_name: Art Gallery - Second Floor Puzzles + panels: + - HOUSE + - PATH + - PARK + - CARRIAGE + Art Gallery (Third Floor): + entrances: + Art Gallery: + room: Art Gallery + door: Third Floor + panels: + AN: + id: Painting Room/Panel_an_many + colors: blue + tag: midblue + MAY: + id: Painting Room/Panel_may_many + colors: blue + tag: midblue + ANY: + id: Painting Room/Panel_any_many + colors: blue + tag: midblue + MAN: + id: Painting Room/Panel_man_many + colors: blue + tag: midblue + doors: + Puzzles: + skip_item: True + location_name: Art Gallery - Third Floor Puzzles + panels: + - AN + - MAY + - ANY + - MAN + Art Gallery (Fourth Floor): + entrances: + Art Gallery: + room: Art Gallery + door: Fourth Floor + panels: + URNS: + id: Painting Room/Panel_urns_turns + colors: blue + tag: midblue + LEARNS: + id: Painting Room/Panel_learns_turns + colors: purple + tag: midpurp + RUNTS: + id: Painting Room/Panel_runts_turns + colors: yellow + tag: midyellow + SEND - USE: + id: Painting Room/Panel_send_use_turns + colors: orange + tag: midorange + TRUST: + id: Painting Room/Panel_trust_06890 + colors: orange + tag: midorange + "062459": + id: Painting Room/Panel_06890_trust + colors: orange + tag: midorange + doors: + Puzzles: + skip_item: True + location_name: Art Gallery - Fourth Floor Puzzles + panels: + - URNS + - LEARNS + - RUNTS + - SEND - USE + - TRUST + - "062459" + Rhyme Room (Smiley): + entrances: + Orange Tower Third Floor: + room: Orange Tower Third Floor + door: Rhyme Room Entrance + Rhyme Room (Circle): + room: Rhyme Room (Circle) + door: Door to Smiley + Rhyme Room (Cross): True # one-way + panels: + LOANS: + id: Double Room/Panel_bones_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme BONES + SKELETON: + id: Double Room/Panel_bones_syn + tag: syn rhyme + subtag: bot + link: rhyme BONES + REPENTANCE: + id: Double Room/Panel_sentence_rhyme + colors: purple + tag: whole rhyme + subtag: top + link: rhyme SENTENCE + WORD: + id: Double Room/Panel_sentence_whole + colors: blue + tag: whole rhyme + subtag: bot + link: rhyme SENTENCE + SCHEME: + id: Double Room/Panel_dream_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme DREAM + FANTASY: + id: Double Room/Panel_dream_syn + tag: syn rhyme + subtag: bot + link: rhyme DREAM + HISTORY: + id: Double Room/Panel_mystery_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme MYSTERY + SECRET: + id: Double Room/Panel_mystery_syn + tag: syn rhyme + subtag: bot + link: rhyme MYSTERY + doors: + # This is complicated. I want the location in here to just be the four + # panels against the wall toward Target. But in vanilla, you also need to + # solve the panels in Circle that are against the Smiley wall. Logic needs + # to know this so that it can handle no door shuffle properly. So we split + # the item and location up. + Door to Target: + id: + - Double Room Area Doors/Door_room_3a + - Double Room Area Doors/Door_room_3bc + skip_location: True + group: Rhyme Room Doors + panels: + - SCHEME + - FANTASY + - HISTORY + - SECRET + - room: Rhyme Room (Circle) + panel: BIRD + - room: Rhyme Room (Circle) + panel: LETTER + - room: Rhyme Room (Circle) + panel: VIOLENT + - room: Rhyme Room (Circle) + panel: MUTE + Door to Target (Location): + location_name: Rhyme Room (Smiley) - Puzzles Toward Target + skip_item: True + panels: + - SCHEME + - FANTASY + - HISTORY + - SECRET + Rhyme Room (Cross): + entrances: + Rhyme Room (Target): # one-way + room: Rhyme Room (Target) + door: Door to Cross + Rhyme Room (Looped Square): + room: Rhyme Room (Looped Square) + door: Door to Cross + panels: + NINE: + id: Backside Room/Panel_nine_nine_9 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + FERN: + id: Double Room/Panel_return_rhyme + colors: purple + tag: ant rhyme + subtag: top + link: rhyme RETURN + STAY: + id: Double Room/Panel_return_ant + colors: black + tag: ant rhyme + subtag: bot + link: rhyme RETURN + FRIEND: + id: Double Room/Panel_descend_rhyme + colors: purple + tag: ant rhyme + subtag: top + link: rhyme DESCEND + RISE: + id: Double Room/Panel_descend_ant + colors: black + tag: ant rhyme + subtag: bot + link: rhyme DESCEND + PLUMP: + id: Double Room/Panel_jump_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme JUMP + BOUNCE: + id: Double Room/Panel_jump_syn + tag: syn rhyme + subtag: bot + link: rhyme JUMP + SCRAWL: + id: Double Room/Panel_fall_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme FALL + PLUNGE: + id: Double Room/Panel_fall_syn + tag: syn rhyme + subtag: bot + link: rhyme FALL + LEAP: + id: Double Room/Panel_leap_leap + tag: midwhite + doors: + Exit: + id: Double Room Area Doors/Door_room_exit + location_name: Rhyme Room (Cross) - Exit Puzzles + group: Rhyme Room Doors + panels: + - PLUMP + - BOUNCE + - SCRAWL + - PLUNGE + Rhyme Room (Circle): + entrances: + Rhyme Room (Looped Square): + room: Rhyme Room (Looped Square) + door: Door to Circle + Hidden Room: + room: Hidden Room + door: Rhyme Room Entrance + Rhyme Room (Smiley): + door: Door to Smiley + panels: + BIRD: + id: Double Room/Panel_word_rhyme + colors: purple + tag: whole rhyme + subtag: top + link: rhyme WORD + LETTER: + id: Double Room/Panel_word_whole + colors: blue + tag: whole rhyme + subtag: bot + link: rhyme WORD + FORBIDDEN: + id: Double Room/Panel_hidden_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme HIDDEN + CONCEALED: + id: Double Room/Panel_hidden_syn + tag: syn rhyme + subtag: bot + link: rhyme HIDDEN + VIOLENT: + id: Double Room/Panel_silent_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme SILENT + MUTE: + id: Double Room/Panel_silent_syn + tag: syn rhyme + subtag: bot + link: rhyme SILENT + doors: + Door to Smiley: + id: + - Double Room Area Doors/Door_room_2b + - Double Room Area Doors/Door_room_3b + location_name: Rhyme Room - Circle/Smiley Wall + group: Rhyme Room Doors + panels: + - BIRD + - LETTER + - VIOLENT + - MUTE + - room: Rhyme Room (Smiley) + panel: LOANS + - room: Rhyme Room (Smiley) + panel: SKELETON + - room: Rhyme Room (Smiley) + panel: REPENTANCE + - room: Rhyme Room (Smiley) + panel: WORD + paintings: + - id: arrows_painting_3 + orientation: north + Rhyme Room (Looped Square): + entrances: + Starting Room: + room: Starting Room + door: Rhyme Room Entrance + Rhyme Room (Circle): + door: Door to Circle + Rhyme Room (Cross): + door: Door to Cross + Rhyme Room (Target): + door: Door to Target + panels: + WALKED: + id: Double Room/Panel_blocked_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme BLOCKED + OBSTRUCTED: + id: Double Room/Panel_blocked_syn + tag: syn rhyme + subtag: bot + link: rhyme BLOCKED + SKIES: + id: Double Room/Panel_rise_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme RISE + SWELL: + id: Double Room/Panel_rise_syn + tag: syn rhyme + subtag: bot + link: rhyme RISE + PENNED: + id: Double Room/Panel_ascend_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme ASCEND + CLIMB: + id: Double Room/Panel_ascend_syn + tag: syn rhyme + subtag: bot + link: rhyme ASCEND + TROUBLE: + id: Double Room/Panel_double_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme DOUBLE + DUPLICATE: + id: Double Room/Panel_double_syn + tag: syn rhyme + subtag: bot + link: rhyme DOUBLE + doors: + Door to Circle: + id: + - Double Room Area Doors/Door_room_2a + - Double Room Area Doors/Door_room_1c + location_name: Rhyme Room - Circle/Looped Square Wall + group: Rhyme Room Doors + panels: + - WALKED + - OBSTRUCTED + - SKIES + - SWELL + - room: Rhyme Room (Circle) + panel: BIRD + - room: Rhyme Room (Circle) + panel: LETTER + - room: Rhyme Room (Circle) + panel: FORBIDDEN + - room: Rhyme Room (Circle) + panel: CONCEALED + Door to Cross: + id: + - Double Room Area Doors/Door_room_1a + - Double Room Area Doors/Door_room_5a + location_name: Rhyme Room - Cross/Looped Square Wall + group: Rhyme Room Doors + panels: + - SKIES + - SWELL + - PENNED + - CLIMB + - room: Rhyme Room (Cross) + panel: FERN + - room: Rhyme Room (Cross) + panel: STAY + - room: Rhyme Room (Cross) + panel: FRIEND + - room: Rhyme Room (Cross) + panel: RISE + Door to Target: + id: + - Double Room Area Doors/Door_room_1b + - Double Room Area Doors/Door_room_4b + location_name: Rhyme Room - Target/Looped Square Wall + group: Rhyme Room Doors + panels: + - PENNED + - CLIMB + - TROUBLE + - DUPLICATE + - room: Rhyme Room (Target) + panel: WILD + - room: Rhyme Room (Target) + panel: KID + - room: Rhyme Room (Target) + panel: PISTOL + - room: Rhyme Room (Target) + panel: QUARTZ + Rhyme Room (Target): + entrances: + Rhyme Room (Smiley): # one-way + room: Rhyme Room (Smiley) + door: Door to Target + Rhyme Room (Looped Square): + room: Rhyme Room (Looped Square) + door: Door to Target + panels: + WILD: + id: Double Room/Panel_child_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme CHILD + KID: + id: Double Room/Panel_child_syn + tag: syn rhyme + subtag: bot + link: rhyme CHILD + PISTOL: + id: Double Room/Panel_crystal_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme CRYSTAL + QUARTZ: + id: Double Room/Panel_crystal_syn + tag: syn rhyme + subtag: bot + link: rhyme CRYSTAL + INNOVATIVE (Top): + id: Double Room/Panel_creative_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme CREATIVE + INNOVATIVE (Bottom): + id: Double Room/Panel_creative_syn + tag: syn rhyme + subtag: bot + link: rhyme CREATIVE + doors: + Door to Cross: + id: Double Room Area Doors/Door_room_4a + location_name: Rhyme Room (Target) - Puzzles Toward Cross + group: Rhyme Room Doors + panels: + - PISTOL + - QUARTZ + - INNOVATIVE (Top) + - INNOVATIVE (Bottom) + paintings: + - id: arrows_painting_4 + orientation: north + Room Room: + # This is a bit of a weird room. You can't really get to it from the roof. + # And even if you were to go through the shortcut on the fifth floor into + # the basement and up the stairs, you'd be blocked by the backsides of the + # ROOM panels, which isn't ideal. So we will, at least for now, say that + # this room is vanilla. + # + # For pretty much the same reason, I don't want to shuffle the paintings in + # here. + entrances: + Orange Tower Fourth Floor: True + panels: + DOOR (1): + id: Panel Room/Panel_room_door_1 + colors: gray + tag: forbid + DOOR (2): + id: Panel Room/Panel_room_door_2 + colors: gray + tag: forbid + WINDOW: + id: Panel Room/Panel_room_window_1 + colors: gray + tag: forbid + STAIRS: + id: Panel Room/Panel_room_stairs_1 + colors: gray + tag: forbid + PAINTING: + id: Panel Room/Panel_room_painting_1 + colors: gray + tag: forbid + FLOOR (1): + id: Panel Room/Panel_room_floor_1 + colors: gray + tag: forbid + FLOOR (2): + id: Panel Room/Panel_room_floor_2 + colors: gray + tag: forbid + FLOOR (3): + id: Panel Room/Panel_room_floor_3 + colors: gray + tag: forbid + FLOOR (4): + id: Panel Room/Panel_room_floor_4 + colors: gray + tag: forbid + FLOOR (5): + id: Panel Room/Panel_room_floor_5 + colors: gray + tag: forbid + FLOOR (7): + id: Panel Room/Panel_room_floor_7 + colors: gray + tag: forbid + FLOOR (8): + id: Panel Room/Panel_room_floor_8 + colors: gray + tag: forbid + FLOOR (9): + id: Panel Room/Panel_room_floor_9 + colors: gray + tag: forbid + FLOOR (10): + id: Panel Room/Panel_room_floor_10 + colors: gray + tag: forbid + CEILING (1): + id: Panel Room/Panel_room_ceiling_1 + colors: gray + tag: forbid + CEILING (2): + id: Panel Room/Panel_room_ceiling_2 + colors: gray + tag: forbid + CEILING (3): + id: Panel Room/Panel_room_ceiling_3 + colors: gray + tag: forbid + CEILING (4): + id: Panel Room/Panel_room_ceiling_4 + colors: gray + tag: forbid + CEILING (5): + id: Panel Room/Panel_room_ceiling_5 + colors: gray + tag: forbid + WALL (1): + id: Panel Room/Panel_room_wall_1 + colors: gray + tag: forbid + WALL (2): + id: Panel Room/Panel_room_wall_2 + colors: gray + tag: forbid + WALL (3): + id: Panel Room/Panel_room_wall_3 + colors: gray + tag: forbid + WALL (4): + id: Panel Room/Panel_room_wall_4 + colors: gray + tag: forbid + WALL (5): + id: Panel Room/Panel_room_wall_5 + colors: gray + tag: forbid + WALL (6): + id: Panel Room/Panel_room_wall_6 + colors: gray + tag: forbid + WALL (7): + id: Panel Room/Panel_room_wall_7 + colors: gray + tag: forbid + WALL (8): + id: Panel Room/Panel_room_wall_8 + colors: gray + tag: forbid + WALL (9): + id: Panel Room/Panel_room_wall_9 + colors: gray + tag: forbid + WALL (10): + id: Panel Room/Panel_room_wall_10 + colors: gray + tag: forbid + WALL (11): + id: Panel Room/Panel_room_wall_11 + colors: gray + tag: forbid + WALL (12): + id: Panel Room/Panel_room_wall_12 + colors: gray + tag: forbid + WALL (13): + id: Panel Room/Panel_room_wall_13 + colors: gray + tag: forbid + WALL (14): + id: Panel Room/Panel_room_wall_14 + colors: gray + tag: forbid + WALL (15): + id: Panel Room/Panel_room_wall_15 + colors: gray + tag: forbid + WALL (16): + id: Panel Room/Panel_room_wall_16 + colors: gray + tag: forbid + WALL (17): + id: Panel Room/Panel_room_wall_17 + colors: gray + tag: forbid + WALL (18): + id: Panel Room/Panel_room_wall_18 + colors: gray + tag: forbid + WALL (19): + id: Panel Room/Panel_room_wall_19 + colors: gray + tag: forbid + WALL (20): + id: Panel Room/Panel_room_wall_20 + colors: gray + tag: forbid + WALL (21): + id: Panel Room/Panel_room_wall_21 + colors: gray + tag: forbid + BROOMED: + id: Panel Room/Panel_broomed_bedroom + colors: yellow + tag: midyellow + required_door: + door: Excavation + LAYS: + id: Panel Room/Panel_lays_maze + colors: purple + tag: toppurp + required_panel: + panel: BROOMED + BASE: + id: Panel Room/Panel_base_basement + colors: blue + tag: midblue + required_panel: + panel: LAYS + MASTERY: + id: Master Room/Panel_mastery_mastery + tag: midwhite + colors: gray + required_door: + room: Orange Tower Seventh Floor + door: Mastery + doors: + Excavation: + event: True + panels: + - WALL (1) + Shortcut to Fifth Floor: + id: + - Tower Room Area Doors/Door_panel_basement + - Tower Room Area Doors/Door_panel_basement2 + panels: + - BASE + Cellar: + entrances: + Room Room: + room: Room Room + door: Excavation + Orange Tower Fifth Floor: + room: Room Room + door: Shortcut to Fifth Floor + Outside The Wise: + entrances: + Orange Tower Sixth Floor: + painting: True + Outside The Initiated: + painting: True + panels: + KITTEN: + id: Clock Room/Panel_kitten_cat + colors: brown + tag: botbrown + CAT: + id: Clock Room/Panel_cat_kitten + tag: bot brown black + colors: + - brown + - black + doors: + Wise Entrance: + id: Clock Room Area Doors/Door_time_start + item_name: The Wise - Entrance + panels: + - KITTEN + - CAT + paintings: + - id: arrows_painting_2 + orientation: east + - id: clock_painting_2 + orientation: east + exit_only: True + required: True + The Wise: + entrances: + Outside The Wise: + room: Outside The Wise + door: Wise Entrance + panels: + Achievement: + id: Countdown Panels/Panel_intelligent_wise + colors: + - brown + - black + tag: forbid + check: True + achievement: The Wise + PUPPY: + id: Clock Room/Panel_puppy_dog + colors: brown + tag: botbrown + ADULT: + id: Clock Room/Panel_adult_child + colors: + - brown + - black + tag: bot brown black + BREAD: + id: Clock Room/Panel_bread_mold + colors: brown + tag: botbrown + DINOSAUR: + id: Clock Room/Panel_dinosaur_fossil + colors: brown + tag: botbrown + OAK: + id: Clock Room/Panel_oak_acorn + colors: + - brown + - black + tag: bot brown black + CORPSE: + id: Clock Room/Panel_corpse_skeleton + colors: brown + tag: botbrown + BEFORE: + id: Clock Room/Panel_before_ere + colors: + - brown + - black + tag: mid brown black + YOUR: + id: Clock Room/Panel_your_thy + colors: + - brown + - black + tag: mid brown black + BETWIXT: + id: Clock Room/Panel_betwixt_between + colors: brown + tag: midbrown + NIGH: + id: Clock Room/Panel_nigh_near + colors: brown + tag: midbrown + CONNEXION: + id: Clock Room/Panel_connexion_connection + colors: brown + tag: midbrown + THOU: + id: Clock Room/Panel_thou_you + colors: brown + tag: midbrown + paintings: + - id: clock_painting_3 + orientation: east + The Red: + entrances: + Roof: True + panels: + Achievement: + id: Countdown Panels/Panel_grandfathered_red + colors: red + tag: forbid + check: True + achievement: The Red + PANDEMIC (1): + id: Hangry Room/Panel_red_top_1 + colors: red + tag: topred + TRINITY: + id: Hangry Room/Panel_red_top_2 + colors: red + tag: topred + CHEMISTRY: + id: Hangry Room/Panel_red_top_3 + colors: red + tag: topred + FLUMMOXED: + id: Hangry Room/Panel_red_top_4 + colors: red + tag: topred + PANDEMIC (2): + id: Hangry Room/Panel_red_mid_1 + colors: red + tag: midred + COUNTERCLOCKWISE: + id: Hangry Room/Panel_red_mid_2 + colors: red + tag: red top red mid black bot + FEARLESS: + id: Hangry Room/Panel_red_mid_3 + colors: red + tag: midred + DEFORESTATION: + id: Hangry Room/Panel_red_mid_4 + colors: red + tag: red mid bot + subtag: mid + link: rmb FORE + CRAFTSMANSHIP: + id: Hangry Room/Panel_red_mid_5 + colors: red + tag: red mid bot + subtag: mid + link: rmb AFT + CAMEL: + id: Hangry Room/Panel_red_bot_1 + colors: red + tag: botred + LION: + id: Hangry Room/Panel_red_bot_2 + colors: red + tag: botred + TIGER: + id: Hangry Room/Panel_red_bot_3 + colors: red + tag: botred + SHIP (1): + id: Hangry Room/Panel_red_bot_4 + colors: red + tag: red mid bot + subtag: bot + link: rmb FORE + SHIP (2): + id: Hangry Room/Panel_red_bot_5 + colors: red + tag: red mid bot + subtag: bot + link: rmb AFT + GIRAFFE: + id: Hangry Room/Panel_red_bot_6 + colors: red + tag: botred + The Ecstatic: + entrances: + Roof: True + panels: + Achievement: + id: Countdown Panels/Panel_ecstatic_ecstatic + colors: yellow + tag: forbid + check: True + achievement: The Ecstatic + FORM (1): + id: Smiley Room/Panel_soundgram_1 + colors: yellow + tag: yellow top bot + subtag: bottom + link: ytb FORM + WIND: + id: Smiley Room/Panel_soundgram_2 + colors: yellow + tag: botyellow + EGGS: + id: Smiley Room/Panel_scrambled_1 + colors: yellow + tag: botyellow + VEGETABLES: + id: Smiley Room/Panel_scrambled_2 + colors: yellow + tag: botyellow + WATER: + id: Smiley Room/Panel_anagram_6_1 + colors: yellow + tag: botyellow + FRUITS: + id: Smiley Room/Panel_anagram_6_2 + colors: yellow + tag: botyellow + LEAVES: + id: Smiley Room/Panel_anagram_7_1 + colors: yellow + tag: topyellow + VINES: + id: Smiley Room/Panel_anagram_7_2 + colors: yellow + tag: topyellow + ICE: + id: Smiley Room/Panel_anagram_7_3 + colors: yellow + tag: topyellow + STYLE: + id: Smiley Room/Panel_anagram_7_4 + colors: yellow + tag: topyellow + FIR: + id: Smiley Room/Panel_anagram_8_1 + colors: yellow + tag: topyellow + REEF: + id: Smiley Room/Panel_anagram_8_2 + colors: yellow + tag: topyellow + ROTS: + id: Smiley Room/Panel_anagram_8_3 + colors: yellow + tag: topyellow + FORM (2): + id: Smiley Room/Panel_anagram_9_1 + colors: yellow + tag: yellow top bot + subtag: top + link: ytb FORM + Outside The Scientific: + entrances: + Roof: True + The Scientific: + door: Scientific Entrance + panels: + OPEN: + id: Chemistry Room/Panel_open + tag: midwhite + CLOSE: + id: Chemistry Room/Panel_close + colors: black + tag: botblack + AHEAD: + id: Chemistry Room/Panel_ahead + colors: black + tag: botblack + doors: + Scientific Entrance: + id: Red Blue Purple Room Area Doors/Door_chemistry_lab + item_name: The Scientific - Entrance + panels: + - OPEN + The Scientific: + entrances: + Outside The Scientific: + room: Outside The Scientific + door: Scientific Entrance + panels: + Achievement: + id: Countdown Panels/Panel_scientific_scientific + colors: + - yellow + - red + - blue + - brown + - black + - purple + tag: forbid + check: True + achievement: The Scientific + HYDROGEN (1): + id: Chemistry Room/Panel_blue_bot_3 + colors: blue + tag: tri botblue + link: tbb WATER + OXYGEN: + id: Chemistry Room/Panel_blue_bot_2 + colors: blue + tag: tri botblue + link: tbb WATER + HYDROGEN (2): + id: Chemistry Room/Panel_blue_bot_4 + colors: blue + tag: tri botblue + link: tbb WATER + SUGAR (1): + id: Chemistry Room/Panel_sugar_1 + colors: red + tag: botred + SUGAR (2): + id: Chemistry Room/Panel_sugar_2 + colors: red + tag: botred + SUGAR (3): + id: Chemistry Room/Panel_sugar_3 + colors: red + tag: botred + CHLORINE: + id: Chemistry Room/Panel_blue_bot_5 + colors: blue + tag: double botblue + subtag: left + link: holo SALT + SODIUM: + id: Chemistry Room/Panel_blue_bot_6 + colors: blue + tag: double botblue + subtag: right + link: holo SALT + FOREST: + id: Chemistry Room/Panel_long_bot_1 + colors: + - red + - blue + tag: chain red bot blue top + POUND: + id: Chemistry Room/Panel_long_top_1 + colors: + - red + - blue + tag: chain blue mid red bot + ICE: + id: Chemistry Room/Panel_brown_bot_1 + colors: brown + tag: botbrown + FISSION: + id: Chemistry Room/Panel_black_bot_1 + colors: black + tag: botblack + FUSION: + id: Chemistry Room/Panel_black_bot_2 + colors: black + tag: botblack + MISS: + id: Chemistry Room/Panel_blue_top_1 + colors: blue + tag: double topblue + subtag: left + link: exp CHEMISTRY + TREE (1): + id: Chemistry Room/Panel_blue_top_2 + colors: blue + tag: double topblue + subtag: right + link: exp CHEMISTRY + BIOGRAPHY: + id: Chemistry Room/Panel_biology_9 + colors: purple + tag: midpurp + CACTUS: + id: Chemistry Room/Panel_biology_4 + colors: red + tag: double botred + subtag: right + link: mero SPINE + VERTEBRATE: + id: Chemistry Room/Panel_biology_8 + colors: red + tag: double botred + subtag: left + link: mero SPINE + ROSE: + id: Chemistry Room/Panel_biology_2 + colors: red + tag: botred + TREE (2): + id: Chemistry Room/Panel_biology_3 + colors: red + tag: botred + FRUIT: + id: Chemistry Room/Panel_biology_1 + colors: red + tag: botred + MAMMAL: + id: Chemistry Room/Panel_biology_5 + colors: red + tag: botred + BIRD: + id: Chemistry Room/Panel_biology_6 + colors: red + tag: botred + FISH: + id: Chemistry Room/Panel_biology_7 + colors: red + tag: botred + GRAVELY: + id: Chemistry Room/Panel_physics_9 + colors: purple + tag: double midpurp + subtag: left + link: change GRAVITY + BREVITY: + id: Chemistry Room/Panel_biology_10 + colors: purple + tag: double midpurp + subtag: right + link: change GRAVITY + PART: + id: Chemistry Room/Panel_physics_2 + colors: blue + tag: blue mid red bot + subtag: mid + link: xur PARTICLE + MATTER: + id: Chemistry Room/Panel_physics_1 + colors: red + tag: blue mid red bot + subtag: bot + link: xur PARTICLE + ELECTRIC: + id: Chemistry Room/Panel_physics_6 + colors: purple + tag: purple mid red bot + subtag: mid + link: xpr ELECTRON + ATOM (1): + id: Chemistry Room/Panel_physics_3 + colors: red + tag: purple mid red bot + subtag: bot + link: xpr ELECTRON + NEUTRAL: + id: Chemistry Room/Panel_physics_7 + colors: purple + tag: purple mid red bot + subtag: mid + link: xpr NEUTRON + ATOM (2): + id: Chemistry Room/Panel_physics_4 + colors: red + tag: purple mid red bot + subtag: bot + link: xpr NEUTRON + PROPEL: + id: Chemistry Room/Panel_physics_8 + colors: purple + tag: purple mid red bot + subtag: mid + link: xpr PROTON + ATOM (3): + id: Chemistry Room/Panel_physics_5 + colors: red + tag: purple mid red bot + subtag: bot + link: xpr PROTON + ORDER: + id: Chemistry Room/Panel_physics_11 + colors: brown + tag: botbrown + OPTICS: + id: Chemistry Room/Panel_physics_10 + colors: yellow + tag: midyellow + GRAPHITE: + id: Chemistry Room/Panel_yellow_bot_1 + colors: yellow + tag: botyellow + HOT RYE: + id: Chemistry Room/Panel_anagram_1 + colors: yellow + tag: midyellow + SIT SHY HOPE: + id: Chemistry Room/Panel_anagram_2 + colors: yellow + tag: midyellow + ME NEXT PIER: + id: Chemistry Room/Panel_anagram_3 + colors: yellow + tag: midyellow + RUT LESS: + id: Chemistry Room/Panel_anagram_4 + colors: yellow + tag: midyellow + SON COUNCIL: + id: Chemistry Room/Panel_anagram_5 + colors: yellow + tag: midyellow + doors: + Chemistry Puzzles: + skip_item: True + location_name: The Scientific - Chemistry Puzzles + panels: + - HYDROGEN (1) + - OXYGEN + - HYDROGEN (2) + - SUGAR (1) + - SUGAR (2) + - SUGAR (3) + - CHLORINE + - SODIUM + - FOREST + - POUND + - ICE + - FISSION + - FUSION + - MISS + - TREE (1) + Biology Puzzles: + skip_item: True + location_name: The Scientific - Biology Puzzles + panels: + - BIOGRAPHY + - CACTUS + - VERTEBRATE + - ROSE + - TREE (2) + - FRUIT + - MAMMAL + - BIRD + - FISH + Physics Puzzles: + skip_item: True + location_name: The Scientific - Physics Puzzles + panels: + - GRAVELY + - BREVITY + - PART + - MATTER + - ELECTRIC + - ATOM (1) + - NEUTRAL + - ATOM (2) + - PROPEL + - ATOM (3) + - ORDER + - OPTICS + paintings: + - id: hi_solved_painting4 + orientation: south + Challenge Room: + entrances: + Welcome Back Area: + door: Welcome Door + Number Hunt: + room: Outside The Undeterred + door: Challenge Entrance + panels: + WELCOME: + id: Challenge Room/Panel_welcome_welcome + tag: midwhite + CHALLENGE: + id: Challenge Room/Panel_challenge_challenge + tag: midwhite + Achievement: + id: Countdown Panels/Panel_challenged_unchallenged + check: True + colors: + - black + - gray + - red + - blue + - yellow + - purple + - brown + - orange + tag: forbid + achievement: The Unchallenged + OPEN: + id: Challenge Room/Panel_open_nepotism + colors: + - black + - blue + tag: chain mid black !!! blue + SINGED: + id: Challenge Room/Panel_singed_singsong + colors: + - red + - blue + tag: chain mid red blue + NEVER TRUSTED: + id: Challenge Room/Panel_nevertrusted_maladjusted + colors: purple + tag: midpurp + CORNER: + id: Challenge Room/Panel_corner_corn + colors: red + tag: midred + STRAWBERRIES: + id: Challenge Room/Panel_strawberries_mold + colors: brown + tag: double botbrown + subtag: left + link: time MOLD + GRUB: + id: Challenge Room/Panel_grub_burger + colors: + - black + - blue + tag: chain mid black blue + BREAD: + id: Challenge Room/Panel_bread_mold + colors: brown + tag: double botbrown + subtag: right + link: time MOLD + COLOR: + id: Challenge Room/Panel_color_gray + colors: gray + tag: forbid + WRITER: + id: Challenge Room/Panel_writer_songwriter + colors: blue + tag: midblue + "02759": + id: Challenge Room/Panel_tales_stale + colors: + - orange + - yellow + tag: chain mid orange yellow + REAL EYES: + id: Challenge Room/Panel_realeyes_realize + tag: topwhite + LOBS: + id: Challenge Room/Panel_lobs_lobster + colors: blue + tag: midblue + PEST ALLY: + id: Challenge Room/Panel_double_anagram_1 + colors: yellow + tag: midyellow + GENIAL HALO: + id: Challenge Room/Panel_double_anagram_2 + colors: yellow + tag: midyellow + DUCK LOGO: + id: Challenge Room/Panel_double_anagram_3 + colors: yellow + tag: midyellow + AVIAN GREEN: + id: Challenge Room/Panel_double_anagram_4 + colors: yellow + tag: midyellow + FEVER TEAR: + id: Challenge Room/Panel_double_anagram_5 + colors: yellow + tag: midyellow + FACTS: + id: Challenge Room/Panel_facts + colors: + - red + - blue + tag: forbid + FACTS (1): + id: Challenge Room/Panel_facts2 + colors: red + tag: forbid + FACTS (3): + id: Challenge Room/Panel_facts3 + tag: forbid + FACTS (4): + id: Challenge Room/Panel_facts4 + colors: blue + tag: forbid + FACTS (5): + id: Challenge Room/Panel_facts5 + colors: blue + tag: forbid + FACTS (6): + id: Challenge Room/Panel_facts6 + colors: blue + tag: forbid + LAPEL SHEEP: + id: Challenge Room/Panel_double_anagram_6 + colors: yellow + tag: midyellow + doors: + Welcome Door: + id: Entry Room Area Doors/Door_challenge_challenge + panels: + - WELCOME diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py new file mode 100644 index 000000000000..1f426c92f24a --- /dev/null +++ b/worlds/lingo/__init__.py @@ -0,0 +1,112 @@ +""" +Archipelago init file for Lingo +""" +from BaseClasses import Item, Tutorial +from worlds.AutoWorld import WebWorld, World +from .items import ALL_ITEM_TABLE, LingoItem +from .locations import ALL_LOCATION_TABLE +from .options import LingoOptions +from .player_logic import LingoPlayerLogic +from .regions import create_regions +from .static_logic import Room, RoomEntrance +from .testing import LingoTestOptions + + +class LingoWebWorld(WebWorld): + theme = "grass" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to playing Lingo with Archipelago.", + "English", + "setup_en.md", + "setup/en", + ["hatkirby"] + )] + + +class LingoWorld(World): + """ + Lingo is a first person indie puzzle game in the vein of The Witness. You find yourself in a mazelike, non-Euclidean + world filled with 800 word puzzles that use a variety of different mechanics. + """ + game = "Lingo" + web = LingoWebWorld() + + base_id = 444400 + topology_present = True + data_version = 1 + + options_dataclass = LingoOptions + options: LingoOptions + + item_name_to_id = { + name: data.code for name, data in ALL_ITEM_TABLE.items() + } + location_name_to_id = { + name: data.code for name, data in ALL_LOCATION_TABLE.items() + } + + player_logic: LingoPlayerLogic + + def generate_early(self): + self.player_logic = LingoPlayerLogic(self) + + def create_regions(self): + create_regions(self, self.player_logic) + + def create_items(self): + pool = [self.create_item(name) for name in self.player_logic.REAL_ITEMS] + + if self.player_logic.FORCED_GOOD_ITEM != "": + new_item = self.create_item(self.player_logic.FORCED_GOOD_ITEM) + location_obj = self.multiworld.get_location("Second Room - Good Luck", self.player) + location_obj.place_locked_item(new_item) + + item_difference = len(self.player_logic.REAL_LOCATIONS) - len(pool) + if item_difference: + trap_percentage = self.options.trap_percentage + traps = int(item_difference * trap_percentage / 100.0) + non_traps = item_difference - traps + + if non_traps: + skip_percentage = self.options.puzzle_skip_percentage + skips = int(non_traps * skip_percentage / 100.0) + non_skips = non_traps - skips + + filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"] + for i in range(0, non_skips): + pool.append(self.create_item(filler_list[i % len(filler_list)])) + + for i in range(0, skips): + pool.append(self.create_item("Puzzle Skip")) + + if traps: + traps_list = ["Slowness Trap", "Iceland Trap", "Atbash Trap"] + + for i in range(0, traps): + pool.append(self.create_item(traps_list[i % len(traps_list)])) + + self.multiworld.itempool += pool + + def create_item(self, name: str) -> Item: + item = ALL_ITEM_TABLE[name] + return LingoItem(name, item.classification, item.code, self.player) + + def set_rules(self): + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + + def fill_slot_data(self): + slot_options = [ + "death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels", + "mastery_achievements", "level_2_requirement", "location_checks", "early_color_hallways" + ] + + slot_data = { + "seed": self.random.randint(0, 1000000), + **self.options.as_dict(*slot_options), + } + + if self.options.shuffle_paintings: + slot_data["painting_entrance_to_exit"] = self.player_logic.PAINTING_MAPPING + + return slot_data diff --git a/worlds/lingo/docs/en_Lingo.md b/worlds/lingo/docs/en_Lingo.md new file mode 100644 index 000000000000..cff0581d9b2f --- /dev/null +++ b/worlds/lingo/docs/en_Lingo.md @@ -0,0 +1,42 @@ +# Lingo + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +There are a couple of modes of randomization currently available, and you can pick and choose which ones you would like +to use. + +* **Door shuffle**: There are many doors in the game, which are opened by completing a set of panels. With door shuffle + on, the doors become items and only open up once you receive the corresponding item. The panel sets that would + ordinarily open the doors become locations. + +* **Color shuffle**: There are ten different colors of puzzle in the game, each representing a different mechanic. With + color shuffle on, you would start with only access to white puzzles. Puzzles of other colors will require you to + receive an item in order to solve them (e.g. you can't solve any red puzzles until you receive the "Red" item). + +* **Panel shuffle**: Panel shuffling replaces the puzzles on each panel with different ones. So far, the only mode of + panel shuffling is "rearrange" mode, which simply shuffles the already-existing puzzles from the base game onto + different panels. + +* **Painting shuffle**: This randomizes the appearance of the paintings in the game, as well as which of them are warps, + and the locations that they warp you to. It is the equivalent of an entrance randomizer in another game. + +## What is a "check" in this game? + +Most panels / panel sets that open a door are now location checks, even if door shuffle is not enabled. Various other +puzzles are also location checks, including the achievement panels for each area. + +## What about wall snipes? + +"Wall sniping" refers to the fact that you are able to solve puzzles on the other side of opaque walls. This randomizer +does not change how wall snipes work, but it will never require the use of them. There are three puzzles from the base +game that you would ordinarily be expected to wall snipe. The randomizer moves these panels out of the wall or otherwise +reveals them so that a snipe is not necessary. + +Because of this, all wall snipes are considered out of logic. This includes sniping The Bearer's MIDDLE while standing +outside The Bold, sniping The Colorful without opening all of the color doors, and sniping WELCOME from next to WELCOME +BACK. diff --git a/worlds/lingo/docs/setup_en.md b/worlds/lingo/docs/setup_en.md new file mode 100644 index 000000000000..97f3ce594063 --- /dev/null +++ b/worlds/lingo/docs/setup_en.md @@ -0,0 +1,45 @@ +# Lingo Randomizer Setup + +## Required Software + +- [Lingo](https://store.steampowered.com/app/1814170/Lingo/) +- [Lingo Archipelago Randomizer](https://code.fourisland.com/lingo-archipelago/about/CHANGELOG.md) + +## Optional Software + +- [Archipelago Text Client](https://github.com/ArchipelagoMW/Archipelago/releases) +- [Lingo AP Tracker](https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md) + +## Installation + +1. Download the Lingo Archipelago Randomizer from the above link. +2. Open up Lingo, go to settings, and click View Game Data. This should open up + a folder in Windows Explorer. +3. Unzip the contents of the randomizer into the "maps" folder. You may need to + create the "maps" folder if you have not played a custom Lingo map before. +4. Installation complete! You may have to click Return to go back to the main + menu and then click Settings again in order to get the randomizer to show up + in the level selection list. + +## Joining a Multiworld game + +1. Launch Lingo +2. Click on Settings, and then Level. Choose Archipelago from the list. +3. Start a new game. Leave the name field blank (anything you type in will be + ignored). +4. Enter the Archipelago address, slot name, and password into the fields. +5. Press Connect. +6. Enjoy! + +To continue an earlier game, you can perform the exact same steps as above. You +do not have to re-select Archipelago in the level selection screen if you were +using Archipelago the last time you launched the game. + +In order to play the base game again, simply return to the level selection +screen and choose Level 1 (or whatever else you want to play). The randomizer +will not affect gameplay unless you launch it by starting a new game while it is +selected in the level selection screen, so it is safe to play the game normally +while the client is installed. + +**Note**: Running the randomizer modifies the game's memory. If you want to play +the base game after playing the randomizer, you need to restart Lingo first. diff --git a/worlds/lingo/ids.yaml b/worlds/lingo/ids.yaml new file mode 100644 index 000000000000..f48858a285f0 --- /dev/null +++ b/worlds/lingo/ids.yaml @@ -0,0 +1,1449 @@ +--- +special_items: + Black: 444400 + Red: 444401 + Blue: 444402 + Yellow: 444403 + Green: 444404 + Orange: 444405 + Gray: 444406 + Brown: 444407 + Purple: 444408 + ":)": 444409 + The Feeling of Being Lost: 444575 + Wanderlust: 444576 + Empty White Hallways: 444577 + Slowness Trap: 444410 + Iceland Trap: 444411 + Atbash Trap: 444412 + Puzzle Skip: 444413 +panels: + Starting Room: + HI: 444400 + HIDDEN: 444401 + TYPE: 444402 + THIS: 444403 + WRITE: 444404 + SAME: 444405 + Hidden Room: + DEAD END: 444406 + OPEN: 444407 + LIES: 444408 + The Seeker: + Achievement: 444409 + BEAR: 444410 + MINE: 444411 + MINE (2): 444412 + BOW: 444413 + DOES: 444414 + MOBILE: 444415 + MOBILE (2): 444416 + DESERT: 444417 + DESSERT: 444418 + SOW: 444419 + SEW: 444420 + TO: 444421 + TOO: 444422 + WRITE: 444423 + EWE: 444424 + KNOT: 444425 + NAUGHT: 444426 + BEAR (2): 444427 + Second Room: + HI: 444428 + LOW: 444429 + ANOTHER TRY: 444430 + LEVEL 2: 444431 + Hub Room: + ORDER: 444432 + SLAUGHTER: 444433 + NEAR: 444434 + FAR: 444435 + TRACE: 444436 + RAT: 444437 + OPEN: 444438 + FOUR: 444439 + LOST: 444440 + FORWARD: 444441 + BETWEEN: 444442 + BACKWARD: 444443 + Dead End Area: + FOUR: 444444 + EIGHT: 444445 + Pilgrim Antechamber: + HOT CRUST: 444446 + PILGRIMAGE: 444447 + MASTERY: 444448 + Pilgrim Room: + THIS: 444449 + TIME ROOM: 444450 + SCIENCE ROOM: 444451 + SHINY ROCK ROOM: 444452 + ANGRY POWER: 444453 + MICRO LEGION: 444454 + LOSERS RELAX: 444455 + '906234': 444456 + MOOR EMORDNILAP: 444457 + HALL ROOMMATE: 444458 + ALL GREY: 444459 + PLUNDER ISLAND: 444460 + FLOSS PATHS: 444461 + Crossroads: + DECAY: 444462 + NOPE: 444463 + EIGHT: 444464 + WE ROT: 444465 + WORDS: 444466 + SWORD: 444467 + TURN: 444468 + BEND HI: 444469 + THE EYES: 444470 + CORNER: 444471 + HOLLOW: 444472 + SWAP: 444473 + GEL: 444474 + THOUGH: 444475 + CROSSROADS: 444476 + Lost Area: + LOST (1): 444477 + LOST (2): 444478 + Amen Name Area: + AMEN: 444479 + NAME: 444480 + NINE: 444481 + Suits Area: + SPADES: 444482 + CLUBS: 444483 + HEARTS: 444484 + The Tenacious: + LEVEL (Black): 444485 + RACECAR (Black): 444486 + SOLOS (Black): 444487 + LEVEL (White): 444488 + RACECAR (White): 444489 + SOLOS (White): 444490 + Achievement: 444491 + Warts Straw Area: + WARTS: 444492 + STRAW: 444493 + Leaf Feel Area: + LEAF: 444494 + FEEL: 444495 + Outside The Agreeable: + MASSACRED: 444496 + BLACK: 444497 + CLOSE: 444498 + LEFT: 444499 + LEFT (2): 444500 + RIGHT: 444501 + PURPLE: 444502 + FIVE (1): 444503 + FIVE (2): 444504 + OUT: 444505 + HIDE: 444506 + DAZE: 444507 + WALL: 444508 + KEEP: 444509 + BAILEY: 444510 + TOWER: 444511 + NORTH: 444512 + DIAMONDS: 444513 + FIRE: 444514 + WINTER: 444515 + Dread Hallway: + DREAD: 444516 + The Agreeable: + Achievement: 444517 + BYE: 444518 + RETOOL: 444519 + DRAWER: 444520 + READ: 444521 + DIFFERENT: 444522 + LOW: 444523 + ALIVE: 444524 + THAT: 444525 + STRESSED: 444526 + STAR: 444527 + TAME: 444528 + CAT: 444529 + Hedge Maze: + DOWN: 444530 + HIDE (1): 444531 + HIDE (2): 444532 + HIDE (3): 444533 + MASTERY (1): 444534 + MASTERY (2): 444535 + PATH (1): 444536 + PATH (2): 444537 + PATH (3): 444538 + PATH (4): 444539 + PATH (5): 444540 + PATH (6): 444541 + PATH (7): 444542 + PATH (8): 444543 + REFLOW: 444544 + LEAP: 444545 + The Perceptive: + Achievement: 444546 + GAZE: 444547 + The Fearless (First Floor): + NAPS: 444548 + TEAM: 444549 + TEEM: 444550 + IMPATIENT: 444551 + EAT: 444552 + The Fearless (Second Floor): + NONE: 444553 + SUM: 444554 + FUNNY: 444555 + MIGHT: 444556 + SAFE: 444557 + SAME: 444558 + CAME: 444559 + The Fearless: + Achievement: 444560 + EASY: 444561 + SOMETIMES: 444562 + DARK: 444563 + EVEN: 444564 + The Observant: + Achievement: 444565 + BACK: 444566 + SIDE: 444567 + BACKSIDE: 444568 + STAIRS: 444569 + WAYS: 444570 + 'ON': 444571 + UP: 444572 + SWIMS: 444573 + UPSTAIRS: 444574 + TOIL: 444575 + STOP: 444576 + TOP: 444577 + HI: 444578 + HI (2): 444579 + '31': 444580 + '52': 444581 + OIL: 444582 + BACKSIDE (GREEN): 444583 + SIDEWAYS: 444584 + The Incomparable: + Achievement: 444585 + A (One): 444586 + A (Two): 444587 + A (Three): 444588 + A (Four): 444589 + A (Five): 444590 + A (Six): 444591 + I (One): 444592 + I (Two): 444593 + I (Three): 444594 + I (Four): 444595 + I (Five): 444596 + I (Six): 444597 + I (Seven): 444598 + Eight Room: + Eight Back: 444599 + Eight Front: 444600 + Nine: 444601 + Orange Tower First Floor: + SECRET: 444602 + DADS + ALE: 444603 + SALT: 444604 + Orange Tower Third Floor: + RED: 444605 + DEER + WREN: 444606 + Orange Tower Fourth Floor: + RUNT: 444607 + RUNT (2): 444608 + LEARNS + UNSEW: 444609 + HOT CRUSTS: 444610 + IRK HORN: 444611 + Hot Crusts Area: + EIGHT: 444612 + Orange Tower Fifth Floor: + SIZE (Small): 444613 + SIZE (Big): 444614 + DRAWL + RUNS: 444615 + NINE: 444616 + SUMMER: 444617 + AUTUMN: 444618 + SPRING: 444619 + PAINTING (1): 445078 + PAINTING (2): 445079 + PAINTING (3): 445080 + PAINTING (4): 445081 + PAINTING (5): 445082 + ROOM: 445083 + Orange Tower Seventh Floor: + THE END: 444620 + THE MASTER: 444621 + MASTERY: 444622 + Roof: + MASTERY (1): 444623 + MASTERY (2): 444624 + MASTERY (3): 444625 + MASTERY (4): 444626 + MASTERY (5): 444627 + MASTERY (6): 444628 + STAIRCASE: 444629 + Orange Tower Basement: + MASTERY: 444630 + THE LIBRARY: 444631 + Courtyard: + I: 444632 + GREEN: 444633 + PINECONE: 444634 + ACORN: 444635 + Yellow Backside Area: + BACKSIDE: 444636 + NINE: 444637 + First Second Third Fourth: + FIRST: 444638 + SECOND: 444639 + THIRD: 444640 + FOURTH: 444641 + The Colorful (White): + BEGIN: 444642 + The Colorful (Black): + FOUND: 444643 + The Colorful (Red): + LOAF: 444644 + The Colorful (Yellow): + CREAM: 444645 + The Colorful (Blue): + SUN: 444646 + The Colorful (Purple): + SPOON: 444647 + The Colorful (Orange): + LETTERS: 444648 + The Colorful (Green): + WALLS: 444649 + The Colorful (Brown): + IRON: 444650 + The Colorful (Gray): + OBSTACLE: 444651 + The Colorful: + Achievement: 444652 + Welcome Back Area: + WELCOME BACK: 444653 + SECRET: 444654 + CLOCKWISE: 444655 + Owl Hallway: + STRAYS: 444656 + READS + RUST: 444657 + Outside The Initiated: + SEVEN (1): 444658 + SEVEN (2): 444659 + EIGHT: 444660 + NINE: 444661 + BLUE: 444662 + ORANGE: 444663 + UNCOVER: 444664 + OXEN: 444665 + BACKSIDE: 444666 + The Optimistic: 444667 + PAST: 444668 + FUTURE: 444669 + FUTURE (2): 444670 + PAST (2): 444671 + PRESENT: 444672 + SMILE: 444673 + ANGERED: 444674 + VOTE: 444675 + The Initiated: + Achievement: 444676 + DAUGHTER: 444677 + START: 444678 + STARE: 444679 + HYPE: 444680 + ABYSS: 444681 + SWEAT: 444682 + BEAT: 444683 + ALUMNI: 444684 + PATS: 444685 + KNIGHT: 444686 + BYTE: 444687 + MAIM: 444688 + MORGUE: 444689 + CHAIR: 444690 + HUMAN: 444691 + BED: 444692 + The Traveled: + Achievement: 444693 + CLOSE: 444694 + COMPOSE: 444695 + RECORD: 444696 + CATEGORY: 444697 + HELLO: 444698 + DUPLICATE: 444699 + IDENTICAL: 444700 + DISTANT: 444701 + HAY: 444702 + GIGGLE: 444703 + CHUCKLE: 444704 + SNITCH: 444705 + CONCEALED: 444706 + PLUNGE: 444707 + AUTUMN: 444708 + ROAD: 444709 + FOUR: 444710 + Outside The Bold: + UNOPEN: 444711 + BEGIN: 444712 + SIX: 444713 + NINE: 444714 + LEFT: 444715 + RIGHT: 444716 + RISE (Horizon): 444717 + RISE (Sunrise): 444718 + ZEN: 444719 + SON: 444720 + STARGAZER: 444721 + MOUTH: 444722 + YEAST: 444723 + WET: 444724 + The Bold: + Achievement: 444725 + FOOT: 444726 + NEEDLE: 444727 + FACE: 444728 + SIGN: 444729 + HEARTBREAK: 444730 + UNDEAD: 444731 + DEADLINE: 444732 + SUSHI: 444733 + THISTLE: 444734 + LANDMASS: 444735 + MASSACRED: 444736 + AIRPLANE: 444737 + NIGHTMARE: 444738 + MOUTH: 444739 + SAW: 444740 + HAND: 444741 + Outside The Undeterred: + HOLLOW: 444742 + ART + ART: 444743 + PEN: 444744 + HUSTLING: 444745 + SUNLIGHT: 444746 + LIGHT: 444747 + BRIGHT: 444748 + SUNNY: 444749 + RAINY: 444750 + ZERO: 444751 + ONE: 444752 + TWO (1): 444753 + TWO (2): 444754 + THREE (1): 444755 + THREE (2): 444756 + THREE (3): 444757 + FOUR: 444758 + The Undeterred: + Achievement: 444759 + BONE: 444760 + EYE: 444761 + MOUTH: 444762 + IRIS: 444763 + EYE (2): 444764 + ICE: 444765 + HEIGHT: 444766 + EYE (3): 444767 + NOT: 444768 + JUST: 444769 + READ: 444770 + FATHER: 444771 + FEATHER: 444772 + CONTINENT: 444773 + OCEAN: 444774 + WALL: 444775 + Number Hunt: + FIVE: 444776 + SIX: 444777 + SEVEN: 444778 + EIGHT: 444779 + NINE: 444780 + Directional Gallery: + PEPPER: 444781 + TURN: 444782 + LEARN: 444783 + FIVE (1): 444784 + FIVE (2): 444785 + SIX (1): 444786 + SIX (2): 444787 + SEVEN: 444788 + EIGHT: 444789 + NINE: 444790 + BACKSIDE: 444791 + '834283054': 444792 + PARANOID: 444793 + YELLOW: 444794 + WADED + WEE: 444795 + THE EYES: 444796 + LEFT: 444797 + RIGHT: 444798 + MIDDLE: 444799 + WARD: 444800 + HIND: 444801 + RIG: 444802 + WINDWARD: 444803 + LIGHT: 444804 + REWIND: 444805 + Champion's Rest: + EXIT: 444806 + HUES: 444807 + RED: 444808 + BLUE: 444809 + YELLOW: 444810 + GREEN: 444811 + PURPLE: 444812 + ORANGE: 444813 + YOU: 444814 + ME: 444815 + SECRET BLUE: 444816 + SECRET YELLOW: 444817 + SECRET RED: 444818 + The Bearer: + Achievement: 444819 + MIDDLE: 444820 + FARTHER: 444821 + BACKSIDE: 444822 + PART: 444823 + HEART: 444824 + The Bearer (East): + SIX: 444825 + PEACE: 444826 + The Bearer (North): + SILENT (1): 444827 + SILENT (2): 444828 + SPACE: 444829 + WARTS: 444830 + The Bearer (South): + SIX: 444831 + TENT: 444832 + BOWL: 444833 + The Bearer (West): + SNOW: 444834 + SMILE: 444835 + Bearer Side Area: + SHORTCUT: 444836 + POTS: 444837 + Cross Tower (East): + WINTER: 444838 + Cross Tower (North): + NORTH: 444839 + Cross Tower (South): + FIRE: 444840 + Cross Tower (West): + DIAMONDS: 444841 + The Steady (Rose): + SOAR: 444842 + The Steady (Ruby): + BURY: 444843 + The Steady (Carnation): + INCARNATION: 444844 + The Steady (Sunflower): + SUN: 444845 + The Steady (Plum): + LUMP: 444846 + The Steady (Lime): + LIMELIGHT: 444847 + The Steady (Lemon): + MELON: 444848 + The Steady (Topaz): + TOP: 444849 + MASTERY: 444850 + The Steady (Orange): + BLUE: 444851 + The Steady (Sapphire): + SAP: 444852 + The Steady (Blueberry): + BLUE: 444853 + The Steady (Amber): + ANTECHAMBER: 444854 + The Steady (Emerald): + HERALD: 444855 + The Steady (Amethyst): + PACIFIST: 444856 + The Steady (Lilac): + LIE LACK: 444857 + The Steady (Cherry): + HAIRY: 444858 + The Steady: + Achievement: 444859 + Knight Night (Outer Ring): + NIGHT: 444860 + KNIGHT: 444861 + BEE: 444862 + NEW: 444863 + FORE: 444864 + TRUSTED (1): 444865 + TRUSTED (2): 444866 + ENCRUSTED: 444867 + ADJUST (1): 444868 + ADJUST (2): 444869 + RIGHT: 444870 + TRUST: 444871 + Knight Night (Right Upper Segment): + RUST (1): 444872 + RUST (2): 444873 + Knight Night (Right Lower Segment): + ADJUST: 444874 + BEFORE: 444875 + BE: 444876 + LEFT: 444877 + TRUST: 444878 + Knight Night (Final): + TRUSTED: 444879 + Knight Night Exit: + SEVEN (1): 444880 + SEVEN (2): 444881 + SEVEN (3): 444882 + DEAD END: 444883 + WARNER: 444884 + The Artistic (Smiley): + Achievement: 444885 + FINE: 444886 + BLADE: 444887 + RED: 444888 + BEARD: 444889 + ICE: 444890 + ROOT: 444891 + The Artistic (Panda): + EYE (Top): 444892 + EYE (Bottom): 444893 + LADYLIKE: 444894 + WATER: 444895 + OURS: 444896 + DAYS: 444897 + NIGHTTIME: 444898 + NIGHT: 444899 + The Artistic (Lattice): + POSH: 444900 + MALL: 444901 + DEICIDE: 444902 + WAVER: 444903 + REPAID: 444904 + BABY: 444905 + LOBE: 444906 + BOWELS: 444907 + The Artistic (Apple): + SPRIG: 444908 + RELEASES: 444909 + MUCH: 444910 + FISH: 444911 + MASK: 444912 + HILL: 444913 + TINE: 444914 + THING: 444915 + The Artistic (Hint Room): + THEME: 444916 + PAINTS: 444917 + I: 444918 + KIT: 444919 + The Discerning: + Achievement: 444920 + HITS: 444921 + WARRED: 444922 + REDRAW: 444923 + ADDER: 444924 + LAUGHTERS: 444925 + STONE: 444926 + ONSET: 444927 + RAT: 444928 + DUSTY: 444929 + ARTS: 444930 + TSAR: 444931 + STATE: 444932 + REACT: 444933 + DEAR: 444934 + DARE: 444935 + SEAM: 444936 + The Eyes They See: + NEAR: 444937 + EIGHT: 444938 + Far Window: + FAR: 444939 + Outside The Wondrous: + SHRINK: 444940 + The Wondrous (Bookcase): + CASE: 444941 + The Wondrous (Chandelier): + CANDLE HEIR: 444942 + The Wondrous (Window): + GLASS: 444943 + The Wondrous (Table): + WOOD: 444944 + BROOK NOD: 444945 + The Wondrous: + FIREPLACE: 444946 + Achievement: 444947 + Arrow Garden: + MASTERY: 444948 + SHARP: 444949 + Hallway Room (2): + WISE: 444950 + CLOCK: 444951 + ER: 444952 + COUNT: 444953 + Hallway Room (3): + TRANCE: 444954 + FORM: 444955 + A: 444956 + SHUN: 444957 + Hallway Room (4): + WHEEL: 444958 + Elements Area: + A: 444959 + NINE: 444960 + UNDISTRACTED: 444961 + MASTERY: 444962 + EARTH: 444963 + WATER: 444964 + AIR: 444965 + Outside The Wanderer: + WANDERLUST: 444966 + The Wanderer: + Achievement: 444967 + '7890': 444968 + '6524': 444969 + '951': 444970 + '4524': 444971 + LEARN: 444972 + DUST: 444973 + STAR: 444974 + WANDER: 444975 + Art Gallery: + EIGHT: 444976 + EON: 444977 + TRUSTWORTHY: 444978 + FREE: 444979 + OUR: 444980 + ONE ROAD MANY TURNS: 444981 + Art Gallery (Second Floor): + HOUSE: 444982 + PATH: 444983 + PARK: 444984 + CARRIAGE: 444985 + Art Gallery (Third Floor): + AN: 444986 + MAY: 444987 + ANY: 444988 + MAN: 444989 + Art Gallery (Fourth Floor): + URNS: 444990 + LEARNS: 444991 + RUNTS: 444992 + SEND - USE: 444993 + TRUST: 444994 + '062459': 444995 + Rhyme Room (Smiley): + LOANS: 444996 + SKELETON: 444997 + REPENTANCE: 444998 + WORD: 444999 + SCHEME: 445000 + FANTASY: 445001 + HISTORY: 445002 + SECRET: 445003 + Rhyme Room (Cross): + NINE: 445004 + FERN: 445005 + STAY: 445006 + FRIEND: 445007 + RISE: 445008 + PLUMP: 445009 + BOUNCE: 445010 + SCRAWL: 445011 + PLUNGE: 445012 + LEAP: 445013 + Rhyme Room (Circle): + BIRD: 445014 + LETTER: 445015 + FORBIDDEN: 445016 + CONCEALED: 445017 + VIOLENT: 445018 + MUTE: 445019 + Rhyme Room (Looped Square): + WALKED: 445020 + OBSTRUCTED: 445021 + SKIES: 445022 + SWELL: 445023 + PENNED: 445024 + CLIMB: 445025 + TROUBLE: 445026 + DUPLICATE: 445027 + Rhyme Room (Target): + WILD: 445028 + KID: 445029 + PISTOL: 445030 + QUARTZ: 445031 + INNOVATIVE (Top): 445032 + INNOVATIVE (Bottom): 445033 + Room Room: + DOOR (1): 445034 + DOOR (2): 445035 + WINDOW: 445036 + STAIRS: 445037 + PAINTING: 445038 + FLOOR (1): 445039 + FLOOR (2): 445040 + FLOOR (3): 445041 + FLOOR (4): 445042 + FLOOR (5): 445043 + FLOOR (7): 445044 + FLOOR (8): 445045 + FLOOR (9): 445046 + FLOOR (10): 445047 + CEILING (1): 445048 + CEILING (2): 445049 + CEILING (3): 445050 + CEILING (4): 445051 + CEILING (5): 445052 + WALL (1): 445053 + WALL (2): 445054 + WALL (3): 445055 + WALL (4): 445056 + WALL (5): 445057 + WALL (6): 445058 + WALL (7): 445059 + WALL (8): 445060 + WALL (9): 445061 + WALL (10): 445062 + WALL (11): 445063 + WALL (12): 445064 + WALL (13): 445065 + WALL (14): 445066 + WALL (15): 445067 + WALL (16): 445068 + WALL (17): 445069 + WALL (18): 445070 + WALL (19): 445071 + WALL (20): 445072 + WALL (21): 445073 + BROOMED: 445074 + LAYS: 445075 + BASE: 445076 + MASTERY: 445077 + Outside The Wise: + KITTEN: 445084 + CAT: 445085 + The Wise: + Achievement: 445086 + PUPPY: 445087 + ADULT: 445088 + BREAD: 445089 + DINOSAUR: 445090 + OAK: 445091 + CORPSE: 445092 + BEFORE: 445093 + YOUR: 445094 + BETWIXT: 445095 + NIGH: 445096 + CONNEXION: 445097 + THOU: 445098 + The Red: + Achievement: 445099 + PANDEMIC (1): 445100 + TRINITY: 445101 + CHEMISTRY: 445102 + FLUMMOXED: 445103 + PANDEMIC (2): 445104 + COUNTERCLOCKWISE: 445105 + FEARLESS: 445106 + DEFORESTATION: 445107 + CRAFTSMANSHIP: 445108 + CAMEL: 445109 + LION: 445110 + TIGER: 445111 + SHIP (1): 445112 + SHIP (2): 445113 + GIRAFFE: 445114 + The Ecstatic: + Achievement: 445115 + FORM (1): 445116 + WIND: 445117 + EGGS: 445118 + VEGETABLES: 445119 + WATER: 445120 + FRUITS: 445121 + LEAVES: 445122 + VINES: 445123 + ICE: 445124 + STYLE: 445125 + FIR: 445126 + REEF: 445127 + ROTS: 445128 + FORM (2): 445129 + Outside The Scientific: + OPEN: 445130 + CLOSE: 445131 + AHEAD: 445132 + The Scientific: + Achievement: 445133 + HYDROGEN (1): 445134 + OXYGEN: 445135 + HYDROGEN (2): 445136 + SUGAR (1): 445137 + SUGAR (2): 445138 + SUGAR (3): 445139 + CHLORINE: 445140 + SODIUM: 445141 + FOREST: 445142 + POUND: 445143 + ICE: 445144 + FISSION: 445145 + FUSION: 445146 + MISS: 445147 + TREE (1): 445148 + BIOGRAPHY: 445149 + CACTUS: 445150 + VERTEBRATE: 445151 + ROSE: 445152 + TREE (2): 445153 + FRUIT: 445154 + MAMMAL: 445155 + BIRD: 445156 + FISH: 445157 + GRAVELY: 445158 + BREVITY: 445159 + PART: 445160 + MATTER: 445161 + ELECTRIC: 445162 + ATOM (1): 445163 + NEUTRAL: 445164 + ATOM (2): 445165 + PROPEL: 445166 + ATOM (3): 445167 + ORDER: 445168 + OPTICS: 445169 + GRAPHITE: 445170 + HOT RYE: 445171 + SIT SHY HOPE: 445172 + ME NEXT PIER: 445173 + RUT LESS: 445174 + SON COUNCIL: 445175 + Challenge Room: + WELCOME: 445176 + CHALLENGE: 445177 + Achievement: 445178 + OPEN: 445179 + SINGED: 445180 + NEVER TRUSTED: 445181 + CORNER: 445182 + STRAWBERRIES: 445183 + GRUB: 445184 + BREAD: 445185 + COLOR: 445186 + WRITER: 445187 + '02759': 445188 + REAL EYES: 445189 + LOBS: 445190 + PEST ALLY: 445191 + GENIAL HALO: 445192 + DUCK LOGO: 445193 + AVIAN GREEN: 445194 + FEVER TEAR: 445195 + FACTS: 445196 + FACTS (1): 445197 + FACTS (3): 445198 + FACTS (4): 445199 + FACTS (5): 445200 + FACTS (6): 445201 + LAPEL SHEEP: 445202 +doors: + Starting Room: + Back Right Door: + item: 444416 + location: 444401 + Rhyme Room Entrance: + item: 444417 + Hidden Room: + Dead End Door: + item: 444419 + Knight Night Entrance: + item: 444421 + Seeker Entrance: + item: 444422 + location: 444407 + Rhyme Room Entrance: + item: 444423 + Second Room: + Exit Door: + item: 444424 + location: 445203 + Hub Room: + Crossroads Entrance: + item: 444425 + location: 444432 + Tenacious Entrance: + item: 444426 + location: 444433 + Symmetry Door: + item: 444428 + location: 445204 + Shortcut to Hedge Maze: + item: 444430 + location: 444436 + Near RAT Door: + item: 444432 + Traveled Entrance: + item: 444433 + location: 444438 + Lost Door: + item: 444435 + location: 444440 + Pilgrim Antechamber: + Sun Painting: + item: 444436 + location: 445205 + Pilgrim Room: + Shortcut to The Seeker: + item: 444437 + location: 444449 + Crossroads: + Tenacious Entrance: + item: 444438 + location: 444462 + Discerning Entrance: + item: 444439 + location: 444463 + Tower Entrance: + item: 444440 + location: 444465 + Tower Back Entrance: + item: 444442 + location: 445206 + Words Sword Door: + item: 444443 + location: 445207 + Eye Wall: + item: 444445 + location: 444469 + Hollow Hallway: + item: 444446 + Roof Access: + item: 444447 + Lost Area: + Exit: + item: 444448 + location: 445208 + Amen Name Area: + Exit: + item: 444449 + location: 445209 + The Tenacious: + Shortcut to Hub Room: + item: 444450 + location: 445210 + White Palindromes: + location: 445211 + Warts Straw Area: + Door: + item: 444451 + location: 445212 + Leaf Feel Area: + Door: + item: 444452 + location: 445213 + Outside The Agreeable: + Tenacious Entrance: + item: 444453 + location: 444496 + Black Door: + item: 444454 + location: 444497 + Agreeable Entrance: + item: 444455 + location: 444498 + Painting Shortcut: + item: 444456 + location: 444501 + Purple Barrier: + item: 444457 + Hallway Door: + item: 444459 + location: 445214 + Dread Hallway: + Tenacious Entrance: + item: 444462 + location: 444516 + The Agreeable: + Shortcut to Hedge Maze: + item: 444463 + location: 444518 + Hedge Maze: + Perceptive Entrance: + item: 444464 + location: 444530 + Painting Shortcut: + item: 444465 + Observant Entrance: + item: 444466 + Hide and Seek: + location: 445215 + The Fearless (First Floor): + Second Floor: + item: 444468 + location: 445216 + The Fearless (Second Floor): + Third Floor: + item: 444471 + location: 445217 + The Observant: + Backside Door: + item: 444472 + location: 445218 + Stairs: + item: 444474 + location: 444569 + The Incomparable: + Eight Painting: + item: 444475 + location: 445219 + Orange Tower: + Second Floor: + item: 444476 + Third Floor: + item: 444477 + Fourth Floor: + item: 444478 + Fifth Floor: + item: 444479 + Sixth Floor: + item: 444480 + Seventh Floor: + item: 444481 + Orange Tower First Floor: + Shortcut to Hub Room: + item: 444483 + location: 444602 + Salt Pepper Door: + item: 444485 + location: 445220 + Orange Tower Third Floor: + Red Barrier: + item: 444486 + Rhyme Room Entrance: + item: 444487 + Orange Barrier: + item: 444488 + location: 445221 + Orange Tower Fourth Floor: + Hot Crusts Door: + item: 444490 + location: 444610 + Orange Tower Fifth Floor: + Welcome Back: + item: 444491 + location: 445222 + Orange Tower Seventh Floor: + Mastery: + item: 444493 + Mastery Panels: + location: 445223 + Courtyard: + Painting Shortcut: + item: 444494 + Green Barrier: + item: 444495 + First Second Third Fourth: + Backside Door: + item: 444496 + location: 445224 + The Colorful (White): + Progress Door: + item: 444497 + location: 445225 + The Colorful (Black): + Progress Door: + item: 444499 + location: 445226 + The Colorful (Red): + Progress Door: + item: 444500 + location: 445227 + The Colorful (Yellow): + Progress Door: + item: 444501 + location: 445228 + The Colorful (Blue): + Progress Door: + item: 444502 + location: 445229 + The Colorful (Purple): + Progress Door: + item: 444503 + location: 445230 + The Colorful (Orange): + Progress Door: + item: 444504 + location: 445231 + The Colorful (Green): + Progress Door: + item: 444505 + location: 445232 + The Colorful (Brown): + Progress Door: + item: 444506 + location: 445233 + The Colorful (Gray): + Progress Door: + item: 444507 + location: 445234 + Welcome Back Area: + Shortcut to Starting Room: + item: 444508 + location: 444653 + Owl Hallway: + Shortcut to Hedge Maze: + item: 444509 + location: 444656 + Outside The Initiated: + Shortcut to Hub Room: + item: 444510 + location: 444664 + Blue Barrier: + item: 444511 + Orange Barrier: + item: 444512 + Initiated Entrance: + item: 444513 + location: 444665 + Green Barrier: + item: 444514 + location: 445235 + Purple Barrier: + item: 444515 + location: 445236 + Entrance: + item: 444516 + location: 445237 + The Traveled: + Color Hallways Entrance: + item: 444517 + location: 444698 + Outside The Bold: + Bold Entrance: + item: 444518 + location: 444711 + Painting Shortcut: + item: 444519 + Steady Entrance: + item: 444520 + location: 444712 + Outside The Undeterred: + Undeterred Entrance: + item: 444521 + location: 444744 + Painting Shortcut: + item: 444522 + Green Painting: + item: 444523 + Twos: + item: 444524 + location: 444752 + Threes: + item: 444525 + location: 445238 + Number Hunt: + item: 444526 + location: 445239 + Fours: + item: 444527 + Fives: + item: 444528 + location: 445240 + Challenge Entrance: + item: 444529 + location: 444751 + Number Hunt: + Door to Directional Gallery: + item: 444530 + Sixes: + item: 444532 + location: 445241 + Sevens: + item: 444533 + location: 445242 + Eights: + item: 444534 + location: 445243 + Nines: + item: 444535 + location: 445244 + Zero Door: + item: 444536 + location: 445245 + Directional Gallery: + Shortcut to The Undeterred: + item: 444537 + location: 445246 + Yellow Barrier: + item: 444538 + Champion's Rest: + Shortcut to The Steady: + item: 444539 + location: 444806 + The Bearer: + Shortcut to The Bold: + item: 444540 + location: 444820 + Backside Door: + item: 444541 + location: 444821 + Bearer Side Area: + Shortcut to Tower: + item: 444542 + location: 445247 + Knight Night (Final): + Exit: + item: 444543 + location: 445248 + The Artistic (Smiley): + Door to Panda: + item: 444544 + location: 445249 + The Artistic (Panda): + Door to Lattice: + item: 444546 + location: 445250 + The Artistic (Lattice): + Door to Apple: + item: 444547 + location: 445251 + The Artistic (Apple): + Door to Smiley: + item: 444548 + location: 445252 + The Eyes They See: + Exit: + item: 444549 + location: 444937 + Outside The Wondrous: + Wondrous Entrance: + item: 444550 + location: 444940 + The Wondrous (Doorknob): + Painting Shortcut: + item: 444551 + The Wondrous: + Exit: + item: 444552 + location: 444947 + Hallway Room (2): + Exit: + item: 444553 + location: 445253 + Hallway Room (3): + Exit: + item: 444554 + location: 445254 + Hallway Room (4): + Exit: + item: 444555 + location: 445255 + Outside The Wanderer: + Wanderer Entrance: + item: 444556 + location: 444966 + Tower Entrance: + item: 444557 + Art Gallery: + Second Floor: + item: 444558 + First Floor Puzzles: + location: 445256 + Third Floor: + item: 444559 + Fourth Floor: + item: 444560 + Fifth Floor: + item: 444561 + Exit: + item: 444562 + location: 444981 + Art Gallery (Second Floor): + Puzzles: + location: 445257 + Art Gallery (Third Floor): + Puzzles: + location: 445258 + Art Gallery (Fourth Floor): + Puzzles: + location: 445259 + Rhyme Room (Smiley): + Door to Target: + item: 444564 + Door to Target (Location): + location: 445260 + Rhyme Room (Cross): + Exit: + item: 444565 + location: 445261 + Rhyme Room (Circle): + Door to Smiley: + item: 444566 + location: 445262 + Rhyme Room (Looped Square): + Door to Circle: + item: 444567 + location: 445263 + Door to Cross: + item: 444568 + location: 445264 + Door to Target: + item: 444569 + location: 445265 + Rhyme Room (Target): + Door to Cross: + item: 444570 + location: 445266 + Room Room: + Shortcut to Fifth Floor: + item: 444571 + location: 445076 + Outside The Wise: + Wise Entrance: + item: 444572 + location: 445267 + Outside The Scientific: + Scientific Entrance: + item: 444573 + location: 445130 + The Scientific: + Chemistry Puzzles: + location: 445268 + Biology Puzzles: + location: 445269 + Physics Puzzles: + location: 445270 + Challenge Room: + Welcome Door: + item: 444574 + location: 445176 +door_groups: + Rhyme Room Doors: 444418 + Dead End Area Access: 444420 + Entrances to The Tenacious: 444427 + Symmetry Doors: 444429 + Hedge Maze Doors: 444431 + Entrance to The Traveled: 444434 + Crossroads - Tower Entrances: 444441 + Crossroads Doors: 444444 + Color Hunt Barriers: 444458 + Hallway Room Doors: 444460 + Observant Doors: 444467 + Fearless Doors: 444469 + Backside Doors: 444473 + Orange Tower First Floor - Shortcuts: 444484 + Champion's Rest - Color Barriers: 444489 + Welcome Back Doors: 444492 + Colorful Doors: 444498 + Directional Gallery Doors: 444531 + Artistic Doors: 444545 +progression: + Progressive Hallway Room: 444461 + Progressive Fearless: 444470 + Progressive Orange Tower: 444482 + Progressive Art Gallery: 444563 diff --git a/worlds/lingo/items.py b/worlds/lingo/items.py new file mode 100644 index 000000000000..af24570f278e --- /dev/null +++ b/worlds/lingo/items.py @@ -0,0 +1,106 @@ +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING + +from BaseClasses import Item, ItemClassification +from .options import ShuffleDoors +from .static_logic import DOORS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, get_door_group_item_id, \ + get_door_item_id, get_progressive_item_id, get_special_item_id + +if TYPE_CHECKING: + from . import LingoWorld + + +class ItemData(NamedTuple): + """ + ItemData for an item in Lingo + """ + code: int + classification: ItemClassification + mode: Optional[str] + door_ids: List[str] + painting_ids: List[str] + + def should_include(self, world: "LingoWorld") -> bool: + if self.mode == "colors": + return world.options.shuffle_colors > 0 + elif self.mode == "doors": + return world.options.shuffle_doors != ShuffleDoors.option_none + elif self.mode == "orange tower": + # door shuffle is on and tower isn't progressive + return world.options.shuffle_doors != ShuffleDoors.option_none \ + and not world.options.progressive_orange_tower + elif self.mode == "complex door": + return world.options.shuffle_doors == ShuffleDoors.option_complex + elif self.mode == "door group": + return world.options.shuffle_doors == ShuffleDoors.option_simple + elif self.mode == "special": + return False + else: + return True + + +class LingoItem(Item): + """ + Item from the game Lingo + """ + game: str = "Lingo" + + +ALL_ITEM_TABLE: Dict[str, ItemData] = {} + + +def load_item_data(): + global ALL_ITEM_TABLE + + for color in ["Black", "Red", "Blue", "Yellow", "Green", "Orange", "Gray", "Brown", "Purple"]: + ALL_ITEM_TABLE[color] = ItemData(get_special_item_id(color), ItemClassification.progression, + "colors", [], []) + + door_groups: Dict[str, List[str]] = {} + for room_name, doors in DOORS_BY_ROOM.items(): + for door_name, door in doors.items(): + if door.skip_item is True or door.event is True: + continue + + if door.group is None: + door_mode = "doors" + else: + door_mode = "complex door" + door_groups.setdefault(door.group, []).extend(door.door_ids) + + if room_name in PROGRESSION_BY_ROOM and door_name in PROGRESSION_BY_ROOM[room_name]: + if room_name == "Orange Tower": + door_mode = "orange tower" + else: + door_mode = "special" + + ALL_ITEM_TABLE[door.item_name] = \ + ItemData(get_door_item_id(room_name, door_name), + ItemClassification.filler if door.junk_item else ItemClassification.progression, door_mode, + door.door_ids, door.painting_ids) + + for group, group_door_ids in door_groups.items(): + ALL_ITEM_TABLE[group] = ItemData(get_door_group_item_id(group), + ItemClassification.progression, "door group", group_door_ids, []) + + special_items: Dict[str, ItemClassification] = { + ":)": ItemClassification.filler, + "The Feeling of Being Lost": ItemClassification.filler, + "Wanderlust": ItemClassification.filler, + "Empty White Hallways": ItemClassification.filler, + "Slowness Trap": ItemClassification.trap, + "Iceland Trap": ItemClassification.trap, + "Atbash Trap": ItemClassification.trap, + "Puzzle Skip": ItemClassification.useful, + } + + for item_name, classification in special_items.items(): + ALL_ITEM_TABLE[item_name] = ItemData(get_special_item_id(item_name), classification, + "special", [], []) + + for item_name in PROGRESSIVE_ITEMS: + ALL_ITEM_TABLE[item_name] = ItemData(get_progressive_item_id(item_name), + ItemClassification.progression, "special", [], []) + + +# Initialize the item data at module scope. +load_item_data() diff --git a/worlds/lingo/locations.py b/worlds/lingo/locations.py new file mode 100644 index 000000000000..5903d603ec4f --- /dev/null +++ b/worlds/lingo/locations.py @@ -0,0 +1,80 @@ +from enum import Flag, auto +from typing import Dict, List, NamedTuple + +from BaseClasses import Location +from .static_logic import DOORS_BY_ROOM, PANELS_BY_ROOM, RoomAndPanel, get_door_location_id, get_panel_location_id + + +class LocationClassification(Flag): + normal = auto() + reduced = auto() + insanity = auto() + + +class LocationData(NamedTuple): + """ + LocationData for a location in Lingo + """ + code: int + room: str + panels: List[RoomAndPanel] + classification: LocationClassification + + def panel_ids(self): + ids = set() + for panel in self.panels: + effective_room = self.room if panel.room is None else panel.room + panel_data = PANELS_BY_ROOM[effective_room][panel.panel] + ids = ids | set(panel_data.internal_ids) + return ids + + +class LingoLocation(Location): + """ + Location from the game Lingo + """ + game: str = "Lingo" + + +ALL_LOCATION_TABLE: Dict[str, LocationData] = {} + + +def load_location_data(): + global ALL_LOCATION_TABLE + + for room_name, panels in PANELS_BY_ROOM.items(): + for panel_name, panel in panels.items(): + location_name = f"{room_name} - {panel_name}" + + classification = LocationClassification.insanity + if panel.check: + classification |= LocationClassification.normal + + if not panel.exclude_reduce: + classification |= LocationClassification.reduced + + ALL_LOCATION_TABLE[location_name] = \ + LocationData(get_panel_location_id(room_name, panel_name), room_name, + [RoomAndPanel(None, panel_name)], classification) + + for room_name, doors in DOORS_BY_ROOM.items(): + for door_name, door in doors.items(): + if door.skip_location or door.event or door.panels is None: + continue + + location_name = door.location_name + classification = LocationClassification.normal + if door.include_reduce: + classification |= LocationClassification.reduced + + if location_name in ALL_LOCATION_TABLE: + new_id = ALL_LOCATION_TABLE[location_name].code + classification |= ALL_LOCATION_TABLE[location_name].classification + else: + new_id = get_door_location_id(room_name, door_name) + + ALL_LOCATION_TABLE[location_name] = LocationData(new_id, room_name, door.panels, classification) + + +# Initialize location data on the module scope. +load_location_data() diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py new file mode 100644 index 000000000000..7dc6a1389c0c --- /dev/null +++ b/worlds/lingo/options.py @@ -0,0 +1,126 @@ +from dataclasses import dataclass + +from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions + + +class ShuffleDoors(Choice): + """If on, opening doors will require their respective "keys". + In "simple", doors are sorted into logical groups, which are all opened by receiving an item. + In "complex", the items are much more granular, and will usually only open a single door each.""" + display_name = "Shuffle Doors" + option_none = 0 + option_simple = 1 + option_complex = 2 + + +class ProgressiveOrangeTower(DefaultOnToggle): + """When "Shuffle Doors" is on, this setting governs the manner in which the Orange Tower floors open up. + If off, there is an item for each floor of the tower, and each floor's item is the only one needed to access that floor. + If on, there are six progressive items, which open up the tower from the bottom floor upward. + """ + display_name = "Progressive Orange Tower" + + +class LocationChecks(Choice): + """On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for + achievement panels and a small handful of other panels. + On "reduced", many of the locations that are associated with opening doors are removed. + On "insanity", every individual panel in the game is a location check.""" + display_name = "Location Checks" + option_normal = 0 + option_reduced = 1 + option_insanity = 2 + + +class ShuffleColors(Toggle): + """If on, an item is added to the pool for every puzzle color (besides White). + You will need to unlock the requisite colors in order to be able to solve puzzles of that color.""" + display_name = "Shuffle Colors" + + +class ShufflePanels(Choice): + """If on, the puzzles on each panel are randomized. + On "rearrange", the puzzles are the same as the ones in the base game, but are placed in different areas.""" + display_name = "Shuffle Panels" + option_none = 0 + option_rearrange = 1 + + +class ShufflePaintings(Toggle): + """If on, the destination, location, and appearance of the painting warps in the game will be randomized.""" + display_name = "Shuffle Paintings" + + +class VictoryCondition(Choice): + """Change the victory condition.""" + display_name = "Victory Condition" + option_the_end = 0 + option_the_master = 1 + option_level_2 = 2 + + +class MasteryAchievements(Range): + """The number of achievements required to unlock THE MASTER. + In the base game, 21 achievements are needed. + If you include The Scientific and The Unchallenged, which are in the base game but are not counted for mastery, 23 would be required. + If you include the custom achievement (The Wanderer), 24 would be required. + """ + display_name = "Mastery Achievements" + range_start = 1 + range_end = 24 + default = 21 + + +class Level2Requirement(Range): + """The number of panel solves required to unlock LEVEL 2. + In the base game, 223 are needed. + Note that this count includes ANOTHER TRY. + """ + display_name = "Level 2 Requirement" + range_start = 2 + range_end = 800 + default = 223 + + +class EarlyColorHallways(Toggle): + """When on, a painting warp to the color hallways area will appear in the starting room. + This lets you avoid being trapped in the starting room for long periods of time when door shuffle is on.""" + display_name = "Early Color Hallways" + + +class TrapPercentage(Range): + """Replaces junk items with traps, at the specified rate.""" + display_name = "Trap Percentage" + range_start = 0 + range_end = 100 + default = 20 + + +class PuzzleSkipPercentage(Range): + """Replaces junk items with puzzle skips, at the specified rate.""" + display_name = "Puzzle Skip Percentage" + range_start = 0 + range_end = 100 + default = 20 + + +class DeathLink(Toggle): + """If on: Whenever another player on death link dies, you will be returned to the starting room.""" + display_name = "Death Link" + + +@dataclass +class LingoOptions(PerGameCommonOptions): + shuffle_doors: ShuffleDoors + progressive_orange_tower: ProgressiveOrangeTower + location_checks: LocationChecks + shuffle_colors: ShuffleColors + shuffle_panels: ShufflePanels + shuffle_paintings: ShufflePaintings + victory_condition: VictoryCondition + mastery_achievements: MasteryAchievements + level_2_requirement: Level2Requirement + early_color_hallways: EarlyColorHallways + trap_percentage: TrapPercentage + puzzle_skip_percentage: PuzzleSkipPercentage + death_link: DeathLink diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py new file mode 100644 index 000000000000..217ad91fcd23 --- /dev/null +++ b/worlds/lingo/player_logic.py @@ -0,0 +1,298 @@ +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING + +from .items import ALL_ITEM_TABLE +from .locations import ALL_LOCATION_TABLE, LocationClassification +from .options import LocationChecks, ShuffleDoors, VictoryCondition +from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \ + PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, ROOMS, \ + RoomAndPanel +from .testing import LingoTestOptions + +if TYPE_CHECKING: + from . import LingoWorld + + +class PlayerLocation(NamedTuple): + name: str + code: Optional[int] = None + panels: List[RoomAndPanel] = [] + + +class LingoPlayerLogic: + """ + Defines logic after a player's options have been applied + """ + + ITEM_BY_DOOR: Dict[str, Dict[str, str]] + + LOCATIONS_BY_ROOM: Dict[str, List[PlayerLocation]] + REAL_LOCATIONS: List[str] + + EVENT_LOC_TO_ITEM: Dict[str, str] + REAL_ITEMS: List[str] + + VICTORY_CONDITION: str + MASTERY_LOCATION: str + LEVEL_2_LOCATION: str + + PAINTING_MAPPING: Dict[str, str] + + FORCED_GOOD_ITEM: str + + def add_location(self, room: str, loc: PlayerLocation): + self.LOCATIONS_BY_ROOM.setdefault(room, []).append(loc) + + def set_door_item(self, room: str, door: str, item: str): + self.ITEM_BY_DOOR.setdefault(room, {})[door] = item + + def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"): + if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]: + if room_name == "Orange Tower" and not world.options.progressive_orange_tower: + self.set_door_item(room_name, door_data.name, door_data.item_name) + else: + progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name + self.set_door_item(room_name, door_data.name, progressive_item_name) + self.REAL_ITEMS.append(progressive_item_name) + else: + self.set_door_item(room_name, door_data.name, door_data.item_name) + + def __init__(self, world: "LingoWorld"): + self.ITEM_BY_DOOR = {} + self.LOCATIONS_BY_ROOM = {} + self.REAL_LOCATIONS = [] + self.EVENT_LOC_TO_ITEM = {} + self.REAL_ITEMS = [] + self.VICTORY_CONDITION = "" + self.MASTERY_LOCATION = "" + self.LEVEL_2_LOCATION = "" + self.PAINTING_MAPPING = {} + self.FORCED_GOOD_ITEM = "" + + door_shuffle = world.options.shuffle_doors + color_shuffle = world.options.shuffle_colors + painting_shuffle = world.options.shuffle_paintings + location_checks = world.options.location_checks + victory_condition = world.options.victory_condition + early_color_hallways = world.options.early_color_hallways + + if location_checks == LocationChecks.option_reduced and door_shuffle != ShuffleDoors.option_none: + raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not " + "be enough locations for all of the door items.") + + # Create an event for every room that represents being able to reach that room. + for room_name in ROOMS.keys(): + roomloc_name = f"{room_name} (Reached)" + self.add_location(room_name, PlayerLocation(roomloc_name, None, [])) + self.EVENT_LOC_TO_ITEM[roomloc_name] = roomloc_name + + # Create an event for every door, representing whether that door has been opened. Also create event items for + # doors that are event-only. + for room_name, room_data in DOORS_BY_ROOM.items(): + for door_name, door_data in room_data.items(): + if door_shuffle == ShuffleDoors.option_none: + itemloc_name = f"{room_name} - {door_name} (Opened)" + self.add_location(room_name, PlayerLocation(itemloc_name, None, door_data.panels)) + self.EVENT_LOC_TO_ITEM[itemloc_name] = itemloc_name + self.set_door_item(room_name, door_name, itemloc_name) + else: + # This line is duplicated from StaticLingoItems + if door_data.skip_item is False and door_data.event is False: + if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple: + # Grouped doors are handled differently if shuffle doors is on simple. + self.set_door_item(room_name, door_name, door_data.group) + else: + self.handle_non_grouped_door(room_name, door_data, world) + + if door_data.event: + self.add_location(room_name, PlayerLocation(door_data.item_name, None, door_data.panels)) + self.EVENT_LOC_TO_ITEM[door_data.item_name] = door_data.item_name + " (Opened)" + self.set_door_item(room_name, door_name, door_data.item_name + " (Opened)") + + # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. We also + # create events for each counting panel, so that we can determine when LEVEL 2 is accessible. + for room_name, room_data in PANELS_BY_ROOM.items(): + for panel_name, panel_data in room_data.items(): + if panel_data.achievement: + event_name = room_name + " - " + panel_name + " (Achieved)" + self.add_location(room_name, PlayerLocation(event_name, None, + [RoomAndPanel(room_name, panel_name)])) + self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement" + + if not panel_data.non_counting and victory_condition == VictoryCondition.option_level_2: + event_name = room_name + " - " + panel_name + " (Counted)" + self.add_location(room_name, PlayerLocation(event_name, None, + [RoomAndPanel(room_name, panel_name)])) + self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" + + # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need + # to prevent the actual victory condition from becoming a check. + self.MASTERY_LOCATION = "Orange Tower Seventh Floor - THE MASTER" + self.LEVEL_2_LOCATION = "N/A" + + if victory_condition == VictoryCondition.option_the_end: + self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END" + self.add_location("Orange Tower Seventh Floor", PlayerLocation("The End (Solved)")) + self.EVENT_LOC_TO_ITEM["The End (Solved)"] = "Victory" + elif victory_condition == VictoryCondition.option_the_master: + self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE MASTER" + self.MASTERY_LOCATION = "Orange Tower Seventh Floor - Mastery Achievements" + + self.add_location("Orange Tower Seventh Floor", PlayerLocation(self.MASTERY_LOCATION, None, [])) + self.EVENT_LOC_TO_ITEM[self.MASTERY_LOCATION] = "Victory" + elif victory_condition == VictoryCondition.option_level_2: + self.VICTORY_CONDITION = "Second Room - LEVEL 2" + self.LEVEL_2_LOCATION = "Second Room - Unlock Level 2" + + self.add_location("Second Room", PlayerLocation(self.LEVEL_2_LOCATION, None, + [RoomAndPanel("Second Room", "LEVEL 2")])) + self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory" + + # Instantiate all real locations. + location_classification = LocationClassification.normal + if location_checks == LocationChecks.option_reduced: + location_classification = LocationClassification.reduced + elif location_checks == LocationChecks.option_insanity: + location_classification = LocationClassification.insanity + + for location_name, location_data in ALL_LOCATION_TABLE.items(): + if location_name != self.VICTORY_CONDITION: + if location_classification not in location_data.classification: + continue + + self.add_location(location_data.room, PlayerLocation(location_name, location_data.code, + location_data.panels)) + self.REAL_LOCATIONS.append(location_name) + + # Instantiate all real items. + for name, item in ALL_ITEM_TABLE.items(): + if item.should_include(world): + self.REAL_ITEMS.append(name) + + # Create the paintings mapping, if painting shuffle is on. + if painting_shuffle: + # Shuffle paintings until we get something workable. + workable_paintings = False + for i in range(0, 20): + workable_paintings = self.randomize_paintings(world) + if workable_paintings: + break + + if not workable_paintings: + raise Exception("This Lingo world was unable to generate a workable painting mapping after 20 " + "iterations. This is very unlikely to happen on its own, and probably indicates some " + "kind of logic error.") + + if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \ + and not early_color_hallways and LingoTestOptions.disable_forced_good_item is False: + # If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK, + # but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right + # now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are + # no extra checks in there. We only include the entrance to the Rhyme Room when color shuffle is off and + # door shuffle is on simple, because otherwise there are no extra checks in there. + good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"] + + if not color_shuffle: + good_item_options.append("Pilgrim Room - Sun Painting") + + if door_shuffle == ShuffleDoors.option_simple: + good_item_options += ["Welcome Back Doors"] + + if not color_shuffle: + good_item_options.append("Rhyme Room Doors") + else: + good_item_options += ["Welcome Back Area - Shortcut to Starting Room"] + + for painting_obj in PAINTINGS_BY_ROOM["Starting Room"]: + if not painting_obj.enter_only or painting_obj.required_door is None: + continue + + # If painting shuffle is on, we only want to consider paintings that actually go somewhere. + if painting_shuffle and painting_obj.id not in self.PAINTING_MAPPING.keys(): + continue + + pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door] + good_item_options.append(pdoor.item_name) + + # Copied from The Witness -- remove any plandoed items from the possible good items set. + for v in world.multiworld.plando_items[world.player]: + if v.get("from_pool", True): + for item_key in {"item", "items"}: + if item_key in v: + if type(v[item_key]) is str: + if v[item_key] in good_item_options: + good_item_options.remove(v[item_key]) + elif type(v[item_key]) is dict: + for item, weight in v[item_key].items(): + if weight and item in good_item_options: + good_item_options.remove(item) + else: + # Other type of iterable + for item in v[item_key]: + if item in good_item_options: + good_item_options.remove(item) + + if len(good_item_options) > 0: + self.FORCED_GOOD_ITEM = world.random.choice(good_item_options) + self.REAL_ITEMS.remove(self.FORCED_GOOD_ITEM) + self.REAL_LOCATIONS.remove("Second Room - Good Luck") + + def randomize_paintings(self, world: "LingoWorld") -> bool: + self.PAINTING_MAPPING.clear() + + door_shuffle = world.options.shuffle_doors + + # Determine the set of exit paintings. All required-exit paintings are included, as are all + # required-when-no-doors paintings if door shuffle is off. We then fill the set with random other paintings. + chosen_exits = [] + if door_shuffle == ShuffleDoors.option_none: + chosen_exits = [painting_id for painting_id, painting in PAINTINGS.items() + if painting.required_when_no_doors] + chosen_exits += [painting_id for painting_id, painting in PAINTINGS.items() + if painting.exit_only and painting.required] + exitable = [painting_id for painting_id, painting in PAINTINGS.items() + if not painting.enter_only and not painting.disable and not painting.required] + chosen_exits += world.random.sample(exitable, PAINTING_EXITS - len(chosen_exits)) + + # Determine the set of entrance paintings. + enterable = [painting_id for painting_id, painting in PAINTINGS.items() + if not painting.exit_only and not painting.disable and painting_id not in chosen_exits] + chosen_entrances = world.random.sample(enterable, PAINTING_ENTRANCES) + + # Create a mapping from entrances to exits. + for warp_exit in chosen_exits: + warp_enter = world.random.choice(chosen_entrances) + + # Check whether this is a warp from a required painting room to another (or the same) required painting + # room. This could cause a cycle that would make certain regions inaccessible. + warp_exit_room = PAINTINGS[warp_exit].room + warp_enter_room = PAINTINGS[warp_enter].room + + required_painting_rooms = REQUIRED_PAINTING_ROOMS + if door_shuffle == ShuffleDoors.option_none: + required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS + + if warp_exit_room in required_painting_rooms and warp_enter_room in required_painting_rooms: + # This shuffling is non-workable. Start over. + return False + + chosen_entrances.remove(warp_enter) + self.PAINTING_MAPPING[warp_enter] = warp_exit + + for warp_enter in chosen_entrances: + warp_exit = world.random.choice(chosen_exits) + self.PAINTING_MAPPING[warp_enter] = warp_exit + + # The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves). + # There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the + # exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall + # is forced to point to the vanilla exit. + if "eye_painting_2" not in self.PAINTING_MAPPING.keys(): + self.PAINTING_MAPPING["eye_painting"] = "eye_painting_2" + + # Just for sanity's sake, ensure that all required painting rooms are accessed. + for painting_id, painting in PAINTINGS.items(): + if painting_id not in self.PAINTING_MAPPING.values() \ + and (painting.required or (painting.required_when_no_doors and door_shuffle == 0)): + return False + + return True diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py new file mode 100644 index 000000000000..c75cf4956d0b --- /dev/null +++ b/worlds/lingo/regions.py @@ -0,0 +1,84 @@ +from typing import Dict, TYPE_CHECKING + +from BaseClasses import ItemClassification, Region +from .items import LingoItem +from .locations import LingoLocation +from .player_logic import LingoPlayerLogic +from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda +from .static_logic import ALL_ROOMS, PAINTINGS, Room + +if TYPE_CHECKING: + from . import LingoWorld + + +def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogic) -> Region: + new_region = Region(room.name, world.player, world.multiworld) + for location in player_logic.LOCATIONS_BY_ROOM.get(room.name, {}): + new_location = LingoLocation(world.player, location.name, location.code, new_region) + new_location.access_rule = make_location_lambda(location, room.name, world, player_logic) + new_region.locations.append(new_location) + if location.name in player_logic.EVENT_LOC_TO_ITEM: + event_name = player_logic.EVENT_LOC_TO_ITEM[location.name] + event_item = LingoItem(event_name, ItemClassification.progression, None, world.player) + new_location.place_locked_item(event_item) + + return new_region + + +def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: + target_region = regions["Pilgrim Antechamber"] + source_region = regions["Outside The Agreeable"] + source_region.connect( + target_region, + "Pilgrimage", + lambda state: lingo_can_use_pilgrimage(state, world.player, player_logic)) + + +def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld", + player_logic: LingoPlayerLogic) -> None: + source_painting = PAINTINGS[warp_enter] + target_painting = PAINTINGS[warp_exit] + + target_region = regions[target_painting.room] + source_region = regions[source_painting.room] + source_region.connect( + target_region, + f"{source_painting.room} to {target_painting.room} (Painting)", + lambda state: lingo_can_use_entrance(state, target_painting.room, source_painting.required_door, world.player, + player_logic)) + + +def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: + regions = { + "Menu": Region("Menu", world.player, world.multiworld) + } + + painting_shuffle = world.options.shuffle_paintings + early_color_hallways = world.options.early_color_hallways + + # Instantiate all rooms as regions with their locations first. + for room in ALL_ROOMS: + regions[room.name] = create_region(room, world, player_logic) + + # Connect all created regions now that they exist. + for room in ALL_ROOMS: + for entrance in room.entrances: + # Don't use the vanilla painting connections if we are shuffling paintings. + if entrance.painting and painting_shuffle: + continue + + regions[entrance.room].connect( + regions[room.name], + f"{entrance.room} to {room.name}", + lambda state, r=room, e=entrance: lingo_can_use_entrance(state, r.name, e.door, world.player, player_logic)) + + handle_pilgrim_room(regions, world, player_logic) + + if early_color_hallways: + regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways") + + if painting_shuffle: + for warp_enter, warp_exit in player_logic.PAINTING_MAPPING.items(): + connect_painting(regions, warp_enter, warp_exit, world, player_logic) + + world.multiworld.regions += regions.values() diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py new file mode 100644 index 000000000000..90c889b7f098 --- /dev/null +++ b/worlds/lingo/rules.py @@ -0,0 +1,104 @@ +from typing import TYPE_CHECKING + +from BaseClasses import CollectionState +from .options import VictoryCondition +from .player_logic import LingoPlayerLogic, PlayerLocation +from .static_logic import PANELS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor + +if TYPE_CHECKING: + from . import LingoWorld + + +def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, player: int, + player_logic: LingoPlayerLogic): + if door is None: + return True + + return _lingo_can_open_door(state, room, room if door.room is None else door.room, door.door, player, player_logic) + + +def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: LingoPlayerLogic): + fake_pilgrimage = [ + ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], + ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"], + ["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"], + ["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"], + ["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"], + ["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"], + ["Outside The Agreeable", "Tenacious Entrance"] + ] + for entrance in fake_pilgrimage: + if not state.has(player_logic.ITEM_BY_DOOR[entrance[0]][entrance[1]], player): + return False + + return True + + +def lingo_can_use_location(state: CollectionState, location: PlayerLocation, room_name: str, world: "LingoWorld", + player_logic: LingoPlayerLogic): + for panel in location.panels: + panel_room = room_name if panel.room is None else panel.room + if not _lingo_can_solve_panel(state, room_name, panel_room, panel.panel, world, player_logic): + return False + + return True + + +def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): + return state.has("Mastery Achievement", world.player, world.options.mastery_achievements.value) + + +def _lingo_can_open_door(state: CollectionState, start_room: str, room: str, door: str, player: int, + player_logic: LingoPlayerLogic): + """ + Determines whether a door can be opened + """ + item_name = player_logic.ITEM_BY_DOOR[room][door] + if item_name in PROGRESSIVE_ITEMS: + progression = PROGRESSION_BY_ROOM[room][door] + return state.has(item_name, player, progression.index) + + return state.has(item_name, player) + + +def _lingo_can_solve_panel(state: CollectionState, start_room: str, room: str, panel: str, world: "LingoWorld", + player_logic: LingoPlayerLogic): + """ + Determines whether a panel can be solved + """ + if start_room != room and not state.has(f"{room} (Reached)", world.player): + return False + + if room == "Second Room" and panel == "ANOTHER TRY" \ + and world.options.victory_condition == VictoryCondition.option_level_2 \ + and not state.has("Counting Panel Solved", world.player, world.options.level_2_requirement.value - 1): + return False + + panel_object = PANELS_BY_ROOM[room][panel] + for req_room in panel_object.required_rooms: + if not state.has(f"{req_room} (Reached)", world.player): + return False + + for req_door in panel_object.required_doors: + if not _lingo_can_open_door(state, start_room, room if req_door.room is None else req_door.room, + req_door.door, world.player, player_logic): + return False + + for req_panel in panel_object.required_panels: + if not _lingo_can_solve_panel(state, start_room, room if req_panel.room is None else req_panel.room, + req_panel.panel, world, player_logic): + return False + + if len(panel_object.colors) > 0 and world.options.shuffle_colors: + for color in panel_object.colors: + if not state.has(color.capitalize(), world.player): + return False + + return True + + +def make_location_lambda(location: PlayerLocation, room_name: str, world: "LingoWorld", player_logic: LingoPlayerLogic): + if location.name == player_logic.MASTERY_LOCATION: + return lambda state: lingo_can_use_mastery_location(state, world) + + return lambda state: lingo_can_use_location(state, location, room_name, world, player_logic) diff --git a/worlds/lingo/static_logic.py b/worlds/lingo/static_logic.py new file mode 100644 index 000000000000..d122169c5d03 --- /dev/null +++ b/worlds/lingo/static_logic.py @@ -0,0 +1,544 @@ +from typing import Dict, List, NamedTuple, Optional, Set + +import yaml + + +class RoomAndDoor(NamedTuple): + room: Optional[str] + door: str + + +class RoomAndPanel(NamedTuple): + room: Optional[str] + panel: str + + +class RoomEntrance(NamedTuple): + room: str # source room + door: Optional[RoomAndDoor] + painting: bool + + +class Room(NamedTuple): + name: str + entrances: List[RoomEntrance] + + +class Door(NamedTuple): + name: str + item_name: str + location_name: Optional[str] + panels: Optional[List[RoomAndPanel]] + skip_location: bool + skip_item: bool + door_ids: List[str] + painting_ids: List[str] + event: bool + group: Optional[str] + include_reduce: bool + junk_item: bool + + +class Panel(NamedTuple): + required_rooms: List[str] + required_doors: List[RoomAndDoor] + required_panels: List[RoomAndPanel] + colors: List[str] + check: bool + event: bool + internal_ids: List[str] + exclude_reduce: bool + achievement: bool + non_counting: bool + + +class Painting(NamedTuple): + id: str + room: str + enter_only: bool + exit_only: bool + orientation: str + required: bool + required_when_no_doors: bool + required_door: Optional[RoomAndDoor] + disable: bool + move: bool + + +class Progression(NamedTuple): + item_name: str + index: int + + +ROOMS: Dict[str, Room] = {} +PANELS: Dict[str, Panel] = {} +DOORS: Dict[str, Door] = {} +PAINTINGS: Dict[str, Painting] = {} + +ALL_ROOMS: List[Room] = [] +DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {} +PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {} +PAINTINGS_BY_ROOM: Dict[str, List[Painting]] = {} + +PROGRESSIVE_ITEMS: List[str] = [] +PROGRESSION_BY_ROOM: Dict[str, Dict[str, Progression]] = {} + +PAINTING_ENTRANCES: int = 0 +PAINTING_EXIT_ROOMS: Set[str] = set() +PAINTING_EXITS: int = 0 +REQUIRED_PAINTING_ROOMS: List[str] = [] +REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS: List[str] = [] + +SPECIAL_ITEM_IDS: Dict[str, int] = {} +PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {} +DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {} +DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {} +DOOR_GROUP_ITEM_IDS: Dict[str, int] = {} +PROGRESSIVE_ITEM_IDS: Dict[str, int] = {} + + +def load_static_data(): + global PAINTING_EXITS, SPECIAL_ITEM_IDS, PANEL_LOCATION_IDS, DOOR_LOCATION_IDS, DOOR_ITEM_IDS, \ + DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS + + try: + from importlib.resources import files + except ImportError: + from importlib_resources import files + + # Load in all item and location IDs. These are broken up into groups based on the type of item/location. + with files("worlds.lingo").joinpath("ids.yaml").open() as file: + config = yaml.load(file, Loader=yaml.Loader) + + if "special_items" in config: + for item_name, item_id in config["special_items"].items(): + SPECIAL_ITEM_IDS[item_name] = item_id + + if "panels" in config: + for room_name in config["panels"].keys(): + PANEL_LOCATION_IDS[room_name] = {} + + for panel_name, location_id in config["panels"][room_name].items(): + PANEL_LOCATION_IDS[room_name][panel_name] = location_id + + if "doors" in config: + for room_name in config["doors"].keys(): + DOOR_LOCATION_IDS[room_name] = {} + DOOR_ITEM_IDS[room_name] = {} + + for door_name, door_data in config["doors"][room_name].items(): + if "location" in door_data: + DOOR_LOCATION_IDS[room_name][door_name] = door_data["location"] + + if "item" in door_data: + DOOR_ITEM_IDS[room_name][door_name] = door_data["item"] + + if "door_groups" in config: + for item_name, item_id in config["door_groups"].items(): + DOOR_GROUP_ITEM_IDS[item_name] = item_id + + if "progression" in config: + for item_name, item_id in config["progression"].items(): + PROGRESSIVE_ITEM_IDS[item_name] = item_id + + # Process the main world file. + with files("worlds.lingo").joinpath("LL1.yaml").open() as file: + config = yaml.load(file, Loader=yaml.Loader) + + for room_name, room_data in config.items(): + process_room(room_name, room_data) + + PAINTING_EXITS = len(PAINTING_EXIT_ROOMS) + + +def get_special_item_id(name: str): + if name not in SPECIAL_ITEM_IDS: + raise Exception(f"Item ID for special item {name} not found in ids.yaml.") + + return SPECIAL_ITEM_IDS[name] + + +def get_panel_location_id(room: str, name: str): + if room not in PANEL_LOCATION_IDS or name not in PANEL_LOCATION_IDS[room]: + raise Exception(f"Location ID for panel {room} - {name} not found in ids.yaml.") + + return PANEL_LOCATION_IDS[room][name] + + +def get_door_location_id(room: str, name: str): + if room not in DOOR_LOCATION_IDS or name not in DOOR_LOCATION_IDS[room]: + raise Exception(f"Location ID for door {room} - {name} not found in ids.yaml.") + + return DOOR_LOCATION_IDS[room][name] + + +def get_door_item_id(room: str, name: str): + if room not in DOOR_ITEM_IDS or name not in DOOR_ITEM_IDS[room]: + raise Exception(f"Item ID for door {room} - {name} not found in ids.yaml.") + + return DOOR_ITEM_IDS[room][name] + + +def get_door_group_item_id(name: str): + if name not in DOOR_GROUP_ITEM_IDS: + raise Exception(f"Item ID for door group {name} not found in ids.yaml.") + + return DOOR_GROUP_ITEM_IDS[name] + + +def get_progressive_item_id(name: str): + if name not in PROGRESSIVE_ITEM_IDS: + raise Exception(f"Item ID for progressive item {name} not found in ids.yaml.") + + return PROGRESSIVE_ITEM_IDS[name] + + +def process_entrance(source_room, doors, room_obj): + global PAINTING_ENTRANCES, PAINTING_EXIT_ROOMS + + # If the value of an entrance is just True, that means that the entrance is always accessible. + if doors is True: + room_obj.entrances.append(RoomEntrance(source_room, None, False)) + elif isinstance(doors, dict): + # If the value of an entrance is a dictionary, that means the entrance requires a door to be accessible, is a + # painting-based entrance, or both. + if "painting" in doors and "door" not in doors: + PAINTING_EXIT_ROOMS.add(room_obj.name) + PAINTING_ENTRANCES += 1 + + room_obj.entrances.append(RoomEntrance(source_room, None, True)) + else: + if "painting" in doors and doors["painting"]: + PAINTING_EXIT_ROOMS.add(room_obj.name) + PAINTING_ENTRANCES += 1 + + room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor( + doors["room"] if "room" in doors else None, + doors["door"] + ), doors["painting"] if "painting" in doors else False)) + else: + # If the value of an entrance is a list, then there are multiple possible doors that can give access to the + # entrance. + for door in doors: + if "painting" in door and door["painting"]: + PAINTING_EXIT_ROOMS.add(room_obj.name) + PAINTING_ENTRANCES += 1 + + room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor( + door["room"] if "room" in door else None, + door["door"] + ), door["painting"] if "painting" in door else False)) + + +def process_panel(room_name, panel_name, panel_data): + global PANELS, PANELS_BY_ROOM + + full_name = f"{room_name} - {panel_name}" + + # required_room can either be a single room or a list of rooms. + if "required_room" in panel_data: + if isinstance(panel_data["required_room"], list): + required_rooms = panel_data["required_room"] + else: + required_rooms = [panel_data["required_room"]] + else: + required_rooms = [] + + # required_door can either be a single door or a list of doors. For convenience, the room key for each door does not + # need to be specified if the door is in this room. + required_doors = list() + if "required_door" in panel_data: + if isinstance(panel_data["required_door"], dict): + door = panel_data["required_door"] + required_doors.append(RoomAndDoor( + door["room"] if "room" in door else None, + door["door"] + )) + else: + for door in panel_data["required_door"]: + required_doors.append(RoomAndDoor( + door["room"] if "room" in door else None, + door["door"] + )) + + # required_panel can either be a single panel or a list of panels. For convenience, the room key for each panel does + # not need to be specified if the panel is in this room. + required_panels = list() + if "required_panel" in panel_data: + if isinstance(panel_data["required_panel"], dict): + other_panel = panel_data["required_panel"] + required_panels.append(RoomAndPanel( + other_panel["room"] if "room" in other_panel else None, + other_panel["panel"] + )) + else: + for other_panel in panel_data["required_panel"]: + required_panels.append(RoomAndPanel( + other_panel["room"] if "room" in other_panel else None, + other_panel["panel"] + )) + + # colors can either be a single color or a list of colors. + if "colors" in panel_data: + if isinstance(panel_data["colors"], list): + colors = panel_data["colors"] + else: + colors = [panel_data["colors"]] + else: + colors = [] + + if "check" in panel_data: + check = panel_data["check"] + else: + check = False + + if "event" in panel_data: + event = panel_data["event"] + else: + event = False + + if "achievement" in panel_data: + achievement = True + else: + achievement = False + + if "exclude_reduce" in panel_data: + exclude_reduce = panel_data["exclude_reduce"] + else: + exclude_reduce = False + + if "non_counting" in panel_data: + non_counting = panel_data["non_counting"] + else: + non_counting = False + + if "id" in panel_data: + if isinstance(panel_data["id"], list): + internal_ids = panel_data["id"] + else: + internal_ids = [panel_data["id"]] + else: + internal_ids = [] + + panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, internal_ids, + exclude_reduce, achievement, non_counting) + PANELS[full_name] = panel_obj + PANELS_BY_ROOM[room_name][panel_name] = panel_obj + + +def process_door(room_name, door_name, door_data): + global DOORS, DOORS_BY_ROOM + + # The item name associated with a door can be explicitly specified in the configuration. If it is not, it is + # generated from the room and door name. + if "item_name" in door_data: + item_name = door_data["item_name"] + else: + item_name = f"{room_name} - {door_name}" + + if "skip_location" in door_data: + skip_location = door_data["skip_location"] + else: + skip_location = False + + if "skip_item" in door_data: + skip_item = door_data["skip_item"] + else: + skip_item = False + + if "event" in door_data: + event = door_data["event"] + else: + event = False + + if "include_reduce" in door_data: + include_reduce = door_data["include_reduce"] + else: + include_reduce = False + + if "junk_item" in door_data: + junk_item = door_data["junk_item"] + else: + junk_item = False + + if "group" in door_data: + group = door_data["group"] + else: + group = None + + # panels is a list of panels. Each panel can either be a simple string (the name of a panel in the current room) or + # a dictionary specifying a panel in a different room. + if "panels" in door_data: + panels = list() + for panel in door_data["panels"]: + if isinstance(panel, dict): + panels.append(RoomAndPanel(panel["room"], panel["panel"])) + else: + panels.append(RoomAndPanel(None, panel)) + else: + skip_location = True + panels = None + + # The location name associated with a door can be explicitly specified in the configuration. If it is not, then the + # name is generated using a combination of all of the panels that would ordinarily open the door. This can get quite + # messy if there are a lot of panels, especially if panels from multiple rooms are involved, so in these cases it + # would be better to specify a name. + if "location_name" in door_data: + location_name = door_data["location_name"] + elif skip_location is False: + panel_per_room = dict() + for panel in panels: + panel_room_name = room_name if panel.room is None else panel.room + panel_per_room.setdefault(panel_room_name, []).append(panel.panel) + + room_strs = list() + for door_room_str, door_panels_str in panel_per_room.items(): + room_strs.append(door_room_str + " - " + ", ".join(door_panels_str)) + + location_name = " and ".join(room_strs) + else: + location_name = None + + # The id field can be a single item, or a list of door IDs, in the event that the item for this logical door should + # open more than one actual in-game door. + if "id" in door_data: + if isinstance(door_data["id"], list): + door_ids = door_data["id"] + else: + door_ids = [door_data["id"]] + else: + door_ids = [] + + # The painting_id field can be a single item, or a list of painting IDs, in the event that the item for this logical + # door should move more than one actual in-game painting. + if "painting_id" in door_data: + if isinstance(door_data["painting_id"], list): + painting_ids = door_data["painting_id"] + else: + painting_ids = [door_data["painting_id"]] + else: + painting_ids = [] + + door_obj = Door(door_name, item_name, location_name, panels, skip_location, skip_item, door_ids, + painting_ids, event, group, include_reduce, junk_item) + + DOORS[door_obj.item_name] = door_obj + DOORS_BY_ROOM[room_name][door_name] = door_obj + + +def process_painting(room_name, painting_data): + global PAINTINGS, PAINTINGS_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS + + # Read in information about this painting and store it in an object. + painting_id = painting_data["id"] + + if "orientation" in painting_data: + orientation = painting_data["orientation"] + else: + orientation = "" + + if "disable" in painting_data: + disable_painting = painting_data["disable"] + else: + disable_painting = False + + if "required" in painting_data: + required_painting = painting_data["required"] + if required_painting: + REQUIRED_PAINTING_ROOMS.append(room_name) + else: + required_painting = False + + if "move" in painting_data: + move_painting = painting_data["move"] + else: + move_painting = False + + if "required_when_no_doors" in painting_data: + rwnd = painting_data["required_when_no_doors"] + if rwnd: + REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS.append(room_name) + else: + rwnd = False + + if "exit_only" in painting_data: + exit_only = painting_data["exit_only"] + else: + exit_only = False + + if "enter_only" in painting_data: + enter_only = painting_data["enter_only"] + else: + enter_only = False + + required_door = None + if "required_door" in painting_data: + door = painting_data["required_door"] + required_door = RoomAndDoor( + door["room"] if "room" in door else room_name, + door["door"] + ) + + painting_obj = Painting(painting_id, room_name, enter_only, exit_only, orientation, + required_painting, rwnd, required_door, disable_painting, move_painting) + PAINTINGS[painting_id] = painting_obj + PAINTINGS_BY_ROOM[room_name].append(painting_obj) + + +def process_progression(room_name, progression_name, progression_doors): + global PROGRESSIVE_ITEMS, PROGRESSION_BY_ROOM + + # Progressive items are configured as a list of doors. + PROGRESSIVE_ITEMS.append(progression_name) + + progression_index = 1 + for door in progression_doors: + if isinstance(door, Dict): + door_room = door["room"] + door_door = door["door"] + else: + door_room = room_name + door_door = door + + room_progressions = PROGRESSION_BY_ROOM.setdefault(door_room, {}) + room_progressions[door_door] = Progression(progression_name, progression_index) + progression_index += 1 + + +def process_room(room_name, room_data): + global ROOMS, ALL_ROOMS + + room_obj = Room(room_name, []) + + if "entrances" in room_data: + for source_room, doors in room_data["entrances"].items(): + process_entrance(source_room, doors, room_obj) + + if "panels" in room_data: + PANELS_BY_ROOM[room_name] = dict() + + for panel_name, panel_data in room_data["panels"].items(): + process_panel(room_name, panel_name, panel_data) + + if "doors" in room_data: + DOORS_BY_ROOM[room_name] = dict() + + for door_name, door_data in room_data["doors"].items(): + process_door(room_name, door_name, door_data) + + if "paintings" in room_data: + PAINTINGS_BY_ROOM[room_name] = [] + + for painting_data in room_data["paintings"]: + process_painting(room_name, painting_data) + + if "progression" in room_data: + for progression_name, progression_doors in room_data["progression"].items(): + process_progression(room_name, progression_name, progression_doors) + + ROOMS[room_name] = room_obj + ALL_ROOMS.append(room_obj) + + +# Initialize the static data at module scope. +load_static_data() diff --git a/worlds/lingo/test/TestDoors.py b/worlds/lingo/test/TestDoors.py new file mode 100644 index 000000000000..5dc989af5989 --- /dev/null +++ b/worlds/lingo/test/TestDoors.py @@ -0,0 +1,89 @@ +from . import LingoTestBase + + +class TestRequiredRoomLogic(LingoTestBase): + options = { + "shuffle_doors": "complex" + } + + def test_pilgrim_first(self) -> None: + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Pilgrim Room - Sun Painting") + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Pilgrim Room - Shortcut to The Seeker") + self.assertTrue(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Starting Room - Back Right Door") + self.assertTrue(self.can_reach_location("The Seeker - Achievement")) + + def test_hidden_first(self) -> None: + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Starting Room - Back Right Door") + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Pilgrim Room - Shortcut to The Seeker") + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Pilgrim Room - Sun Painting") + self.assertTrue(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertTrue(self.can_reach_location("The Seeker - Achievement")) + + +class TestRequiredDoorLogic(LingoTestBase): + options = { + "shuffle_doors": "complex" + } + + def test_through_rhyme(self) -> None: + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + self.collect_by_name("Starting Room - Rhyme Room Entrance") + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + self.collect_by_name("Rhyme Room (Looped Square) - Door to Circle") + self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + def test_through_hidden(self) -> None: + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + self.collect_by_name("Starting Room - Rhyme Room Entrance") + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + self.collect_by_name("Starting Room - Back Right Door") + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + self.collect_by_name("Hidden Room - Rhyme Room Entrance") + self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + +class TestSimpleDoors(LingoTestBase): + options = { + "shuffle_doors": "simple" + } + + def test_requirement(self): + self.assertFalse(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + + self.collect_by_name("Rhyme Room Doors") + self.assertTrue(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + diff --git a/worlds/lingo/test/TestMastery.py b/worlds/lingo/test/TestMastery.py new file mode 100644 index 000000000000..3fb3c95a0208 --- /dev/null +++ b/worlds/lingo/test/TestMastery.py @@ -0,0 +1,39 @@ +from . import LingoTestBase + + +class TestMasteryWhenVictoryIsTheEnd(LingoTestBase): + options = { + "mastery_achievements": "22", + "victory_condition": "the_end", + "shuffle_colors": "true" + } + + def test_requirement(self): + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect_by_name(["Red", "Blue", "Black", "Purple", "Orange"]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + self.assertTrue(self.can_reach_location("The End (Solved)")) + self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - THE MASTER")) + + self.collect_by_name(["Green", "Brown", "Yellow"]) + self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - THE MASTER")) + + +class TestMasteryWhenVictoryIsTheMaster(LingoTestBase): + options = { + "mastery_achievements": "24", + "victory_condition": "the_master", + "shuffle_colors": "true" + } + + def test_requirement(self): + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect_by_name(["Red", "Blue", "Black", "Purple", "Orange"]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - THE END")) + self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements")) + + self.collect_by_name(["Green", "Gray", "Brown", "Yellow"]) + self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements")) \ No newline at end of file diff --git a/worlds/lingo/test/TestOptions.py b/worlds/lingo/test/TestOptions.py new file mode 100644 index 000000000000..176967786243 --- /dev/null +++ b/worlds/lingo/test/TestOptions.py @@ -0,0 +1,31 @@ +from . import LingoTestBase + + +class TestMultiShuffleOptions(LingoTestBase): + options = { + "shuffle_doors": "complex", + "progressive_orange_tower": "true", + "shuffle_colors": "true", + "shuffle_paintings": "true", + "early_color_hallways": "true" + } + + +class TestPanelsanity(LingoTestBase): + options = { + "shuffle_doors": "complex", + "progressive_orange_tower": "true", + "location_checks": "insanity", + "shuffle_colors": "true" + } + + +class TestAllPanelHunt(LingoTestBase): + options = { + "shuffle_doors": "complex", + "progressive_orange_tower": "true", + "shuffle_colors": "true", + "victory_condition": "level_2", + "level_2_requirement": "800", + "early_color_hallways": "true" + } diff --git a/worlds/lingo/test/TestOrangeTower.py b/worlds/lingo/test/TestOrangeTower.py new file mode 100644 index 000000000000..7b0c3bb52518 --- /dev/null +++ b/worlds/lingo/test/TestOrangeTower.py @@ -0,0 +1,175 @@ +from . import LingoTestBase + + +class TestProgressiveOrangeTower(LingoTestBase): + options = { + "shuffle_doors": "complex", + "progressive_orange_tower": "true" + } + + def test_from_welcome_back(self) -> None: + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect_by_name("Welcome Back Area - Shortcut to Starting Room") + self.collect_by_name("Orange Tower Fifth Floor - Welcome Back") + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + progressive_tower = self.get_items_by_name("Progressive Orange Tower") + + self.collect(progressive_tower[0]) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[1]) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[2]) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[3]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[4]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[5]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + def test_from_hub_room(self) -> None: + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect_by_name("Second Room - Exit Door") + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect_by_name("Orange Tower First Floor - Shortcut to Hub Room") + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + progressive_tower = self.get_items_by_name("Progressive Orange Tower") + + self.collect(progressive_tower[0]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.remove(self.get_item_by_name("Orange Tower First Floor - Shortcut to Hub Room")) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[1]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[2]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[3]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[4]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[5]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) diff --git a/worlds/lingo/test/TestProgressive.py b/worlds/lingo/test/TestProgressive.py new file mode 100644 index 000000000000..026971c45d65 --- /dev/null +++ b/worlds/lingo/test/TestProgressive.py @@ -0,0 +1,191 @@ +from . import LingoTestBase + + +class TestComplexProgressiveHallwayRoom(LingoTestBase): + options = { + "shuffle_doors": "complex" + } + + def test_item(self): + self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + self.collect_by_name(["Second Room - Exit Door", "The Tenacious - Shortcut to Hub Room", + "Outside The Agreeable - Tenacious Entrance"]) + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + progressive_hallway_room = self.get_items_by_name("Progressive Hallway Room") + + self.collect(progressive_hallway_room[0]) + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + self.collect(progressive_hallway_room[1]) + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + self.collect(progressive_hallway_room[2]) + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + self.collect(progressive_hallway_room[3]) + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + +class TestSimpleHallwayRoom(LingoTestBase): + options = { + "shuffle_doors": "simple" + } + + def test_item(self): + self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + self.collect_by_name(["Second Room - Exit Door", "Entrances to The Tenacious"]) + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + self.collect_by_name("Hallway Room Doors") + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + +class TestProgressiveArtGallery(LingoTestBase): + options = { + "shuffle_doors": "complex" + } + + def test_item(self): + self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect_by_name(["Second Room - Exit Door", "Crossroads - Tower Entrance", + "Orange Tower Fourth Floor - Hot Crusts Door"]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + progressive_gallery_room = self.get_items_by_name("Progressive Art Gallery") + + self.collect(progressive_gallery_room[0]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect(progressive_gallery_room[1]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect(progressive_gallery_room[2]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect(progressive_gallery_room[3]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect(progressive_gallery_room[4]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + +class TestNoDoorsArtGallery(LingoTestBase): + options = { + "shuffle_doors": "none", + "shuffle_colors": "true" + } + + def test_item(self): + self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect_by_name("Yellow") + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect_by_name("Brown") + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect_by_name("Blue") + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect_by_name(["Orange", "Gray"]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) diff --git a/worlds/lingo/test/__init__.py b/worlds/lingo/test/__init__.py new file mode 100644 index 000000000000..ffbf9032b64a --- /dev/null +++ b/worlds/lingo/test/__init__.py @@ -0,0 +1,13 @@ +from typing import ClassVar + +from test.bases import WorldTestBase +from .. import LingoTestOptions + + +class LingoTestBase(WorldTestBase): + game = "Lingo" + player: ClassVar[int] = 1 + + def world_setup(self, *args, **kwargs): + LingoTestOptions.disable_forced_good_item = True + super().world_setup(*args, **kwargs) diff --git a/worlds/lingo/testing.py b/worlds/lingo/testing.py new file mode 100644 index 000000000000..22fafea0fc6a --- /dev/null +++ b/worlds/lingo/testing.py @@ -0,0 +1,2 @@ +class LingoTestOptions: + disable_forced_good_item: bool = False diff --git a/worlds/lingo/utils/assign_ids.rb b/worlds/lingo/utils/assign_ids.rb new file mode 100644 index 000000000000..9e1ce67bd2db --- /dev/null +++ b/worlds/lingo/utils/assign_ids.rb @@ -0,0 +1,178 @@ +# This utility goes through the provided Lingo config and assigns item and +# location IDs to entities that require them (such as doors and panels). These +# IDs are output in a separate yaml file. If the output file already exists, +# then it will be updated with any newly assigned IDs rather than overwritten. +# In this event, all new IDs will be greater than any already existing IDs, +# even if there are gaps in the ID space; this is to prevent collision when IDs +# are retired. +# +# This utility should be run whenever logically new items or locations are +# required. If an item or location is created that is logically equivalent to +# one that used to exist, this utility should not be used, and instead the ID +# file should be manually edited so that the old ID can be reused. + +require 'set' +require 'yaml' + +configpath = ARGV[0] +outputpath = ARGV[1] + +next_item_id = 444400 +next_location_id = 444400 + +location_id_by_name = {} + +old_generated = YAML.load_file(outputpath) +File.write(outputpath + ".old", old_generated.to_yaml) + +if old_generated.include? "special_items" then + old_generated["special_items"].each do |name, id| + if id >= next_item_id then + next_item_id = id + 1 + end + end +end +if old_generated.include? "special_locations" then + old_generated["special_locations"].each do |name, id| + if id >= next_location_id then + next_location_id = id + 1 + end + end +end +if old_generated.include? "panels" then + old_generated["panels"].each do |room, panels| + panels.each do |name, id| + if id >= next_location_id then + next_location_id = id + 1 + end + location_name = "#{room} - #{name}" + location_id_by_name[location_name] = id + end + end +end +if old_generated.include? "doors" then + old_generated["doors"].each do |room, doors| + doors.each do |name, ids| + if ids.include? "location" then + if ids["location"] >= next_location_id then + next_location_id = ids["location"] + 1 + end + end + if ids.include? "item" then + if ids["item"] >= next_item_id then + next_item_id = ids["item"] + 1 + end + end + end + end +end +if old_generated.include? "door_groups" then + old_generated["door_groups"].each do |name, id| + if id >= next_item_id then + next_item_id = id + 1 + end + end +end +if old_generated.include? "progression" then + old_generated["progression"].each do |name, id| + if id >= next_item_id then + next_item_id = id + 1 + end + end +end + +door_groups = Set[] + +config = YAML.load_file(configpath) +config.each do |room_name, room_data| + if room_data.include? "panels" + room_data["panels"].each do |panel_name, panel| + unless old_generated.include? "panels" and old_generated["panels"].include? room_name and old_generated["panels"][room_name].include? panel_name then + old_generated["panels"] ||= {} + old_generated["panels"][room_name] ||= {} + old_generated["panels"][room_name][panel_name] = next_location_id + + location_name = "#{room_name} - #{panel_name}" + location_id_by_name[location_name] = next_location_id + + next_location_id += 1 + end + end + end +end + +config.each do |room_name, room_data| + if room_data.include? "doors" + room_data["doors"].each do |door_name, door| + if door.include? "event" and door["event"] then + next + end + + unless door.include? "skip_item" and door["skip_item"] then + unless old_generated.include? "doors" and old_generated["doors"].include? room_name and old_generated["doors"][room_name].include? door_name and old_generated["doors"][room_name][door_name].include? "item" then + old_generated["doors"] ||= {} + old_generated["doors"][room_name] ||= {} + old_generated["doors"][room_name][door_name] ||= {} + old_generated["doors"][room_name][door_name]["item"] = next_item_id + + next_item_id += 1 + end + + if door.include? "group" and not door_groups.include? door["group"] then + door_groups.add(door["group"]) + + unless old_generated.include? "door_groups" and old_generated["door_groups"].include? door["group"] then + old_generated["door_groups"] ||= {} + old_generated["door_groups"][door["group"]] = next_item_id + + next_item_id += 1 + end + end + end + + unless door.include? "skip_location" and door["skip_location"] then + location_name = "" + if door.include? "location_name" then + location_name = door["location_name"] + elsif door.include? "panels" then + location_name = door["panels"].map do |panel| + if panel.kind_of? Hash then + panel + else + {"room" => room_name, "panel" => panel} + end + end.sort_by {|panel| panel["room"]}.chunk {|panel| panel["room"]}.map do |room_panels| + room_panels[0] + " - " + room_panels[1].map{|panel| panel["panel"]}.join(", ") + end.join(" and ") + end + + if location_id_by_name.has_key? location_name then + old_generated["doors"] ||= {} + old_generated["doors"][room_name] ||= {} + old_generated["doors"][room_name][door_name] ||= {} + old_generated["doors"][room_name][door_name]["location"] = location_id_by_name[location_name] + elsif not (old_generated.include? "doors" and old_generated["doors"].include? room_name and old_generated["doors"][room_name].include? door_name and old_generated["doors"][room_name][door_name].include? "location") then + old_generated["doors"] ||= {} + old_generated["doors"][room_name] ||= {} + old_generated["doors"][room_name][door_name] ||= {} + old_generated["doors"][room_name][door_name]["location"] = next_location_id + + next_location_id += 1 + end + end + end + end + + if room_data.include? "progression" + room_data["progression"].each do |progression_name, pdata| + unless old_generated.include? "progression" and old_generated["progression"].include? progression_name then + old_generated["progression"] ||= {} + old_generated["progression"][progression_name] = next_item_id + + next_item_id += 1 + end + end + end +end + +File.write(outputpath, old_generated.to_yaml) diff --git a/worlds/lingo/utils/validate_config.rb b/worlds/lingo/utils/validate_config.rb new file mode 100644 index 000000000000..ed2e9058f9ad --- /dev/null +++ b/worlds/lingo/utils/validate_config.rb @@ -0,0 +1,329 @@ +# Script to validate a level config file. This checks that the names used within +# the file are consistent. It also checks that the panel and door IDs mentioned +# all exist in the map file. +# +# Usage: validate_config.rb [config file] [map file] + +require 'set' +require 'yaml' + +configpath = ARGV[0] +mappath = ARGV[1] + +panels = Set["Countdown Panels/Panel_1234567890_wanderlust"] +doors = Set["Naps Room Doors/Door_hider_new1", "Tower Room Area Doors/Door_wanderer_entrance"] +paintings = Set[] + +File.readlines(mappath).each do |line| + line.match(/node name=\"(.*)\" parent=\"Panels\/(.*)\" instance/) do |m| + panels.add(m[2] + "/" + m[1]) + end + line.match(/node name=\"(.*)\" parent=\"Doors\/(.*)\" instance/) do |m| + doors.add(m[2] + "/" + m[1]) + end + line.match(/node name=\"(.*)\" parent=\"Decorations\/Paintings\" instance/) do |m| + paintings.add(m[1]) + end + line.match(/node name=\"(.*)\" parent=\"Decorations\/EndPanel\" instance/) do |m| + panels.add("EndPanel/" + m[1]) + end +end + +configured_rooms = Set["Menu"] +configured_doors = Set[] +configured_panels = Set[] + +mentioned_rooms = Set[] +mentioned_doors = Set[] +mentioned_panels = Set[] + +door_groups = {} + +directives = Set["entrances", "panels", "doors", "paintings", "progression"] +panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting"] +door_directives = Set["id", "painting_id", "panels", "item_name", "location_name", "skip_location", "skip_item", "group", "include_reduce", "junk_item", "event"] +painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move"] + +non_counting = 0 + +config = YAML.load_file(configpath) +config.each do |room_name, room| + configured_rooms.add(room_name) + + used_directives = Set[] + room.each_key do |key| + used_directives.add(key) + end + diff_directives = used_directives - directives + unless diff_directives.empty? then + puts("#{room_name} has the following invalid top-level directives: #{diff_directives.to_s}") + end + + (room["entrances"] || {}).each do |source_room, entrance| + mentioned_rooms.add(source_room) + + entrances = [] + if entrance.kind_of? Hash + if entrance.keys() != ["painting"] then + entrances = [entrance] + end + elsif entrance.kind_of? Array + entrances = entrance + end + + entrances.each do |e| + entrance_room = e.include?("room") ? e["room"] : room_name + mentioned_rooms.add(entrance_room) + mentioned_doors.add(entrance_room + " - " + e["door"]) + end + end + + (room["panels"] || {}).each do |panel_name, panel| + unless panel_name.kind_of? String then + puts "#{room_name} has an invalid panel name" + end + + configured_panels.add(room_name + " - " + panel_name) + + if panel.include?("id") + panel_ids = [] + if panel["id"].kind_of? Array + panel_ids = panel["id"] + else + panel_ids = [panel["id"]] + end + + panel_ids.each do |panel_id| + unless panels.include? panel_id then + puts "#{room_name} - #{panel_name} :::: Invalid Panel ID #{panel_id}" + end + end + else + puts "#{room_name} - #{panel_name} :::: Panel is missing an ID" + end + + if panel.include?("required_room") + required_rooms = [] + if panel["required_room"].kind_of? Array + required_rooms = panel["required_room"] + else + required_rooms = [panel["required_room"]] + end + + required_rooms.each do |required_room| + mentioned_rooms.add(required_room) + end + end + + if panel.include?("required_door") + required_doors = [] + if panel["required_door"].kind_of? Array + required_doors = panel["required_door"] + else + required_doors = [panel["required_door"]] + end + + required_doors.each do |required_door| + other_room = required_door.include?("room") ? required_door["room"] : room_name + mentioned_rooms.add(other_room) + mentioned_doors.add("#{other_room} - #{required_door["door"]}") + end + end + + if panel.include?("required_panel") + required_panels = [] + if panel["required_panel"].kind_of? Array + required_panels = panel["required_panel"] + else + required_panels = [panel["required_panel"]] + end + + required_panels.each do |required_panel| + other_room = required_panel.include?("room") ? required_panel["room"] : room_name + mentioned_rooms.add(other_room) + mentioned_panels.add("#{other_room} - #{required_panel["panel"]}") + end + end + + unless panel.include?("tag") then + puts "#{room_name} - #{panel_name} :::: Panel is missing a tag" + end + + if panel.include?("non_counting") then + non_counting += 1 + end + + bad_subdirectives = [] + panel.keys.each do |key| + unless panel_directives.include?(key) then + bad_subdirectives << key + end + end + unless bad_subdirectives.empty? then + puts "#{room_name} - #{panel_name} :::: Panel has the following invalid subdirectives: #{bad_subdirectives.join(", ")}" + end + end + + (room["doors"] || {}).each do |door_name, door| + configured_doors.add("#{room_name} - #{door_name}") + + if door.include?("id") + door_ids = [] + if door["id"].kind_of? Array + door_ids = door["id"] + else + door_ids = [door["id"]] + end + + door_ids.each do |door_id| + unless doors.include? door_id then + puts "#{room_name} - #{door_name} :::: Invalid Door ID #{door_id}" + end + end + end + + if door.include?("painting_id") + painting_ids = [] + if door["painting_id"].kind_of? Array + painting_ids = door["painting_id"] + else + painting_ids = [door["painting_id"]] + end + + painting_ids.each do |painting_id| + unless paintings.include? painting_id then + puts "#{room_name} - #{door_name} :::: Invalid Painting ID #{painting_id}" + end + end + end + + if not door.include?("id") and not door.include?("painting_id") and not door["skip_item"] and not door["event"] then + puts "#{room_name} - #{door_name} :::: Should be marked skip_item or event if there are no doors or paintings" + end + + if door.include?("panels") + door["panels"].each do |panel| + if panel.kind_of? Hash then + other_room = panel.include?("room") ? panel["room"] : room_name + mentioned_panels.add("#{other_room} - #{panel["panel"]}") + else + other_room = panel.include?("room") ? panel["room"] : room_name + mentioned_panels.add("#{room_name} - #{panel}") + end + end + elsif not door["skip_location"] + puts "#{room_name} - #{door_name} :::: Should be marked skip_location if there are no panels" + end + + if door.include?("group") + door_groups[door["group"]] ||= 0 + door_groups[door["group"]] += 1 + end + + bad_subdirectives = [] + door.keys.each do |key| + unless door_directives.include?(key) then + bad_subdirectives << key + end + end + unless bad_subdirectives.empty? then + puts "#{room_name} - #{door_name} :::: Door has the following invalid subdirectives: #{bad_subdirectives.join(", ")}" + end + end + + (room["paintings"] || []).each do |painting| + if painting.include?("id") and painting["id"].kind_of? String then + unless paintings.include? painting["id"] then + puts "#{room_name} :::: Invalid Painting ID #{painting["id"]}" + end + else + puts "#{room_name} :::: Painting is missing an ID" + end + + if painting["disable"] then + # We're good. + next + end + + if painting.include?("orientation") then + unless ["north", "south", "east", "west"].include? painting["orientation"] then + puts "#{room_name} - #{painting["id"] || "painting"} :::: Invalid orientation #{painting["orientation"]}" + end + else + puts "#{room_name} :::: Painting is missing an orientation" + end + + if painting.include?("required_door") + other_room = painting["required_door"].include?("room") ? painting["required_door"]["room"] : room_name + mentioned_doors.add("#{other_room} - #{painting["required_door"]["door"]}") + + unless painting["enter_only"] then + puts "#{room_name} - #{painting["id"] || "painting"} :::: Should be marked enter_only if there is a required_door" + end + end + + bad_subdirectives = [] + painting.keys.each do |key| + unless painting_directives.include?(key) then + bad_subdirectives << key + end + end + unless bad_subdirectives.empty? then + puts "#{room_name} - #{painting["id"] || "painting"} :::: Painting has the following invalid subdirectives: #{bad_subdirectives.join(", ")}" + end + end + + (room["progression"] || {}).each do |progression_name, door_list| + door_list.each do |door| + if door.kind_of? Hash then + mentioned_doors.add("#{door["room"]} - #{door["door"]}") + else + mentioned_doors.add("#{room_name} - #{door}") + end + end + end +end + +errored_rooms = mentioned_rooms - configured_rooms +unless errored_rooms.empty? then + puts "The folloring rooms are mentioned but do not exist: " + errored_rooms.to_s +end + +errored_panels = mentioned_panels - configured_panels +unless errored_panels.empty? then + puts "The folloring panels are mentioned but do not exist: " + errored_panels.to_s +end + +errored_doors = mentioned_doors - configured_doors +unless errored_doors.empty? then + puts "The folloring doors are mentioned but do not exist: " + errored_doors.to_s +end + +door_groups.each do |group,num| + if num == 1 then + puts "Door group \"#{group}\" only has one door in it" + end +end + +slashed_rooms = configured_rooms.select do |room| + room.include? "/" +end +unless slashed_rooms.empty? then + puts "The following rooms have slashes in their names: " + slashed_rooms.to_s +end + +slashed_panels = configured_panels.select do |panel| + panel.include? "/" +end +unless slashed_panels.empty? then + puts "The following panels have slashes in their names: " + slashed_panels.to_s +end + +slashed_doors = configured_doors.select do |door| + door.include? "/" +end +unless slashed_doors.empty? then + puts "The following doors have slashes in their names: " + slashed_doors.to_s +end + +puts "#{configured_panels.size} panels (#{non_counting} non counting)" From b5bd95771d7e89422205040a76f124660633c2e6 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 9 Nov 2023 08:47:36 +0100 Subject: [PATCH 010/104] Raft: Use world.random instead of global random (#2439) --- worlds/raft/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index fec60c3bd51b..8e4eda09e10f 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -1,5 +1,4 @@ import typing -import random from .Locations import location_table, lookup_name_to_id as locations_lookup_name_to_id from .Items import (createResourcePackName, item_table, progressive_table, progressive_item_list, @@ -100,7 +99,7 @@ def create_items(self): extraItemNamePool.append(item["name"]) if (len(extraItemNamePool) > 0): - for randomItem in random.choices(extraItemNamePool, k=extras): + for randomItem in self.random.choices(extraItemNamePool, k=extras): raft_item = self.create_item_replaceAsNecessary(randomItem) pool.append(raft_item) @@ -194,7 +193,7 @@ def pre_fill(self): previousLocation = "RadioTower" while (len(availableLocationList) > 0): if (len(availableLocationList) > 1): - currentLocation = availableLocationList[random.randint(0, len(availableLocationList) - 2)] + currentLocation = availableLocationList[self.random.randint(0, len(availableLocationList) - 2)] else: currentLocation = availableLocationList[0] # Utopia (only one left in list) availableLocationList.remove(currentLocation) @@ -212,7 +211,7 @@ def setLocationItem(self, location: str, itemName: str): def setLocationItemFromRegion(self, region: str, itemName: str): itemToUse = next(filter(lambda itm: itm.name == itemName, self.multiworld.raft_frequencyItemsPerPlayer[self.player])) self.multiworld.raft_frequencyItemsPerPlayer[self.player].remove(itemToUse) - location = random.choice(list(loc for loc in location_table if loc["region"] == region)) + location = self.random.choice(list(loc for loc in location_table if loc["region"] == region)) self.multiworld.get_location(location["name"], self.player).place_locked_item(itemToUse) def fill_slot_data(self): From f444d570d3bee733972ad44e3b9fb1467f159b11 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 10 Nov 2023 14:07:56 -0500 Subject: [PATCH 011/104] Lingo: Fix edge case painting shuffle accessibility issues (#2441) * Lingo: Fix painting shuffle logic issue in The Wise * Lingo: More generic painting cycle prevention * Lingo: okay how about now * Lingo: Consider Owl Hallway blocked painting areas in vanilla doors * Lingo: so honestly I should've seen this one coming * Lingo: Refined req_blocked for vanilla doors * Lingo: Orange Tower Basement is also owl-blocked * Lingo: Rewrite randomize_paintings to eliminate rerolls Now, mapping is done in two phases, rather than assigning everything at once and then rerolling if the mapping is non-viable. --- worlds/lingo/LL1.yaml | 11 +++++++ worlds/lingo/player_logic.py | 62 +++++++++++++++++++----------------- worlds/lingo/static_logic.py | 15 ++++++++- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/worlds/lingo/LL1.yaml b/worlds/lingo/LL1.yaml index 7ae015dc6432..db1418f5963d 100644 --- a/worlds/lingo/LL1.yaml +++ b/worlds/lingo/LL1.yaml @@ -97,6 +97,11 @@ # Use "required_when_no_doors" instead if it would be # possible to enter the room without the painting in door # shuffle mode. + # - req_blocked: Marks that a painting cannot be an entrance leading to a + # required painting. Paintings within a room that has a + # required painting are automatically req blocked. + # Use "req_blocked_when_no_doors" instead if it would be + # fine in door shuffle mode. # - move: Denotes that the painting is able to move. Starting Room: entrances: @@ -2210,6 +2215,7 @@ - id: map_painting2 orientation: north enter_only: True # otherwise you might just skip the whole game! + req_blocked_when_no_doors: True # owl hallway in vanilla doors Roof: entrances: Orange Tower Seventh Floor: True @@ -2276,6 +2282,7 @@ paintings: - id: arrows_painting_11 orientation: east + req_blocked_when_no_doors: True # owl hallway in vanilla doors Courtyard: entrances: Roof: True @@ -5755,11 +5762,13 @@ move: True required_door: door: Exit + req_blocked_when_no_doors: True # the wondrous (table) in vanilla doors - id: symmetry_painting_a_6 orientation: west exit_only: True - id: symmetry_painting_b_6 orientation: north + req_blocked_when_no_doors: True # the wondrous (table) in vanilla doors Arrow Garden: entrances: The Wondrous: @@ -6914,6 +6923,7 @@ paintings: - id: clock_painting_3 orientation: east + req_blocked: True # outside the wise (with or without door shuffle) The Red: entrances: Roof: True @@ -7362,6 +7372,7 @@ paintings: - id: hi_solved_painting4 orientation: south + req_blocked_when_no_doors: True # owl hallway in vanilla doors Challenge Room: entrances: Welcome Back Area: diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 217ad91fcd23..66fe317d1420 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -241,43 +241,46 @@ def randomize_paintings(self, world: "LingoWorld") -> bool: door_shuffle = world.options.shuffle_doors - # Determine the set of exit paintings. All required-exit paintings are included, as are all - # required-when-no-doors paintings if door shuffle is off. We then fill the set with random other paintings. - chosen_exits = [] + # First, assign mappings to the required-exit paintings. We ensure that req-blocked paintings do not lead to + # required paintings. + req_exits = [] + required_painting_rooms = REQUIRED_PAINTING_ROOMS if door_shuffle == ShuffleDoors.option_none: - chosen_exits = [painting_id for painting_id, painting in PAINTINGS.items() - if painting.required_when_no_doors] - chosen_exits += [painting_id for painting_id, painting in PAINTINGS.items() - if painting.exit_only and painting.required] + required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS + req_exits = [painting_id for painting_id, painting in PAINTINGS.items() if painting.required_when_no_doors] + req_enterable = [painting_id for painting_id, painting in PAINTINGS.items() + if not painting.exit_only and not painting.disable and not painting.req_blocked and + not painting.req_blocked_when_no_doors and painting.room not in required_painting_rooms] + else: + req_enterable = [painting_id for painting_id, painting in PAINTINGS.items() + if not painting.exit_only and not painting.disable and not painting.req_blocked and + painting.room not in required_painting_rooms] + req_exits += [painting_id for painting_id, painting in PAINTINGS.items() + if painting.exit_only and painting.required] + req_entrances = world.random.sample(req_enterable, len(req_exits)) + + self.PAINTING_MAPPING = dict(zip(req_entrances, req_exits)) + + # Next, determine the rest of the exit paintings. exitable = [painting_id for painting_id, painting in PAINTINGS.items() - if not painting.enter_only and not painting.disable and not painting.required] - chosen_exits += world.random.sample(exitable, PAINTING_EXITS - len(chosen_exits)) + if not painting.enter_only and not painting.disable and painting_id not in req_exits and + painting_id not in req_entrances] + nonreq_exits = world.random.sample(exitable, PAINTING_EXITS - len(req_exits)) + chosen_exits = req_exits + nonreq_exits - # Determine the set of entrance paintings. + # Determine the rest of the entrance paintings. enterable = [painting_id for painting_id, painting in PAINTINGS.items() - if not painting.exit_only and not painting.disable and painting_id not in chosen_exits] - chosen_entrances = world.random.sample(enterable, PAINTING_ENTRANCES) + if not painting.exit_only and not painting.disable and painting_id not in chosen_exits and + painting_id not in req_entrances] + chosen_entrances = world.random.sample(enterable, PAINTING_ENTRANCES - len(req_entrances)) - # Create a mapping from entrances to exits. - for warp_exit in chosen_exits: + # Assign one entrance to each non-required exit, to ensure that the total number of exits is achieved. + for warp_exit in nonreq_exits: warp_enter = world.random.choice(chosen_entrances) - - # Check whether this is a warp from a required painting room to another (or the same) required painting - # room. This could cause a cycle that would make certain regions inaccessible. - warp_exit_room = PAINTINGS[warp_exit].room - warp_enter_room = PAINTINGS[warp_enter].room - - required_painting_rooms = REQUIRED_PAINTING_ROOMS - if door_shuffle == ShuffleDoors.option_none: - required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS - - if warp_exit_room in required_painting_rooms and warp_enter_room in required_painting_rooms: - # This shuffling is non-workable. Start over. - return False - chosen_entrances.remove(warp_enter) self.PAINTING_MAPPING[warp_enter] = warp_exit + # Assign each of the remaining entrances to any required or non-required exit. for warp_enter in chosen_entrances: warp_exit = world.random.choice(chosen_exits) self.PAINTING_MAPPING[warp_enter] = warp_exit @@ -292,7 +295,8 @@ def randomize_paintings(self, world: "LingoWorld") -> bool: # Just for sanity's sake, ensure that all required painting rooms are accessed. for painting_id, painting in PAINTINGS.items(): if painting_id not in self.PAINTING_MAPPING.values() \ - and (painting.required or (painting.required_when_no_doors and door_shuffle == 0)): + and (painting.required or (painting.required_when_no_doors and + door_shuffle == ShuffleDoors.option_none)): return False return True diff --git a/worlds/lingo/static_logic.py b/worlds/lingo/static_logic.py index d122169c5d03..f6690f93a439 100644 --- a/worlds/lingo/static_logic.py +++ b/worlds/lingo/static_logic.py @@ -63,6 +63,8 @@ class Painting(NamedTuple): required_door: Optional[RoomAndDoor] disable: bool move: bool + req_blocked: bool + req_blocked_when_no_doors: bool class Progression(NamedTuple): @@ -471,6 +473,16 @@ def process_painting(room_name, painting_data): else: enter_only = False + if "req_blocked" in painting_data: + req_blocked = painting_data["req_blocked"] + else: + req_blocked = False + + if "req_blocked_when_no_doors" in painting_data: + req_blocked_when_no_doors = painting_data["req_blocked_when_no_doors"] + else: + req_blocked_when_no_doors = False + required_door = None if "required_door" in painting_data: door = painting_data["required_door"] @@ -480,7 +492,8 @@ def process_painting(room_name, painting_data): ) painting_obj = Painting(painting_id, room_name, enter_only, exit_only, orientation, - required_painting, rwnd, required_door, disable_painting, move_painting) + required_painting, rwnd, required_door, disable_painting, move_painting, req_blocked, + req_blocked_when_no_doors) PAINTINGS[painting_id] = painting_obj PAINTINGS_BY_ROOM[room_name].append(painting_obj) From 7af7ef2dc7ff9563aef5f2d01ab7851dc5bf3052 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 10 Nov 2023 14:19:05 -0500 Subject: [PATCH 012/104] Lingo: Removed "Reached" event items (#2442) --- worlds/lingo/player_logic.py | 6 ------ worlds/lingo/rules.py | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 66fe317d1420..abb975e020ae 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -79,12 +79,6 @@ def __init__(self, world: "LingoWorld"): raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not " "be enough locations for all of the door items.") - # Create an event for every room that represents being able to reach that room. - for room_name in ROOMS.keys(): - roomloc_name = f"{room_name} (Reached)" - self.add_location(room_name, PlayerLocation(roomloc_name, None, [])) - self.EVENT_LOC_TO_ITEM[roomloc_name] = roomloc_name - # Create an event for every door, representing whether that door has been opened. Also create event items for # doors that are event-only. for room_name, room_data in DOORS_BY_ROOM.items(): diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index 90c889b7f098..d59b8a1ef78a 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -66,7 +66,7 @@ def _lingo_can_solve_panel(state: CollectionState, start_room: str, room: str, p """ Determines whether a panel can be solved """ - if start_room != room and not state.has(f"{room} (Reached)", world.player): + if start_room != room and not state.can_reach(room, "Region", world.player): return False if room == "Second Room" and panel == "ANOTHER TRY" \ @@ -76,7 +76,7 @@ def _lingo_can_solve_panel(state: CollectionState, start_room: str, room: str, p panel_object = PANELS_BY_ROOM[room][panel] for req_room in panel_object.required_rooms: - if not state.has(f"{req_room} (Reached)", world.player): + if not state.can_reach(req_room, "Region", world.player): return False for req_door in panel_object.required_doors: From ac77666f2f3d031218347a7085f8d911e3a4adb5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 10 Nov 2023 22:02:34 +0100 Subject: [PATCH 013/104] Factorio: skip a bunch of file IO (#2444) In a lot of cases, Factorio would write data to file first, then attach that file into zip. It now directly attaches the data to the zip and encapsulation was used to allow earlier GC in places (rendered templates especially). --- worlds/factorio/Mod.py | 67 +++++++++++++++--------------- worlds/factorio/data/mod/info.json | 14 ------- 2 files changed, 34 insertions(+), 47 deletions(-) delete mode 100644 worlds/factorio/data/mod/info.json diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 270e7dacf087..c897e72dcd11 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -5,7 +5,7 @@ import shutil import threading import zipfile -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple import jinja2 @@ -24,6 +24,7 @@ data_final_template: Optional[jinja2.Template] = None locale_template: Optional[jinja2.Template] = None control_template: Optional[jinja2.Template] = None +settings_template: Optional[jinja2.Template] = None template_load_lock = threading.Lock() @@ -62,15 +63,24 @@ class FactorioModFile(worlds.Files.APContainer): game = "Factorio" compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives + writing_tasks: List[Callable[[], Tuple[str, str]]] + + def __init__(self, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + self.writing_tasks = [] def write_contents(self, opened_zipfile: zipfile.ZipFile): # directory containing Factorio mod has to come first, or Factorio won't recognize this file as a mod. mod_dir = self.path[:-4] # cut off .zip for root, dirs, files in os.walk(mod_dir): for file in files: - opened_zipfile.write(os.path.join(root, file), - os.path.relpath(os.path.join(root, file), + filename = os.path.join(root, file) + opened_zipfile.write(filename, + os.path.relpath(filename, os.path.join(mod_dir, '..'))) + for task in self.writing_tasks: + target, content = task() + opened_zipfile.writestr(target, content) # now we can add extras. super(FactorioModFile, self).write_contents(opened_zipfile) @@ -98,6 +108,7 @@ def load_template(name: str): locations = [(location, location.item) for location in world.science_locations] mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" + versioned_mod_name = mod_name + "_" + Utils.__version__ random = multiworld.per_slot_randoms[player] @@ -153,48 +164,38 @@ def flop_random(low, high, base=None): template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value}) template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value}) - control_code = control_template.render(**template_data) - data_template_code = data_template.render(**template_data) - data_final_fixes_code = data_final_template.render(**template_data) - settings_code = settings_template.render(**template_data) + mod_dir = os.path.join(output_directory, versioned_mod_name) - mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__) - en_locale_dir = os.path.join(mod_dir, "locale", "en") - os.makedirs(en_locale_dir, exist_ok=True) + zf_path = os.path.join(mod_dir + ".zip") + mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player]) if world.zip_path: - # Maybe investigate read from zip, write to zip, without temp file? with zipfile.ZipFile(world.zip_path) as zf: for file in zf.infolist(): if not file.is_dir() and "/data/mod/" in file.filename: path_part = Utils.get_text_after(file.filename, "/data/mod/") - target = os.path.join(mod_dir, path_part) - os.makedirs(os.path.split(target)[0], exist_ok=True) - - with open(target, "wb") as f: - f.write(zf.read(file)) + mod.writing_tasks.append(lambda arcpath=versioned_mod_name+"/"+path_part, content=zf.read(file): + (arcpath, content)) else: shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True) - with open(os.path.join(mod_dir, "data.lua"), "wt") as f: - f.write(data_template_code) - with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f: - f.write(data_final_fixes_code) - with open(os.path.join(mod_dir, "control.lua"), "wt") as f: - f.write(control_code) - with open(os.path.join(mod_dir, "settings.lua"), "wt") as f: - f.write(settings_code) - locale_content = locale_template.render(**template_data) - with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f: - f.write(locale_content) + mod.writing_tasks.append(lambda: (versioned_mod_name + "/data.lua", + data_template.render(**template_data))) + mod.writing_tasks.append(lambda: (versioned_mod_name + "/data-final-fixes.lua", + data_final_template.render(**template_data))) + mod.writing_tasks.append(lambda: (versioned_mod_name + "/control.lua", + control_template.render(**template_data))) + mod.writing_tasks.append(lambda: (versioned_mod_name + "/settings.lua", + settings_template.render(**template_data))) + mod.writing_tasks.append(lambda: (versioned_mod_name + "/locale/en/locale.cfg", + locale_template.render(**template_data))) + info = base_info.copy() info["name"] = mod_name - with open(os.path.join(mod_dir, "info.json"), "wt") as f: - json.dump(info, f, indent=4) + mod.writing_tasks.append(lambda: (versioned_mod_name + "/info.json", + json.dumps(info, indent=4))) - # zip the result - zf_path = os.path.join(mod_dir + ".zip") - mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player]) + # write the mod file mod.write() - + # clean up shutil.rmtree(mod_dir) diff --git a/worlds/factorio/data/mod/info.json b/worlds/factorio/data/mod/info.json deleted file mode 100644 index 70a951834428..000000000000 --- a/worlds/factorio/data/mod/info.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "archipelago-client", - "version": "0.0.1", - "title": "Archipelago", - "author": "Berserker and Dewiniaid", - "homepage": "https://archipelago.gg", - "description": "Integration client for the Archipelago Randomizer", - "factorio_version": "1.1", - "dependencies": [ - "base >= 1.1.0", - "? science-not-invited", - "? factory-levels" - ] -} From 64159a6d0fab4b4c866628f9a0ec9b8a9752179a Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Fri, 10 Nov 2023 22:49:55 -0600 Subject: [PATCH 014/104] The Messenger: fix logic rule for spike darts and power seal hunt (#2414) --- worlds/messenger/__init__.py | 7 ++++--- worlds/messenger/regions.py | 6 ------ worlds/messenger/rules.py | 21 ++++++++------------- worlds/messenger/subclasses.py | 6 ++---- worlds/messenger/test/test_shop_chest.py | 10 +++++----- 5 files changed, 19 insertions(+), 31 deletions(-) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 3fe13a3cb421..304b43cf5316 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -82,7 +82,10 @@ def generate_early(self) -> None: self.shop_prices, self.figurine_prices = shuffle_shop_prices(self) def create_regions(self) -> None: - self.multiworld.regions += [MessengerRegion(reg_name, self) for reg_name in REGIONS] + # MessengerRegion adds itself to the multiworld + for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]: + if region.name in REGION_CONNECTIONS: + region.add_exits(REGION_CONNECTIONS[region.name]) def create_items(self) -> None: # create items that are always in the item pool @@ -136,8 +139,6 @@ def create_items(self) -> None: self.multiworld.itempool += itempool def set_rules(self) -> None: - for reg_name, connections in REGION_CONNECTIONS.items(): - self.multiworld.get_region(reg_name, self.player).add_exits(connections) logic = self.options.logic_level if logic == Logic.option_normal: MessengerRules(self).set_messenger_rules() diff --git a/worlds/messenger/regions.py b/worlds/messenger/regions.py index 28750b949ede..3a6c95bff5a2 100644 --- a/worlds/messenger/regions.py +++ b/worlds/messenger/regions.py @@ -68,7 +68,6 @@ "Quillshroom Marsh": ["Quillshroom Marsh Mega Shard"], "Searing Crags Upper": ["Searing Crags Mega Shard"], "Glacial Peak": ["Glacial Peak Mega Shard"], - "Tower of Time": [], "Cloud Ruins": ["Cloud Entrance Mega Shard", "Time Warp Mega Shard"], "Cloud Ruins Right": ["Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2"], "Underworld": ["Under Entrance Mega Shard", "Hot Tub Mega Shard", "Projectile Pit Mega Shard"], @@ -84,8 +83,6 @@ "Menu": {"Tower HQ"}, "Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time", "Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"}, - "Tower of Time": set(), - "Ninja Village": set(), "Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"}, "Forlorn Temple": {"Catacombs", "Bamboo Creek"}, "Catacombs": {"Autumn Hills", "Bamboo Creek", "Dark Cave"}, @@ -97,11 +94,8 @@ "Glacial Peak": {"Searing Crags Upper", "Tower HQ", "Cloud Ruins", "Elemental Skylands"}, "Cloud Ruins": {"Cloud Ruins Right"}, "Cloud Ruins Right": {"Underworld"}, - "Underworld": set(), "Dark Cave": {"Catacombs", "Riviere Turquoise Entrance"}, "Riviere Turquoise Entrance": {"Riviere Turquoise"}, - "Riviere Turquoise": set(), "Sunken Shrine": {"Howling Grotto"}, - "Elemental Skylands": set(), } """Vanilla layout mapping with all Tower HQ portals open. from -> to""" diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index c9bd9b86253d..876acd42c108 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -1,10 +1,9 @@ from typing import Callable, Dict, TYPE_CHECKING from BaseClasses import CollectionState -from worlds.generic.Rules import add_rule, allow_self_locking_items, set_rule +from worlds.generic.Rules import add_rule, allow_self_locking_items from .constants import NOTES, PHOBEKINS -from .options import Goal, MessengerAccessibility -from .subclasses import MessengerShopLocation +from .options import MessengerAccessibility if TYPE_CHECKING: from . import MessengerWorld @@ -37,7 +36,9 @@ def __init__(self, world: MessengerWorld) -> None: "Forlorn Temple": lambda state: state.has_all({"Wingsuit", *PHOBEKINS}, self.player) and self.can_dboost(state), "Glacial Peak": self.has_vertical, "Elemental Skylands": lambda state: state.has("Magic Firefly", self.player) and self.has_wingsuit(state), - "Music Box": lambda state: state.has_all(set(NOTES), self.player) and self.has_dart(state), + "Music Box": lambda state: (state.has_all(set(NOTES), self.player) + or state.has("Power Seal", self.player, max(1, self.world.required_seals))) + and self.has_dart(state), } self.location_rules = { @@ -92,8 +93,6 @@ def __init__(self, world: MessengerWorld) -> None: # corrupted future "Corrupted Future - Key of Courage": lambda state: state.has_all({"Demon King Crown", "Magic Firefly"}, self.player), - # the shop - "Shop Chest": self.has_enough_seals, # tower hq "Money Wrench": self.can_shop, } @@ -143,14 +142,11 @@ def set_messenger_rules(self) -> None: if loc.name in self.location_rules: loc.access_rule = self.location_rules[loc.name] if region.name == "The Shop": - for loc in [location for location in region.locations if isinstance(location, MessengerShopLocation)]: + for loc in region.locations: loc.access_rule = loc.can_afford - if self.world.options.goal == Goal.option_power_seal_hunt: - set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player), - lambda state: state.has("Shop Chest", self.player)) multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player) - if multiworld.accessibility[self.player] > MessengerAccessibility.option_locations: + if multiworld.accessibility[self.player]: # not locations accessibility set_self_locking_items(self.world, self.player) @@ -201,8 +197,7 @@ def __init__(self, world: MessengerWorld) -> None: self.extra_rules = { "Searing Crags - Key of Strength": lambda state: self.has_dart(state) or self.has_windmill(state), "Elemental Skylands - Key of Symbiosis": lambda state: self.has_windmill(state) or self.can_dboost(state), - "Autumn Hills Seal - Spike Ball Darts": lambda state: (self.has_dart(state) and self.has_windmill(state)) - or self.has_wingsuit(state), + "Autumn Hills Seal - Spike Ball Darts": lambda state: self.has_dart(state) or self.has_windmill(state), "Underworld Seal - Fireball Wave": self.has_windmill, } diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index ce31d43d60b0..0c04bc015c35 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -17,8 +17,6 @@ def __init__(self, name: str, world: "MessengerWorld") -> None: super().__init__(name, world.player, world.multiworld) locations = [loc for loc in REGIONS[self.name]] if self.name == "The Shop": - if world.options.goal > Goal.option_open_music_box: - locations.append("Shop Chest") shop_locations = {f"The Shop - {shop_loc}": world.location_name_to_id[f"The Shop - {shop_loc}"] for shop_loc in SHOP_ITEMS} shop_locations.update(**{figurine: world.location_name_to_id[figurine] for figurine in FIGURINES}) @@ -29,9 +27,9 @@ def __init__(self, name: str, world: "MessengerWorld") -> None: locations += [seal_loc for seal_loc in SEALS[self.name]] if world.options.shuffle_shards and self.name in MEGA_SHARDS: locations += [shard for shard in MEGA_SHARDS[self.name]] - loc_dict = {loc: world.location_name_to_id[loc] if loc in world.location_name_to_id else None - for loc in locations} + loc_dict = {loc: world.location_name_to_id.get(loc, None) for loc in locations} self.add_locations(loc_dict, MessengerLocation) + world.multiworld.regions.append(self) class MessengerLocation(Location): diff --git a/worlds/messenger/test/test_shop_chest.py b/worlds/messenger/test/test_shop_chest.py index 058a2004478e..a34fa0fb96c0 100644 --- a/worlds/messenger/test/test_shop_chest.py +++ b/worlds/messenger/test/test_shop_chest.py @@ -17,18 +17,18 @@ def test_chest_access(self) -> None: with self.subTest("Access Dependency"): self.assertEqual(len([seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]), self.multiworld.total_seals[self.player]) - locations = ["Shop Chest"] + locations = ["Rescue Phantom"] items = [["Power Seal"]] self.assertAccessDependency(locations, items) self.multiworld.state = CollectionState(self.multiworld) - self.assertEqual(self.can_reach_location("Shop Chest"), False) + self.assertEqual(self.can_reach_location("Rescue Phantom"), False) self.assertBeatable(False) - self.collect_all_but(["Power Seal", "Shop Chest", "Rescue Phantom"]) - self.assertEqual(self.can_reach_location("Shop Chest"), False) + self.collect_all_but(["Power Seal", "Rescue Phantom"]) + self.assertEqual(self.can_reach_location("Rescue Phantom"), False) self.assertBeatable(False) self.collect_by_name("Power Seal") - self.assertEqual(self.can_reach_location("Shop Chest"), True) + self.assertEqual(self.can_reach_location("Rescue Phantom"), True) self.assertBeatable(True) From 2dd904e7586b6ac974c86f1cf778d3b257e9c91a Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 10 Nov 2023 22:06:54 -0800 Subject: [PATCH 015/104] Allow worlds to provide item and location descriptions (#2409) These are displayed in the weighted options page as hoverable tooltips. --- WebHostLib/options.py | 24 ++++---- WebHostLib/static/assets/weighted-options.js | 40 +++++++++++-- docs/world api.md | 63 ++++++++++++++++++++ test/bases.py | 21 +++++++ worlds/AutoWorld.py | 35 +++++++++++ worlds/dark_souls_3/Items.py | 8 +++ worlds/dark_souls_3/__init__.py | 3 +- 7 files changed, 177 insertions(+), 17 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 1a2aab6d883d..3c0f47f32714 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -136,16 +136,20 @@ def get_html_doc(option_type: type(Options.Option)) -> str: option["defaultValue"] = "random" weighted_options["baseOptions"]["game"][game_name] = 0 - weighted_options["games"][game_name] = {} - weighted_options["games"][game_name]["gameSettings"] = game_options - weighted_options["games"][game_name]["gameItems"] = tuple(world.item_names) - weighted_options["games"][game_name]["gameItemGroups"] = [ - group for group in world.item_name_groups.keys() if group != "Everything" - ] - weighted_options["games"][game_name]["gameLocations"] = tuple(world.location_names) - weighted_options["games"][game_name]["gameLocationGroups"] = [ - group for group in world.location_name_groups.keys() if group != "Everywhere" - ] + weighted_options["games"][game_name] = { + "gameSettings": game_options, + "gameItems": tuple(world.item_names), + "gameItemGroups": [ + group for group in world.item_name_groups.keys() if group != "Everything" + ], + "gameItemDescriptions": world.item_descriptions, + "gameLocations": tuple(world.location_names), + "gameLocationGroups": [ + group for group in world.location_name_groups.keys() if group != "Everywhere" + ], + "gameLocationDescriptions": world.location_descriptions, + } with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f: json.dump(weighted_options, f, indent=2, separators=(',', ': ')) + diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js index 3811bd42bac9..34dfbae4bbee 100644 --- a/WebHostLib/static/assets/weighted-options.js +++ b/WebHostLib/static/assets/weighted-options.js @@ -1024,12 +1024,18 @@ class GameSettings { // Builds a div for a setting whose value is a list of locations. #buildLocationsDiv(setting) { - return this.#buildListDiv(setting, this.data.gameLocations, this.data.gameLocationGroups); + return this.#buildListDiv(setting, this.data.gameLocations, { + groups: this.data.gameLocationGroups, + descriptions: this.data.gameLocationDescriptions, + }); } // Builds a div for a setting whose value is a list of items. #buildItemsDiv(setting) { - return this.#buildListDiv(setting, this.data.gameItems, this.data.gameItemGroups); + return this.#buildListDiv(setting, this.data.gameItems, { + groups: this.data.gameItemGroups, + descriptions: this.data.gameItemDescriptions + }); } // Builds a div for a setting named `setting` with a list value that can @@ -1038,12 +1044,15 @@ class GameSettings { // The `groups` option can be a list of additional options for this list // (usually `item_name_groups` or `location_name_groups`) that are displayed // in a special section at the top of the list. - #buildListDiv(setting, items, groups = []) { + // + // The `descriptions` option can be a map from item names or group names to + // descriptions for the user's benefit. + #buildListDiv(setting, items, {groups = [], descriptions = {}} = {}) { const div = document.createElement('div'); div.classList.add('simple-list'); groups.forEach((group) => { - const row = this.#addListRow(setting, group); + const row = this.#addListRow(setting, group, descriptions[group]); div.appendChild(row); }); @@ -1052,7 +1061,7 @@ class GameSettings { } items.forEach((item) => { - const row = this.#addListRow(setting, item); + const row = this.#addListRow(setting, item, descriptions[item]); div.appendChild(row); }); @@ -1060,7 +1069,9 @@ class GameSettings { } // Builds and returns a row for a list of checkboxes. - #addListRow(setting, item) { + // + // If `help` is passed, it's displayed as a help tooltip for this list item. + #addListRow(setting, item, help = undefined) { const row = document.createElement('div'); row.classList.add('list-row'); @@ -1081,6 +1092,23 @@ class GameSettings { const name = document.createElement('span'); name.innerText = item; + + if (help) { + const helpSpan = document.createElement('span'); + helpSpan.classList.add('interactive'); + helpSpan.setAttribute('data-tooltip', help); + helpSpan.innerText = '(?)'; + name.innerText += ' '; + name.appendChild(helpSpan); + + // Put the first 7 tooltips below their rows. CSS tooltips in scrolling + // containers can't be visible outside those containers, so this helps + // ensure they won't be pushed out the top. + if (helpSpan.parentNode.childNodes.length < 7) { + helpSpan.classList.add('tooltip-bottom'); + } + } + label.appendChild(name); row.appendChild(label); diff --git a/docs/world api.md b/docs/world api.md index b128e2b146b4..9b7573dccd9d 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -121,6 +121,38 @@ Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being required, and will prevent progression and useful items from being placed at excluded locations. +#### Documenting Locations + +Worlds can optionally provide a `location_descriptions` map which contains +human-friendly descriptions of locations or location groups. These descriptions +will show up in location-selection options in the Weighted Options page. Extra +indentation and single newlines will be collapsed into spaces. + +```python +# Locations.py + +location_descriptions = { + "Red Potion #6": "In a secret destructible block under the second stairway", + "L2 Spaceship": """ + The group of all items in the spaceship in Level 2. + + This doesn't include the item on the spaceship door, since it can be + accessed without the Spaeship Key. + """ +} +``` + +```python +# __init__.py + +from worlds.AutoWorld import World +from .Locations import location_descriptions + + +class MyGameWorld(World): + location_descriptions = location_descriptions +``` + ### Items Items are all things that can "drop" for your game. This may be RPG items like @@ -147,6 +179,37 @@ Other classifications include * `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that will not be moved around by progression balancing; used, e.g., for currency or tokens +#### Documenting Items + +Worlds can optionally provide an `item_descriptions` map which contains +human-friendly descriptions of items or item groups. These descriptions will +show up in item-selection options in the Weighted Options page. Extra +indentation and single newlines will be collapsed into spaces. + +```python +# Items.py + +item_descriptions = { + "Red Potion": "A standard health potion", + "Spaceship Key": """ + The key to the spaceship in Level 2. + + This is necessary to get to the Star Realm. + """ +} +``` + +```python +# __init__.py + +from worlds.AutoWorld import World +from .Items import item_descriptions + + +class MyGameWorld(World): + item_descriptions = item_descriptions +``` + ### Events Events will mark some progress. You define an event location, an diff --git a/test/bases.py b/test/bases.py index 2054c2d18725..3d704579a7f3 100644 --- a/test/bases.py +++ b/test/bases.py @@ -333,3 +333,24 @@ def fulfills_accessibility() -> bool: placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code] self.assertLessEqual(len(self.multiworld.itempool), len(placed_items), "Unplaced Items remaining in itempool") + + def test_descriptions_have_valid_names(self): + """Ensure all item and location descriptions match a name of the corresponding type""" + if not (self.run_default_tests and self.constructed): + return + with self.subTest("Game", game=self.game): + with self.subTest("Items"): + world = self.multiworld.worlds[1] + valid_names = world.item_names.union(world.item_name_groups) + for name in world.item_descriptions.keys(): + with self.subTest("Name should be valid", name=name): + self.assertIn(name, valid_names, + """All item descriptions must match defined item names""") + + with self.subTest("Locations"): + world = self.multiworld.worlds[1] + valid_names = world.location_names.union(world.location_name_groups) + for name in world.location_descriptions.keys(): + with self.subTest("Name should be valid", name=name): + self.assertIn(name, valid_names, + """All item descriptions must match defined item names""") diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index d05797cf9e12..5b4dec83179c 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -3,6 +3,7 @@ import hashlib import logging import pathlib +import re import sys import time from dataclasses import make_dataclass @@ -51,11 +52,17 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set in dct.get("item_name_groups", {}).items()} dct["item_name_groups"]["Everything"] = dct["item_names"] + dct["item_descriptions"] = {name: _normalize_description(description) for name, description + in dct.get("item_descriptions", {}).items()} + dct["item_descriptions"]["Everything"] = "All items in the entire game." dct["location_names"] = frozenset(dct["location_name_to_id"]) dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set in dct.get("location_name_groups", {}).items()} dct["location_name_groups"]["Everywhere"] = dct["location_names"] dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {}))) + dct["location_descriptions"] = {name: _normalize_description(description) for name, description + in dct.get("location_descriptions", {}).items()} + dct["location_descriptions"]["Everywhere"] = "All locations in the entire game." # move away from get_required_client_version function if "game" in dct: @@ -205,9 +212,23 @@ class World(metaclass=AutoWorldRegister): item_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}""" + item_descriptions: ClassVar[Dict[str, str]] = {} + """An optional map from item names (or item group names) to brief descriptions for users. + + Individual newlines and indentation will be collapsed into spaces before these descriptions are + displayed. This may cover only a subset of items. + """ + location_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}""" + location_descriptions: ClassVar[Dict[str, str]] = {} + """An optional map from location names (or location group names) to brief descriptions for users. + + Individual newlines and indentation will be collapsed into spaces before these descriptions are + displayed. This may cover only a subset of locations. + """ + data_version: ClassVar[int] = 0 """ Increment this every time something in your world's names/id mappings changes. @@ -462,3 +483,17 @@ def data_package_checksum(data: "GamesPackage") -> str: assert sorted(data) == list(data), "Data not ordered" from NetUtils import encode return hashlib.sha1(encode(data).encode()).hexdigest() + + +def _normalize_description(description): + """Normalizes a description in item_descriptions or location_descriptions. + + This allows authors to write descritions with nice indentation and line lengths in their world + definitions without having it affect the rendered format. + """ + # First, collapse the whitespace around newlines and the ends of the description. + description = re.sub(r' *\n *', '\n', description.strip()) + # Next, condense individual newlines into spaces. + description = re.sub(r'(? dict: ("Dorris Swarm", 0x40393870, DS3ItemCategory.SKIP), ]] +item_descriptions = { + "Cinders": """ + All four Cinders of a Lord. + + Once you have these four, you can fight Soul of Cinder and win the game. + """, +} + _all_items = _vanilla_items + _dlc_items item_dictionary = {item_data.name: item_data for item_data in _all_items} diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 195d319887d5..b9879f70f302 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -7,7 +7,7 @@ from worlds.AutoWorld import World, WebWorld from worlds.generic.Rules import set_rule, add_rule, add_item_rule -from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names +from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names, item_descriptions from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary from .Options import RandomizeWeaponLevelOption, PoolTypeOption, dark_souls_options @@ -60,6 +60,7 @@ class DarkSouls3World(World): "Cinders of a Lord - Lothric Prince" } } + item_descriptions = item_descriptions def __init__(self, multiworld: MultiWorld, player: int): From df1e78c6f24dad70bc5cad761715650efd5488df Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Fri, 10 Nov 2023 22:13:32 -0800 Subject: [PATCH 016/104] WebHost: Sort tracker last activity 'None' as maximum instead of -1 (#2446) When managing an async, it can be useful to sort the tracker by Last Activity to see who has potentially abandoned their slots. Today, if a slot hasn't been started (last activity is None) then it is sorted as if last activity is -1, that it is it has had more recent activity than any other slot. This change makes it so slots that haven't started are treated as if they have last activity MAX_VALUE time ago. This way they get sorted with slots that haven't been touched in a long time which should make intuitive sense as the "last activity" is effectively inf time ago. --- WebHostLib/static/assets/trackerCommon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/trackerCommon.js b/WebHostLib/static/assets/trackerCommon.js index 41c4020dace8..cb16a4de782d 100644 --- a/WebHostLib/static/assets/trackerCommon.js +++ b/WebHostLib/static/assets/trackerCommon.js @@ -55,7 +55,7 @@ window.addEventListener('load', () => { render: function (data, type, row) { if (type === "sort" || type === 'type') { if (data === "None") - return -1; + return Number.MAX_VALUE; return parseInt(data); } From e670ca513bdf808b6d7ba99ceb3c44e6a0a45159 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 11 Nov 2023 10:54:51 +0100 Subject: [PATCH 017/104] Fill: fix swap error found in CI (#2397) * Fill: add test for swap error with item rules https://discord.com/channels/731205301247803413/731214280439103580/1167195750082560121 * Fill: fix swap error found in CI Swap now assumes the unplaced items can be placed before the to-be-swapped item. Unsure if that is safe or unsafe. * Test: clarify docstring and comments in fill swap test * Test: clarify comments in fill swap test more --- Fill.py | 2 +- test/general/test_fill.py | 41 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index c9660ab708ca..9fdbcc384392 100644 --- a/Fill.py +++ b/Fill.py @@ -112,7 +112,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: location.item = None placed_item.location = None - swap_state = sweep_from_pool(base_state, [placed_item] if unsafe else []) + swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool) # unsafe means swap_state assumes we can somehow collect placed_item before item_to_place # by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic # to clean that up later, so there is a chance generation fails. diff --git a/test/general/test_fill.py b/test/general/test_fill.py index 1e469ef04d0d..e454b3e61d7a 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -442,6 +442,47 @@ def test_swap_to_earlier_location_with_item_rule(self): self.assertTrue(sphere1_loc.item, "Did not swap required item into Sphere 1") self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1") + def test_swap_to_earlier_location_with_item_rule2(self): + """Test that swap works before all items are placed""" + multi_world = generate_multi_world(1) + player1 = generate_player_data(multi_world, 1, 5, 5) + locations = player1.locations[:] # copy required + items = player1.prog_items[:] # copy required + # Two items provide access to sphere 2. + # One of them is forbidden in sphere 1, the other is first placed in sphere 4 because of placement order, + # requiring a swap. + # There are spheres in between, so for the swap to work, it'll have to assume all other items are collected. + one_to_two1 = items[4].name + one_to_two2 = items[3].name + three_to_four = items[2].name + two_to_three1 = items[1].name + two_to_three2 = items[0].name + # Sphere 4 + set_rule(locations[0], lambda state: ((state.has(one_to_two1, player1.id) or state.has(one_to_two2, player1.id)) + and state.has(two_to_three1, player1.id) + and state.has(two_to_three2, player1.id) + and state.has(three_to_four, player1.id))) + # Sphere 3 + set_rule(locations[1], lambda state: ((state.has(one_to_two1, player1.id) or state.has(one_to_two2, player1.id)) + and state.has(two_to_three1, player1.id) + and state.has(two_to_three2, player1.id))) + # Sphere 2 + set_rule(locations[2], lambda state: state.has(one_to_two1, player1.id) or state.has(one_to_two2, player1.id)) + # Sphere 1 + sphere1_loc1 = locations[3] + sphere1_loc2 = locations[4] + # forbid one_to_two2 in sphere 1 to make the swap happen as described above + add_item_rule(sphere1_loc1, lambda item_to_place: item_to_place.name != one_to_two2) + add_item_rule(sphere1_loc2, lambda item_to_place: item_to_place.name != one_to_two2) + + # Now fill should place one_to_two1 in sphere1_loc1 or sphere1_loc2 via swap, + # which it will attempt before two_to_three and three_to_four are placed, testing the behavior. + fill_restrictive(multi_world, multi_world.state, player1.locations, player1.prog_items) + # assert swap happened + self.assertTrue(sphere1_loc1.item and sphere1_loc2.item, "Did not swap required item into Sphere 1") + self.assertTrue(sphere1_loc1.item.name == one_to_two1 or + sphere1_loc2.item.name == one_to_two1, "Wrong item in Sphere 1") + def test_double_sweep(self): """Test that sweep doesn't duplicate Event items when sweeping""" # test for PR1114 From 43041f72920beb20c0f46a67089fe9ab0ebff16f Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 12 Nov 2023 13:39:34 -0800 Subject: [PATCH 018/104] Pokemon Emerald: Implement New Game (#1813) --- .gitignore | 1 + README.md | 1 + docs/CODEOWNERS | 3 + inno_setup.iss | 5 + worlds/pokemon_emerald/LICENSE | 19 + worlds/pokemon_emerald/README.md | 58 + worlds/pokemon_emerald/__init__.py | 882 ++++++ worlds/pokemon_emerald/client.py | 277 ++ worlds/pokemon_emerald/data.py | 995 +++++++ worlds/pokemon_emerald/data/README.md | 99 + .../pokemon_emerald/data/base_patch.bsdiff4 | Bin 0 -> 209743 bytes .../pokemon_emerald/data/extracted_data.json | 1 + worlds/pokemon_emerald/data/items.json | 1481 ++++++++++ worlds/pokemon_emerald/data/locations.json | 1441 +++++++++ .../pokemon_emerald/data/regions/cities.json | 2604 +++++++++++++++++ .../data/regions/dungeons.json | 2231 ++++++++++++++ .../pokemon_emerald/data/regions/routes.json | 1871 ++++++++++++ .../data/regions/unused/battle_frontier.json | 396 +++ .../data/regions/unused/dungeons.json | 52 + .../data/regions/unused/islands.json | 276 ++ .../data/regions/unused/routes.json | 82 + .../docs/en_Pokemon Emerald.md | 78 + worlds/pokemon_emerald/docs/setup_en.md | 72 + worlds/pokemon_emerald/items.py | 77 + worlds/pokemon_emerald/locations.py | 122 + worlds/pokemon_emerald/options.py | 606 ++++ worlds/pokemon_emerald/pokemon.py | 196 ++ worlds/pokemon_emerald/regions.py | 49 + worlds/pokemon_emerald/rom.py | 420 +++ worlds/pokemon_emerald/rules.py | 1368 +++++++++ worlds/pokemon_emerald/sanity_check.py | 352 +++ worlds/pokemon_emerald/test/__init__.py | 5 + .../test/test_accessibility.py | 178 ++ worlds/pokemon_emerald/test/test_warps.py | 21 + worlds/pokemon_emerald/util.py | 19 + 35 files changed, 16338 insertions(+) create mode 100644 worlds/pokemon_emerald/LICENSE create mode 100644 worlds/pokemon_emerald/README.md create mode 100644 worlds/pokemon_emerald/__init__.py create mode 100644 worlds/pokemon_emerald/client.py create mode 100644 worlds/pokemon_emerald/data.py create mode 100644 worlds/pokemon_emerald/data/README.md create mode 100644 worlds/pokemon_emerald/data/base_patch.bsdiff4 create mode 100644 worlds/pokemon_emerald/data/extracted_data.json create mode 100644 worlds/pokemon_emerald/data/items.json create mode 100644 worlds/pokemon_emerald/data/locations.json create mode 100644 worlds/pokemon_emerald/data/regions/cities.json create mode 100644 worlds/pokemon_emerald/data/regions/dungeons.json create mode 100644 worlds/pokemon_emerald/data/regions/routes.json create mode 100644 worlds/pokemon_emerald/data/regions/unused/battle_frontier.json create mode 100644 worlds/pokemon_emerald/data/regions/unused/dungeons.json create mode 100644 worlds/pokemon_emerald/data/regions/unused/islands.json create mode 100644 worlds/pokemon_emerald/data/regions/unused/routes.json create mode 100644 worlds/pokemon_emerald/docs/en_Pokemon Emerald.md create mode 100644 worlds/pokemon_emerald/docs/setup_en.md create mode 100644 worlds/pokemon_emerald/items.py create mode 100644 worlds/pokemon_emerald/locations.py create mode 100644 worlds/pokemon_emerald/options.py create mode 100644 worlds/pokemon_emerald/pokemon.py create mode 100644 worlds/pokemon_emerald/regions.py create mode 100644 worlds/pokemon_emerald/rom.py create mode 100644 worlds/pokemon_emerald/rules.py create mode 100644 worlds/pokemon_emerald/sanity_check.py create mode 100644 worlds/pokemon_emerald/test/__init__.py create mode 100644 worlds/pokemon_emerald/test/test_accessibility.py create mode 100644 worlds/pokemon_emerald/test/test_warps.py create mode 100644 worlds/pokemon_emerald/util.py diff --git a/.gitignore b/.gitignore index f4bcd35c32ae..aaea45ce985a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.apmc *.apz5 *.aptloz +*.apemerald *.pyc *.pyd *.sfc diff --git a/README.md b/README.md index bcbc885b4678..a6a482942efc 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Currently, the following games are supported: * DOOM 1993 * Terraria * Lingo +* Pokémon Emerald For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 0afc565280f1..83f47235323a 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -95,6 +95,9 @@ # Overcooked! 2 /worlds/overcooked2/ @toasterparty +# Pokemon Emerald +/worlds/pokemon_emerald/ @Zunawe + # Pokemon Red and Blue /worlds/pokemon_rb/ @Alchav diff --git a/inno_setup.iss b/inno_setup.iss index b6f40f770110..d39e2895f4d5 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -153,6 +153,11 @@ Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Arc Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: ".apemerald"; ValueData: "{#MyAppName}pkmnepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/bizhawk +Root: HKCR; Subkey: "{#MyAppName}pkmnepatch"; ValueData: "Archipelago Pokemon Emerald Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/bizhawk +Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; Components: client/bizhawk +Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/bizhawk + Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; diff --git a/worlds/pokemon_emerald/LICENSE b/worlds/pokemon_emerald/LICENSE new file mode 100644 index 000000000000..30b4f413fe4c --- /dev/null +++ b/worlds/pokemon_emerald/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 Zunawe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/worlds/pokemon_emerald/README.md b/worlds/pokemon_emerald/README.md new file mode 100644 index 000000000000..61aee774525f --- /dev/null +++ b/worlds/pokemon_emerald/README.md @@ -0,0 +1,58 @@ +# Pokemon Emerald + +Version 1.2.0 + +This README contains general info useful for understanding the world. Pretty much all the long lists of locations, +regions, and items are stored in `data/` and (mostly) loaded in by `data.py`. Access rules are in `rules.py`. Check +[data/README.md](data/README.md) for more detailed information on the JSON files holding most of the data. + +## Warps + +Quick note to start, you should not be defining or modifying encoded warps from this repository. They're encoded in the +source code repository for the mod, and then assigned to regions in `data/regions/`. All warps in the game already exist +within `extracted_data.json`, and all relevant warps are already placed in `data/regions/` (unless they were deleted +accidentally). + +Many warps are actually two or three events acting as one logical warp. Doorways, for example, are often 2 tiles wide +indoors but only 1 tile wide outdoors. Both indoor warps point to the outdoor warp, and the outdoor warp points to only +one of the indoor warps. We want to describe warps logically in a way that retains information about individual warp +events. That way a 2-tile-wide doorway doesnt look like a one-way warp next to an unrelated two-way warp, but if we want +to randomize the destinations of those warps, we can still get back each individual id of the multi-tile warp. + +This is how warps are encoded: + +`{source_map}:{source_warp_ids}/{dest_map}:{dest_warp_ids}[!]` + +- `source_map`: The map the warp events are located in +- `source_warp_ids`: The ids of all adjacent warp events in source_map which lead to the same destination (these must be +in ascending order) +- `dest_map`: The map of the warp event to which this one is connected +- `dest_warp_ids`: The ids of the warp events in dest_map +- `[!]`: If the warp expects to lead to a destination which doesnot lead back to it, add a ! to the end + +Example: `MAP_LAVARIDGE_TOWN_HOUSE:0,1/MAP_LAVARIDGE_TOWN:4` + +Example 2: `MAP_AQUA_HIDEOUT_B1F:14/MAP_AQUA_HIDEOUT_B1F:12!` + +Note: A warp must have its destination set to another warp event. However, that does not guarantee that the destination +warp event will warp back to the source. + +Note 2: Some warps _only_ act as destinations and cannot actually be interacted with by the player as sources. These are +usually places you fall from a hole above. At the time of writing, these are actually not accounted for, but there are +no instances where it changes logical access. + +Note 3: Some warp destinations go to the map `MAP_DYNAMIC` and have a special warp id. These edge cases are: + +- The Moving Truck +- Terra Cave +- Marine Cave +- The Department Store Elevator +- Secret Bases +- The Trade Center +- The Union Room +- The Record Corner +- 2P/4P Battle Colosseum + +Note 4: The trick house on Route 110 changes the warp destinations of its entrance and ending room as you progress +through the puzzles, but the source code only sets the trick house up for the first puzzle, and I assume the destination +gets overwritten at run time when certain flags are set. diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py new file mode 100644 index 000000000000..d3ced5f3ca62 --- /dev/null +++ b/worlds/pokemon_emerald/__init__.py @@ -0,0 +1,882 @@ +""" +Archipelago World definition for Pokemon Emerald Version +""" +from collections import Counter +import copy +import logging +import os +from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar + +from BaseClasses import ItemClassification, MultiWorld, Tutorial +from Fill import FillError, fill_restrictive +from Options import Toggle +import settings +from worlds.AutoWorld import WebWorld, World + +from .client import PokemonEmeraldClient # Unused, but required to register with BizHawkClient +from .data import (SpeciesData, MapData, EncounterTableData, LearnsetMove, TrainerPokemonData, StaticEncounterData, + TrainerData, data as emerald_data) +from .items import (ITEM_GROUPS, PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification, + offset_item_value) +from .locations import (LOCATION_GROUPS, PokemonEmeraldLocation, create_location_label_to_id_map, + create_locations_with_tags) +from .options import (ItemPoolType, RandomizeWildPokemon, RandomizeBadges, RandomizeTrainerParties, RandomizeHms, + RandomizeStarters, LevelUpMoves, RandomizeAbilities, RandomizeTypes, TmCompatibility, + HmCompatibility, RandomizeStaticEncounters, NormanRequirement, PokemonEmeraldOptions) +from .pokemon import get_random_species, get_random_move, get_random_damaging_move, get_random_type +from .regions import create_regions +from .rom import PokemonEmeraldDeltaPatch, generate_output, location_visited_event_to_id_map +from .rules import set_rules +from .sanity_check import validate_regions +from .util import int_to_bool_array, bool_array_to_int + + +class PokemonEmeraldWebWorld(WebWorld): + """ + Webhost info for Pokemon Emerald + """ + theme = "ocean" + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to playing Pokémon Emerald with Archipelago.", + "English", + "setup_en.md", + "setup/en", + ["Zunawe"] + ) + + tutorials = [setup_en] + + +class PokemonEmeraldSettings(settings.Group): + class PokemonEmeraldRomFile(settings.UserFilePath): + """File name of your English Pokemon Emerald ROM""" + description = "Pokemon Emerald ROM File" + copy_to = "Pokemon - Emerald Version (USA, Europe).gba" + md5s = [PokemonEmeraldDeltaPatch.hash] + + rom_file: PokemonEmeraldRomFile = PokemonEmeraldRomFile(PokemonEmeraldRomFile.copy_to) + + +class PokemonEmeraldWorld(World): + """ + Pokémon Emerald is the definitive Gen III Pokémon game and one of the most beloved in the franchise. + Catch, train, and battle Pokémon, explore the Hoenn region, thwart the plots + of Team Magma and Team Aqua, challenge gyms, and become the Pokémon champion! + """ + game = "Pokemon Emerald" + web = PokemonEmeraldWebWorld() + topology_present = True + + settings_key = "pokemon_emerald_settings" + settings: ClassVar[PokemonEmeraldSettings] + + options_dataclass = PokemonEmeraldOptions + options: PokemonEmeraldOptions + + item_name_to_id = create_item_label_to_code_map() + location_name_to_id = create_location_label_to_id_map() + item_name_groups = ITEM_GROUPS + location_name_groups = LOCATION_GROUPS + + data_version = 1 + required_client_version = (0, 4, 3) + + badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] = None + hm_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] = None + free_fly_location_id: int = 0 + + modified_species: List[Optional[SpeciesData]] + modified_maps: List[MapData] + modified_tmhm_moves: List[int] + modified_static_encounters: List[int] + modified_starters: Tuple[int, int, int] + modified_trainers: List[TrainerData] + + @classmethod + def stage_assert_generate(cls, multiworld: MultiWorld) -> None: + if not os.path.exists(cls.settings.rom_file): + raise FileNotFoundError(cls.settings.rom_file) + + assert validate_regions() + + def get_filler_item_name(self) -> str: + return "Great Ball" + + def generate_early(self) -> None: + # If badges or HMs are vanilla, Norman locks you from using Surf, which means you're not guaranteed to be + # able to reach Fortree Gym, Mossdeep Gym, or Sootopolis Gym. So we can't require reaching those gyms to + # challenge Norman or it creates a circular dependency. + # This is never a problem for completely random badges/hms because the algo will not place Surf/Balance Badge + # on Norman on its own. It's never a problem for shuffled badges/hms because there is no scenario where Cut or + # the Stone Badge can be a lynchpin for access to any gyms, so they can always be put on Norman in a worst case + # scenario. + # This will also be a problem in warp rando if direct access to Norman's room requires Surf or if access + # any gym leader in general requires Surf. We will probably have to force this to 0 in that case. + max_norman_count = 7 + + if self.options.badges == RandomizeBadges.option_vanilla: + max_norman_count = 4 + + if self.options.hms == RandomizeHms.option_vanilla: + if self.options.norman_requirement == NormanRequirement.option_badges: + if self.options.badges != RandomizeBadges.option_completely_random: + max_norman_count = 4 + if self.options.norman_requirement == NormanRequirement.option_gyms: + max_norman_count = 4 + + if self.options.norman_count.value > max_norman_count: + logging.warning("Pokemon Emerald: Norman requirements for Player %s (%s) are unsafe in combination with " + "other settings. Reducing to 4.", self.player, self.multiworld.get_player_name(self.player)) + self.options.norman_count.value = max_norman_count + + def create_regions(self) -> None: + regions = create_regions(self) + + tags = {"Badge", "HM", "KeyItem", "Rod", "Bike"} + if self.options.overworld_items: + tags.add("OverworldItem") + if self.options.hidden_items: + tags.add("HiddenItem") + if self.options.npc_gifts: + tags.add("NpcGift") + if self.options.enable_ferry: + tags.add("Ferry") + create_locations_with_tags(self, regions, tags) + + self.multiworld.regions.extend(regions.values()) + + def create_items(self) -> None: + item_locations: List[PokemonEmeraldLocation] = [ + location + for location in self.multiworld.get_locations(self.player) + if location.address is not None + ] + + # Filter progression items which shouldn't be shuffled into the itempool. Their locations + # still exist, but event items will be placed and locked at their vanilla locations instead. + filter_tags = set() + + if not self.options.key_items: + filter_tags.add("KeyItem") + if not self.options.rods: + filter_tags.add("Rod") + if not self.options.bikes: + filter_tags.add("Bike") + + if self.options.badges in {RandomizeBadges.option_vanilla, RandomizeBadges.option_shuffle}: + filter_tags.add("Badge") + if self.options.hms in {RandomizeHms.option_vanilla, RandomizeHms.option_shuffle}: + filter_tags.add("HM") + + if self.options.badges == RandomizeBadges.option_shuffle: + self.badge_shuffle_info = [ + (location, self.create_item_by_code(location.default_item_code)) + for location in [l for l in item_locations if "Badge" in l.tags] + ] + if self.options.hms == RandomizeHms.option_shuffle: + self.hm_shuffle_info = [ + (location, self.create_item_by_code(location.default_item_code)) + for location in [l for l in item_locations if "HM" in l.tags] + ] + + item_locations = [location for location in item_locations if len(filter_tags & location.tags) == 0] + default_itempool = [self.create_item_by_code(location.default_item_code) for location in item_locations] + + if self.options.item_pool_type == ItemPoolType.option_shuffled: + self.multiworld.itempool += default_itempool + + elif self.options.item_pool_type in {ItemPoolType.option_diverse, ItemPoolType.option_diverse_balanced}: + item_categories = ["Ball", "Heal", "Vitamin", "EvoStone", "Money", "TM", "Held", "Misc"] + + # Count occurrences of types of vanilla items in pool + item_category_counter = Counter() + for item in default_itempool: + if not item.advancement: + item_category_counter.update([tag for tag in item.tags if tag in item_categories]) + + item_category_weights = [item_category_counter.get(category) for category in item_categories] + item_category_weights = [weight if weight is not None else 0 for weight in item_category_weights] + + # Create lists of item codes that can be used to fill + fill_item_candidates = emerald_data.items.values() + + fill_item_candidates = [item for item in fill_item_candidates if "Unique" not in item.tags] + + fill_item_candidates_by_category = {category: [] for category in item_categories} + for item_data in fill_item_candidates: + for category in item_categories: + if category in item_data.tags: + fill_item_candidates_by_category[category].append(offset_item_value(item_data.item_id)) + + for category in fill_item_candidates_by_category: + fill_item_candidates_by_category[category].sort() + + # Ignore vanilla occurrences and pick completely randomly + if self.options.item_pool_type == ItemPoolType.option_diverse: + item_category_weights = [ + len(category_list) + for category_list in fill_item_candidates_by_category.values() + ] + + # TMs should not have duplicates until every TM has been used already + all_tm_choices = fill_item_candidates_by_category["TM"].copy() + + def refresh_tm_choices() -> None: + fill_item_candidates_by_category["TM"] = all_tm_choices.copy() + self.random.shuffle(fill_item_candidates_by_category["TM"]) + + # Create items + for item in default_itempool: + if not item.advancement and "Unique" not in item.tags: + category = self.random.choices(item_categories, item_category_weights)[0] + if category == "TM": + if len(fill_item_candidates_by_category["TM"]) == 0: + refresh_tm_choices() + item_code = fill_item_candidates_by_category["TM"].pop() + else: + item_code = self.random.choice(fill_item_candidates_by_category[category]) + item = self.create_item_by_code(item_code) + + self.multiworld.itempool.append(item) + + def set_rules(self) -> None: + set_rules(self) + + def generate_basic(self) -> None: + locations: List[PokemonEmeraldLocation] = self.multiworld.get_locations(self.player) + + # Set our free fly location + # If not enabled, set it to Littleroot Town by default + fly_location_name = "EVENT_VISITED_LITTLEROOT_TOWN" + if self.options.free_fly_location: + fly_location_name = self.random.choice([ + "EVENT_VISITED_SLATEPORT_CITY", + "EVENT_VISITED_MAUVILLE_CITY", + "EVENT_VISITED_VERDANTURF_TOWN", + "EVENT_VISITED_FALLARBOR_TOWN", + "EVENT_VISITED_LAVARIDGE_TOWN", + "EVENT_VISITED_FORTREE_CITY", + "EVENT_VISITED_LILYCOVE_CITY", + "EVENT_VISITED_MOSSDEEP_CITY", + "EVENT_VISITED_SOOTOPOLIS_CITY", + "EVENT_VISITED_EVER_GRANDE_CITY" + ]) + + self.free_fly_location_id = location_visited_event_to_id_map[fly_location_name] + + free_fly_location_location = self.multiworld.get_location("FREE_FLY_LOCATION", self.player) + free_fly_location_location.item = None + free_fly_location_location.place_locked_item(self.create_event(fly_location_name)) + + # Key items which are considered in access rules but not randomized are converted to events and placed + # in their vanilla locations so that the player can have them in their inventory for logic. + def convert_unrandomized_items_to_events(tag: str) -> None: + for location in locations: + if location.tags is not None and tag in location.tags: + location.place_locked_item(self.create_event(self.item_id_to_name[location.default_item_code])) + location.address = None + + if self.options.badges == RandomizeBadges.option_vanilla: + convert_unrandomized_items_to_events("Badge") + if self.options.hms == RandomizeHms.option_vanilla: + convert_unrandomized_items_to_events("HM") + if not self.options.rods: + convert_unrandomized_items_to_events("Rod") + if not self.options.bikes: + convert_unrandomized_items_to_events("Bike") + if not self.options.key_items: + convert_unrandomized_items_to_events("KeyItem") + + def pre_fill(self) -> None: + # Items which are shuffled between their own locations + if self.options.badges == RandomizeBadges.option_shuffle: + badge_locations: List[PokemonEmeraldLocation] + badge_items: List[PokemonEmeraldItem] + + # Sort order makes `fill_restrictive` try to place important badges later, which + # makes it less likely to have to swap at all, and more likely for swaps to work. + # In the case of vanilla HMs, navigating Granite Cave is required to access more than 2 gyms, + # so Knuckle Badge deserves highest priority if Flash is logically required. + badge_locations, badge_items = [list(l) for l in zip(*self.badge_shuffle_info)] + badge_priority = { + "Knuckle Badge": 0 if (self.options.hms == RandomizeHms.option_vanilla and self.options.require_flash) else 3, + "Balance Badge": 1, + "Dynamo Badge": 1, + "Mind Badge": 2, + "Heat Badge": 2, + "Rain Badge": 3, + "Stone Badge": 4, + "Feather Badge": 5 + } + badge_items.sort(key=lambda item: badge_priority.get(item.name, 0)) + + collection_state = self.multiworld.get_all_state(False) + if self.hm_shuffle_info is not None: + for _, item in self.hm_shuffle_info: + collection_state.collect(item) + + # In specific very constrained conditions, fill_restrictive may run + # out of swaps before it finds a valid solution if it gets unlucky. + # This is a band-aid until fill/swap can reliably find those solutions. + attempts_remaining = 2 + while attempts_remaining > 0: + attempts_remaining -= 1 + self.random.shuffle(badge_locations) + try: + fill_restrictive(self.multiworld, collection_state, badge_locations, badge_items, + single_player_placement=True, lock=True, allow_excluded=True) + break + except FillError as exc: + if attempts_remaining == 0: + raise exc + + logging.debug(f"Failed to shuffle badges for player {self.player}. Retrying.") + continue + + if self.options.hms == RandomizeHms.option_shuffle: + hm_locations: List[PokemonEmeraldLocation] + hm_items: List[PokemonEmeraldItem] + + # Sort order makes `fill_restrictive` try to place important HMs later, which + # makes it less likely to have to swap at all, and more likely for swaps to work. + # In the case of vanilla badges, navigating Granite Cave is required to access more than 2 gyms, + # so Flash deserves highest priority if it's logically required. + hm_locations, hm_items = [list(l) for l in zip(*self.hm_shuffle_info)] + hm_priority = { + "HM05 Flash": 0 if (self.options.badges == RandomizeBadges.option_vanilla and self.options.require_flash) else 3, + "HM03 Surf": 1, + "HM06 Rock Smash": 1, + "HM08 Dive": 2, + "HM04 Strength": 2, + "HM07 Waterfall": 3, + "HM01 Cut": 4, + "HM02 Fly": 5 + } + hm_items.sort(key=lambda item: hm_priority.get(item.name, 0)) + + collection_state = self.multiworld.get_all_state(False) + + # In specific very constrained conditions, fill_restrictive may run + # out of swaps before it finds a valid solution if it gets unlucky. + # This is a band-aid until fill/swap can reliably find those solutions. + attempts_remaining = 2 + while attempts_remaining > 0: + attempts_remaining -= 1 + self.random.shuffle(hm_locations) + try: + fill_restrictive(self.multiworld, collection_state, hm_locations, hm_items, + single_player_placement=True, lock=True, allow_excluded=True) + break + except FillError as exc: + if attempts_remaining == 0: + raise exc + + logging.debug(f"Failed to shuffle HMs for player {self.player}. Retrying.") + continue + + def generate_output(self, output_directory: str) -> None: + def randomize_abilities() -> None: + # Creating list of potential abilities + ability_label_to_value = {ability.label.lower(): ability.ability_id for ability in emerald_data.abilities} + + ability_blacklist_labels = {"cacophony"} + option_ability_blacklist = self.options.ability_blacklist.value + if option_ability_blacklist is not None: + ability_blacklist_labels |= {ability_label.lower() for ability_label in option_ability_blacklist} + + ability_blacklist = {ability_label_to_value[label] for label in ability_blacklist_labels} + ability_whitelist = [a.ability_id for a in emerald_data.abilities if a.ability_id not in ability_blacklist] + + if self.options.abilities == RandomizeAbilities.option_follow_evolutions: + already_modified: Set[int] = set() + + # Loops through species and only tries to modify abilities if the pokemon has no pre-evolution + # or if the pre-evolution has already been modified. Then tries to modify all species that evolve + # from this one which have the same abilities. + # The outer while loop only runs three times for vanilla ordering: Once for a first pass, once for + # Hitmonlee/Hitmonchan, and once to verify that there's nothing left to do. + while True: + had_clean_pass = True + for species in self.modified_species: + if species is None: + continue + if species.species_id in already_modified: + continue + if species.pre_evolution is not None and species.pre_evolution not in already_modified: + continue + + had_clean_pass = False + + old_abilities = species.abilities + new_abilities = ( + 0 if old_abilities[0] == 0 else self.random.choice(ability_whitelist), + 0 if old_abilities[1] == 0 else self.random.choice(ability_whitelist) + ) + + evolutions = [species] + while len(evolutions) > 0: + evolution = evolutions.pop() + if evolution.abilities == old_abilities: + evolution.abilities = new_abilities + already_modified.add(evolution.species_id) + evolutions += [ + self.modified_species[evolution.species_id] + for evolution in evolution.evolutions + if evolution.species_id not in already_modified + ] + + if had_clean_pass: + break + else: # Not following evolutions + for species in self.modified_species: + if species is None: + continue + + old_abilities = species.abilities + new_abilities = ( + 0 if old_abilities[0] == 0 else self.random.choice(ability_whitelist), + 0 if old_abilities[1] == 0 else self.random.choice(ability_whitelist) + ) + + species.abilities = new_abilities + + def randomize_types() -> None: + if self.options.types == RandomizeTypes.option_shuffle: + type_map = list(range(18)) + self.random.shuffle(type_map) + + # We never want to map to the ??? type, so swap whatever index maps to ??? with ??? + # So ??? will always map to itself, and there are no pokemon which have the ??? type + mystery_type_index = type_map.index(9) + type_map[mystery_type_index], type_map[9] = type_map[9], type_map[mystery_type_index] + + for species in self.modified_species: + if species is not None: + species.types = (type_map[species.types[0]], type_map[species.types[1]]) + elif self.options.types == RandomizeTypes.option_completely_random: + for species in self.modified_species: + if species is not None: + new_type_1 = get_random_type(self.random) + new_type_2 = new_type_1 + if species.types[0] != species.types[1]: + while new_type_1 == new_type_2: + new_type_2 = get_random_type(self.random) + + species.types = (new_type_1, new_type_2) + elif self.options.types == RandomizeTypes.option_follow_evolutions: + already_modified: Set[int] = set() + + # Similar to follow evolutions for abilities, but only needs to loop through once. + # For every pokemon without a pre-evolution, generates a random mapping from old types to new types + # and then walks through the evolution tree applying that map. This means that evolutions that share + # types will have those types mapped to the same new types, and evolutions with new or diverging types + # will still have new or diverging types. + # Consider: + # - Charmeleon (Fire/Fire) -> Charizard (Fire/Flying) + # - Onyx (Rock/Ground) -> Steelix (Steel/Ground) + # - Nincada (Bug/Ground) -> Ninjask (Bug/Flying) && Shedinja (Bug/Ghost) + # - Azurill (Normal/Normal) -> Marill (Water/Water) + for species in self.modified_species: + if species is None: + continue + if species.species_id in already_modified: + continue + if species.pre_evolution is not None and species.pre_evolution not in already_modified: + continue + + type_map = list(range(18)) + self.random.shuffle(type_map) + + # We never want to map to the ??? type, so swap whatever index maps to ??? with ??? + # So ??? will always map to itself, and there are no pokemon which have the ??? type + mystery_type_index = type_map.index(9) + type_map[mystery_type_index], type_map[9] = type_map[9], type_map[mystery_type_index] + + evolutions = [species] + while len(evolutions) > 0: + evolution = evolutions.pop() + evolution.types = (type_map[evolution.types[0]], type_map[evolution.types[1]]) + already_modified.add(evolution.species_id) + evolutions += [self.modified_species[evo.species_id] for evo in evolution.evolutions] + + def randomize_learnsets() -> None: + type_bias = self.options.move_match_type_bias.value + normal_bias = self.options.move_normal_type_bias.value + + for species in self.modified_species: + if species is None: + continue + + old_learnset = species.learnset + new_learnset: List[LearnsetMove] = [] + + i = 0 + # Replace filler MOVE_NONEs at start of list + while old_learnset[i].move_id == 0: + if self.options.level_up_moves == LevelUpMoves.option_start_with_four_moves: + new_move = get_random_move(self.random, {move.move_id for move in new_learnset}, type_bias, + normal_bias, species.types) + else: + new_move = 0 + new_learnset.append(LearnsetMove(old_learnset[i].level, new_move)) + i += 1 + + while i < len(old_learnset): + # Guarantees the starter has a good damaging move + if i == 3: + new_move = get_random_damaging_move(self.random, {move.move_id for move in new_learnset}) + else: + new_move = get_random_move(self.random, {move.move_id for move in new_learnset}, type_bias, + normal_bias, species.types) + new_learnset.append(LearnsetMove(old_learnset[i].level, new_move)) + i += 1 + + species.learnset = new_learnset + + def randomize_tm_hm_compatibility() -> None: + for species in self.modified_species: + if species is None: + continue + + combatibility_array = int_to_bool_array(species.tm_hm_compatibility) + + # TMs + for i in range(0, 50): + if self.options.tm_compatibility == TmCompatibility.option_fully_compatible: + combatibility_array[i] = True + elif self.options.tm_compatibility == TmCompatibility.option_completely_random: + combatibility_array[i] = self.random.choice([True, False]) + + # HMs + for i in range(50, 58): + if self.options.hm_compatibility == HmCompatibility.option_fully_compatible: + combatibility_array[i] = True + elif self.options.hm_compatibility == HmCompatibility.option_completely_random: + combatibility_array[i] = self.random.choice([True, False]) + + species.tm_hm_compatibility = bool_array_to_int(combatibility_array) + + def randomize_tm_moves() -> None: + new_moves: Set[int] = set() + + for i in range(50): + new_move = get_random_move(self.random, new_moves) + new_moves.add(new_move) + self.modified_tmhm_moves[i] = new_move + + def randomize_wild_encounters() -> None: + should_match_bst = self.options.wild_pokemon in { + RandomizeWildPokemon.option_match_base_stats, + RandomizeWildPokemon.option_match_base_stats_and_type + } + should_match_type = self.options.wild_pokemon in { + RandomizeWildPokemon.option_match_type, + RandomizeWildPokemon.option_match_base_stats_and_type + } + should_allow_legendaries = self.options.allow_wild_legendaries == Toggle.option_true + + for map_data in self.modified_maps: + new_encounters: List[Optional[EncounterTableData]] = [None, None, None] + old_encounters = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters] + + for i, table in enumerate(old_encounters): + if table is not None: + species_old_to_new_map: Dict[int, int] = {} + for species_id in table.slots: + if species_id not in species_old_to_new_map: + original_species = emerald_data.species[species_id] + target_bst = sum(original_species.base_stats) if should_match_bst else None + target_type = self.random.choice(original_species.types) if should_match_type else None + + species_old_to_new_map[species_id] = get_random_species( + self.random, + self.modified_species, + target_bst, + target_type, + should_allow_legendaries + ).species_id + + new_slots: List[int] = [] + for species_id in table.slots: + new_slots.append(species_old_to_new_map[species_id]) + + new_encounters[i] = EncounterTableData(new_slots, table.rom_address) + + map_data.land_encounters = new_encounters[0] + map_data.water_encounters = new_encounters[1] + map_data.fishing_encounters = new_encounters[2] + + def randomize_static_encounters() -> None: + if self.options.static_encounters == RandomizeStaticEncounters.option_shuffle: + shuffled_species = [encounter.species_id for encounter in emerald_data.static_encounters] + self.random.shuffle(shuffled_species) + + self.modified_static_encounters = [] + for i, encounter in enumerate(emerald_data.static_encounters): + self.modified_static_encounters.append(StaticEncounterData( + shuffled_species[i], + encounter.rom_address + )) + else: + should_match_bst = self.options.static_encounters in { + RandomizeStaticEncounters.option_match_base_stats, + RandomizeStaticEncounters.option_match_base_stats_and_type + } + should_match_type = self.options.static_encounters in { + RandomizeStaticEncounters.option_match_type, + RandomizeStaticEncounters.option_match_base_stats_and_type + } + + for encounter in emerald_data.static_encounters: + original_species = self.modified_species[encounter.species_id] + target_bst = sum(original_species.base_stats) if should_match_bst else None + target_type = self.random.choice(original_species.types) if should_match_type else None + + self.modified_static_encounters.append(StaticEncounterData( + get_random_species(self.random, self.modified_species, target_bst, target_type).species_id, + encounter.rom_address + )) + + def randomize_opponent_parties() -> None: + should_match_bst = self.options.trainer_parties in { + RandomizeTrainerParties.option_match_base_stats, + RandomizeTrainerParties.option_match_base_stats_and_type + } + should_match_type = self.options.trainer_parties in { + RandomizeTrainerParties.option_match_type, + RandomizeTrainerParties.option_match_base_stats_and_type + } + allow_legendaries = self.options.allow_trainer_legendaries == Toggle.option_true + + per_species_tmhm_moves: Dict[int, List[int]] = {} + + for trainer in self.modified_trainers: + new_party = [] + for pokemon in trainer.party.pokemon: + original_species = emerald_data.species[pokemon.species_id] + target_bst = sum(original_species.base_stats) if should_match_bst else None + target_type = self.random.choice(original_species.types) if should_match_type else None + + new_species = get_random_species( + self.random, + self.modified_species, + target_bst, + target_type, + allow_legendaries + ) + + if new_species.species_id not in per_species_tmhm_moves: + per_species_tmhm_moves[new_species.species_id] = list({ + self.modified_tmhm_moves[i] + for i, is_compatible in enumerate(int_to_bool_array(new_species.tm_hm_compatibility)) + if is_compatible + }) + + tm_hm_movepool = per_species_tmhm_moves[new_species.species_id] + level_up_movepool = list({ + move.move_id + for move in new_species.learnset + if move.level <= pokemon.level + }) + + new_moves = ( + self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool), + self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool), + self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool), + self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool) + ) + + new_party.append(TrainerPokemonData(new_species.species_id, pokemon.level, new_moves)) + + trainer.party.pokemon = new_party + + def randomize_starters() -> None: + match_bst = self.options.starters in { + RandomizeStarters.option_match_base_stats, + RandomizeStarters.option_match_base_stats_and_type + } + match_type = self.options.starters in { + RandomizeStarters.option_match_type, + RandomizeStarters.option_match_base_stats_and_type + } + allow_legendaries = self.options.allow_starter_legendaries == Toggle.option_true + + starter_bsts = ( + sum(emerald_data.species[emerald_data.starters[0]].base_stats) if match_bst else None, + sum(emerald_data.species[emerald_data.starters[1]].base_stats) if match_bst else None, + sum(emerald_data.species[emerald_data.starters[2]].base_stats) if match_bst else None + ) + + starter_types = ( + self.random.choice(emerald_data.species[emerald_data.starters[0]].types) if match_type else None, + self.random.choice(emerald_data.species[emerald_data.starters[1]].types) if match_type else None, + self.random.choice(emerald_data.species[emerald_data.starters[2]].types) if match_type else None + ) + + new_starters = ( + get_random_species(self.random, self.modified_species, + starter_bsts[0], starter_types[0], allow_legendaries), + get_random_species(self.random, self.modified_species, + starter_bsts[1], starter_types[1], allow_legendaries), + get_random_species(self.random, self.modified_species, + starter_bsts[2], starter_types[2], allow_legendaries) + ) + + egg_code = self.options.easter_egg.value + egg_check_1 = 0 + egg_check_2 = 0 + + for i in egg_code: + egg_check_1 += ord(i) + egg_check_2 += egg_check_1 * egg_check_1 + + egg = 96 + egg_check_2 - (egg_check_1 * 0x077C) + if egg_check_2 == 0x14E03A and egg < 411 and egg > 0 and egg not in range(252, 277): + self.modified_starters = (egg, egg, egg) + else: + self.modified_starters = ( + new_starters[0].species_id, + new_starters[1].species_id, + new_starters[2].species_id + ) + + # Putting the unchosen starter onto the rival's team + rival_teams: List[List[Tuple[str, int, bool]]] = [ + [ + ("TRAINER_BRENDAN_ROUTE_103_TREECKO", 0, False), + ("TRAINER_BRENDAN_RUSTBORO_TREECKO", 1, False), + ("TRAINER_BRENDAN_ROUTE_110_TREECKO", 2, True ), + ("TRAINER_BRENDAN_ROUTE_119_TREECKO", 2, True ), + ("TRAINER_BRENDAN_LILYCOVE_TREECKO", 3, True ), + ("TRAINER_MAY_ROUTE_103_TREECKO", 0, False), + ("TRAINER_MAY_RUSTBORO_TREECKO", 1, False), + ("TRAINER_MAY_ROUTE_110_TREECKO", 2, True ), + ("TRAINER_MAY_ROUTE_119_TREECKO", 2, True ), + ("TRAINER_MAY_LILYCOVE_TREECKO", 3, True ) + ], + [ + ("TRAINER_BRENDAN_ROUTE_103_TORCHIC", 0, False), + ("TRAINER_BRENDAN_RUSTBORO_TORCHIC", 1, False), + ("TRAINER_BRENDAN_ROUTE_110_TORCHIC", 2, True ), + ("TRAINER_BRENDAN_ROUTE_119_TORCHIC", 2, True ), + ("TRAINER_BRENDAN_LILYCOVE_TORCHIC", 3, True ), + ("TRAINER_MAY_ROUTE_103_TORCHIC", 0, False), + ("TRAINER_MAY_RUSTBORO_TORCHIC", 1, False), + ("TRAINER_MAY_ROUTE_110_TORCHIC", 2, True ), + ("TRAINER_MAY_ROUTE_119_TORCHIC", 2, True ), + ("TRAINER_MAY_LILYCOVE_TORCHIC", 3, True ) + ], + [ + ("TRAINER_BRENDAN_ROUTE_103_MUDKIP", 0, False), + ("TRAINER_BRENDAN_RUSTBORO_MUDKIP", 1, False), + ("TRAINER_BRENDAN_ROUTE_110_MUDKIP", 2, True ), + ("TRAINER_BRENDAN_ROUTE_119_MUDKIP", 2, True ), + ("TRAINER_BRENDAN_LILYCOVE_MUDKIP", 3, True ), + ("TRAINER_MAY_ROUTE_103_MUDKIP", 0, False), + ("TRAINER_MAY_RUSTBORO_MUDKIP", 1, False), + ("TRAINER_MAY_ROUTE_110_MUDKIP", 2, True ), + ("TRAINER_MAY_ROUTE_119_MUDKIP", 2, True ), + ("TRAINER_MAY_LILYCOVE_MUDKIP", 3, True ) + ] + ] + + for i, starter in enumerate([new_starters[1], new_starters[2], new_starters[0]]): + potential_evolutions = [evolution.species_id for evolution in starter.evolutions] + picked_evolution = starter.species_id + if len(potential_evolutions) > 0: + picked_evolution = self.random.choice(potential_evolutions) + + for trainer_name, starter_position, is_evolved in rival_teams[i]: + trainer_data = self.modified_trainers[emerald_data.constants[trainer_name]] + trainer_data.party.pokemon[starter_position].species_id = picked_evolution if is_evolved else starter.species_id + + self.modified_species = copy.deepcopy(emerald_data.species) + self.modified_trainers = copy.deepcopy(emerald_data.trainers) + self.modified_maps = copy.deepcopy(emerald_data.maps) + self.modified_tmhm_moves = copy.deepcopy(emerald_data.tmhm_moves) + self.modified_static_encounters = copy.deepcopy(emerald_data.static_encounters) + self.modified_starters = copy.deepcopy(emerald_data.starters) + + # Randomize species data + if self.options.abilities != RandomizeAbilities.option_vanilla: + randomize_abilities() + + if self.options.types != RandomizeTypes.option_vanilla: + randomize_types() + + if self.options.level_up_moves != LevelUpMoves.option_vanilla: + randomize_learnsets() + + randomize_tm_hm_compatibility() # Options are checked within this function + + min_catch_rate = min(self.options.min_catch_rate.value, 255) + for species in self.modified_species: + if species is not None: + species.catch_rate = max(species.catch_rate, min_catch_rate) + + if self.options.tm_moves: + randomize_tm_moves() + + # Randomize wild encounters + if self.options.wild_pokemon != RandomizeWildPokemon.option_vanilla: + randomize_wild_encounters() + + # Randomize static encounters + if self.options.static_encounters != RandomizeStaticEncounters.option_vanilla: + randomize_static_encounters() + + # Randomize opponents + if self.options.trainer_parties != RandomizeTrainerParties.option_vanilla: + randomize_opponent_parties() + + # Randomize starters + if self.options.starters != RandomizeStarters.option_vanilla: + randomize_starters() + + generate_output(self, output_directory) + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data = self.options.as_dict( + "goal", + "badges", + "hms", + "key_items", + "bikes", + "rods", + "overworld_items", + "hidden_items", + "npc_gifts", + "require_itemfinder", + "require_flash", + "enable_ferry", + "elite_four_requirement", + "elite_four_count", + "norman_requirement", + "norman_count", + "extra_boulders", + "remove_roadblocks", + "free_fly_location", + "fly_without_badge", + ) + slot_data["free_fly_location_id"] = self.free_fly_location_id + return slot_data + + def create_item(self, name: str) -> PokemonEmeraldItem: + return self.create_item_by_code(self.item_name_to_id[name]) + + def create_item_by_code(self, item_code: int) -> PokemonEmeraldItem: + return PokemonEmeraldItem( + self.item_id_to_name[item_code], + get_item_classification(item_code), + item_code, + self.player + ) + + def create_event(self, name: str) -> PokemonEmeraldItem: + return PokemonEmeraldItem( + name, + ItemClassification.progression, + None, + self.player + ) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py new file mode 100644 index 000000000000..5420b15fbe95 --- /dev/null +++ b/worlds/pokemon_emerald/client.py @@ -0,0 +1,277 @@ +from typing import TYPE_CHECKING, Dict, Set + +from NetUtils import ClientStatus +import worlds._bizhawk as bizhawk +from worlds._bizhawk.client import BizHawkClient + +from .data import BASE_OFFSET, data +from .options import Goal + +if TYPE_CHECKING: + from worlds._bizhawk.context import BizHawkClientContext + + +EXPECTED_ROM_NAME = "pokemon emerald version / AP 2" + +IS_CHAMPION_FLAG = data.constants["FLAG_IS_CHAMPION"] +DEFEATED_STEVEN_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_STEVEN"] +DEFEATED_NORMAN_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_NORMAN_1"] + +# These flags are communicated to the tracker as a bitfield using this order. +# Modifying the order will cause undetectable autotracking issues. +TRACKER_EVENT_FLAGS = [ + "FLAG_DEFEATED_RUSTBORO_GYM", + "FLAG_DEFEATED_DEWFORD_GYM", + "FLAG_DEFEATED_MAUVILLE_GYM", + "FLAG_DEFEATED_LAVARIDGE_GYM", + "FLAG_DEFEATED_PETALBURG_GYM", + "FLAG_DEFEATED_FORTREE_GYM", + "FLAG_DEFEATED_MOSSDEEP_GYM", + "FLAG_DEFEATED_SOOTOPOLIS_GYM", + "FLAG_RECEIVED_POKENAV", # Talk to Mr. Stone + "FLAG_DELIVERED_STEVEN_LETTER", + "FLAG_DELIVERED_DEVON_GOODS", + "FLAG_HIDE_ROUTE_119_TEAM_AQUA", # Clear Weather Institute + "FLAG_MET_ARCHIE_METEOR_FALLS", # Magma steals meteorite + "FLAG_GROUDON_AWAKENED_MAGMA_HIDEOUT", # Clear Magma Hideout + "FLAG_MET_TEAM_AQUA_HARBOR", # Aqua steals submarine + "FLAG_TEAM_AQUA_ESCAPED_IN_SUBMARINE", # Clear Aqua Hideout + "FLAG_HIDE_MOSSDEEP_CITY_SPACE_CENTER_MAGMA_NOTE", # Clear Space Center + "FLAG_KYOGRE_ESCAPED_SEAFLOOR_CAVERN", + "FLAG_HIDE_SKY_PILLAR_TOP_RAYQUAZA", # Rayquaza departs for Sootopolis + "FLAG_OMIT_DIVE_FROM_STEVEN_LETTER", # Steven gives Dive HM (clears seafloor cavern grunt) + "FLAG_IS_CHAMPION", + "FLAG_PURCHASED_HARBOR_MAIL" +] +EVENT_FLAG_MAP = {data.constants[flag_name]: flag_name for flag_name in TRACKER_EVENT_FLAGS} + +KEY_LOCATION_FLAGS = [ + "NPC_GIFT_RECEIVED_HM01", + "NPC_GIFT_RECEIVED_HM02", + "NPC_GIFT_RECEIVED_HM03", + "NPC_GIFT_RECEIVED_HM04", + "NPC_GIFT_RECEIVED_HM05", + "NPC_GIFT_RECEIVED_HM06", + "NPC_GIFT_RECEIVED_HM07", + "NPC_GIFT_RECEIVED_HM08", + "NPC_GIFT_RECEIVED_ACRO_BIKE", + "NPC_GIFT_RECEIVED_WAILMER_PAIL", + "NPC_GIFT_RECEIVED_DEVON_GOODS_RUSTURF_TUNNEL", + "NPC_GIFT_RECEIVED_LETTER", + "NPC_GIFT_RECEIVED_METEORITE", + "NPC_GIFT_RECEIVED_GO_GOGGLES", + "NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON", + "NPC_GIFT_RECEIVED_ITEMFINDER", + "NPC_GIFT_RECEIVED_DEVON_SCOPE", + "NPC_GIFT_RECEIVED_MAGMA_EMBLEM", + "NPC_GIFT_RECEIVED_POKEBLOCK_CASE", + "NPC_GIFT_RECEIVED_SS_TICKET", + "HIDDEN_ITEM_ABANDONED_SHIP_RM_2_KEY", + "HIDDEN_ITEM_ABANDONED_SHIP_RM_1_KEY", + "HIDDEN_ITEM_ABANDONED_SHIP_RM_4_KEY", + "HIDDEN_ITEM_ABANDONED_SHIP_RM_6_KEY", + "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_4_SCANNER", + "ITEM_ABANDONED_SHIP_CAPTAINS_OFFICE_STORAGE_KEY", + "NPC_GIFT_RECEIVED_OLD_ROD", + "NPC_GIFT_RECEIVED_GOOD_ROD", + "NPC_GIFT_RECEIVED_SUPER_ROD", +] +KEY_LOCATION_FLAG_MAP = {data.locations[location_name].flag: location_name for location_name in KEY_LOCATION_FLAGS} + + +class PokemonEmeraldClient(BizHawkClient): + game = "Pokemon Emerald" + system = "GBA" + patch_suffix = ".apemerald" + local_checked_locations: Set[int] + local_set_events: Dict[str, bool] + local_found_key_items: Dict[str, bool] + goal_flag: int + + def __init__(self) -> None: + super().__init__() + self.local_checked_locations = set() + self.local_set_events = {} + self.local_found_key_items = {} + self.goal_flag = IS_CHAMPION_FLAG + + async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: + from CommonClient import logger + + try: + # Check ROM name/patch version + rom_name_bytes = ((await bizhawk.read(ctx.bizhawk_ctx, [(0x108, 32, "ROM")]))[0]) + rom_name = bytes([byte for byte in rom_name_bytes if byte != 0]).decode("ascii") + if not rom_name.startswith("pokemon emerald version"): + return False + if rom_name == "pokemon emerald version": + logger.info("ERROR: You appear to be running an unpatched version of Pokemon Emerald. " + "You need to generate a patch file and use it to create a patched ROM.") + return False + if rom_name != EXPECTED_ROM_NAME: + logger.info("ERROR: The patch file used to create this ROM is not compatible with " + "this client. Double check your client version against the version being " + "used by the generator.") + return False + except UnicodeDecodeError: + return False + except bizhawk.RequestFailedError: + return False # Should verify on the next pass + + ctx.game = self.game + ctx.items_handling = 0b001 + ctx.want_slot_data = True + ctx.watcher_timeout = 0.125 + + return True + + async def set_auth(self, ctx: "BizHawkClientContext") -> None: + slot_name_bytes = (await bizhawk.read(ctx.bizhawk_ctx, [(data.rom_addresses["gArchipelagoInfo"], 64, "ROM")]))[0] + ctx.auth = bytes([byte for byte in slot_name_bytes if byte != 0]).decode("utf-8") + + async def game_watcher(self, ctx: "BizHawkClientContext") -> None: + if ctx.slot_data is not None: + if ctx.slot_data["goal"] == Goal.option_champion: + self.goal_flag = IS_CHAMPION_FLAG + elif ctx.slot_data["goal"] == Goal.option_steven: + self.goal_flag = DEFEATED_STEVEN_FLAG + elif ctx.slot_data["goal"] == Goal.option_norman: + self.goal_flag = DEFEATED_NORMAN_FLAG + + try: + # Checks that the player is in the overworld + overworld_guard = (data.ram_addresses["gMain"] + 4, (data.ram_addresses["CB2_Overworld"] + 1).to_bytes(4, "little"), "System Bus") + + # Read save block address + read_result = await bizhawk.guarded_read( + ctx.bizhawk_ctx, + [(data.ram_addresses["gSaveBlock1Ptr"], 4, "System Bus")], + [overworld_guard] + ) + if read_result is None: # Not in overworld + return + + # Checks that the save block hasn't moved + save_block_address_guard = (data.ram_addresses["gSaveBlock1Ptr"], read_result[0], "System Bus") + + save_block_address = int.from_bytes(read_result[0], "little") + + # Handle giving the player items + read_result = await bizhawk.guarded_read( + ctx.bizhawk_ctx, + [ + (save_block_address + 0x3778, 2, "System Bus"), # Number of received items + (data.ram_addresses["gArchipelagoReceivedItem"] + 4, 1, "System Bus") # Received item struct full? + ], + [overworld_guard, save_block_address_guard] + ) + if read_result is None: # Not in overworld, or save block moved + return + + num_received_items = int.from_bytes(read_result[0], "little") + received_item_is_empty = read_result[1][0] == 0 + + # If the game hasn't received all items yet and the received item struct doesn't contain an item, then + # fill it with the next item + if num_received_items < len(ctx.items_received) and received_item_is_empty: + next_item = ctx.items_received[num_received_items] + await bizhawk.write(ctx.bizhawk_ctx, [ + (data.ram_addresses["gArchipelagoReceivedItem"] + 0, (next_item.item - BASE_OFFSET).to_bytes(2, "little"), "System Bus"), + (data.ram_addresses["gArchipelagoReceivedItem"] + 2, (num_received_items + 1).to_bytes(2, "little"), "System Bus"), + (data.ram_addresses["gArchipelagoReceivedItem"] + 4, [1], "System Bus"), # Mark struct full + (data.ram_addresses["gArchipelagoReceivedItem"] + 5, [next_item.flags & 1], "System Bus"), + ]) + + # Read flags in 2 chunks + read_result = await bizhawk.guarded_read( + ctx.bizhawk_ctx, + [(save_block_address + 0x1450, 0x96, "System Bus")], # Flags + [overworld_guard, save_block_address_guard] + ) + if read_result is None: # Not in overworld, or save block moved + return + + flag_bytes = read_result[0] + + read_result = await bizhawk.guarded_read( + ctx.bizhawk_ctx, + [(save_block_address + 0x14E6, 0x96, "System Bus")], # Flags + [overworld_guard, save_block_address_guard] + ) + if read_result is not None: + flag_bytes += read_result[0] + + game_clear = False + local_checked_locations = set() + local_set_events = {flag_name: False for flag_name in TRACKER_EVENT_FLAGS} + local_found_key_items = {location_name: False for location_name in KEY_LOCATION_FLAGS} + + # Check set flags + for byte_i, byte in enumerate(flag_bytes): + for i in range(8): + if byte & (1 << i) != 0: + flag_id = byte_i * 8 + i + + location_id = flag_id + BASE_OFFSET + if location_id in ctx.server_locations: + local_checked_locations.add(location_id) + + if flag_id == self.goal_flag: + game_clear = True + + if flag_id in EVENT_FLAG_MAP: + local_set_events[EVENT_FLAG_MAP[flag_id]] = True + + if flag_id in KEY_LOCATION_FLAG_MAP: + local_found_key_items[KEY_LOCATION_FLAG_MAP[flag_id]] = True + + # Send locations + if local_checked_locations != self.local_checked_locations: + self.local_checked_locations = local_checked_locations + + if local_checked_locations is not None: + await ctx.send_msgs([{ + "cmd": "LocationChecks", + "locations": list(local_checked_locations) + }]) + + # Send game clear + if not ctx.finished_game and game_clear: + await ctx.send_msgs([{ + "cmd": "StatusUpdate", + "status": ClientStatus.CLIENT_GOAL + }]) + + # Send tracker event flags + if local_set_events != self.local_set_events and ctx.slot is not None: + event_bitfield = 0 + for i, flag_name in enumerate(TRACKER_EVENT_FLAGS): + if local_set_events[flag_name]: + event_bitfield |= 1 << i + + await ctx.send_msgs([{ + "cmd": "Set", + "key": f"pokemon_emerald_events_{ctx.team}_{ctx.slot}", + "default": 0, + "want_reply": False, + "operations": [{"operation": "replace", "value": event_bitfield}] + }]) + self.local_set_events = local_set_events + + if local_found_key_items != self.local_found_key_items: + key_bitfield = 0 + for i, location_name in enumerate(KEY_LOCATION_FLAGS): + if local_found_key_items[location_name]: + key_bitfield |= 1 << i + + await ctx.send_msgs([{ + "cmd": "Set", + "key": f"pokemon_emerald_keys_{ctx.team}_{ctx.slot}", + "default": 0, + "want_reply": False, + "operations": [{"operation": "replace", "value": key_bitfield}] + }]) + self.local_found_key_items = local_found_key_items + except bizhawk.RequestFailedError: + # Exit handler and return to main loop to reconnect + pass diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py new file mode 100644 index 000000000000..bc51d84963c5 --- /dev/null +++ b/worlds/pokemon_emerald/data.py @@ -0,0 +1,995 @@ +""" +Pulls data from JSON files in worlds/pokemon_emerald/data/ into classes. +This also includes marrying automatically extracted data with manually +defined data (like location labels or usable pokemon species), some cleanup +and sorting, and Warp methods. +""" +from dataclasses import dataclass +import copy +from enum import IntEnum +import orjson +from typing import Dict, List, NamedTuple, Optional, Set, FrozenSet, Tuple, Any, Union +import pkgutil +import pkg_resources + +from BaseClasses import ItemClassification + + +BASE_OFFSET = 3860000 + + +class Warp: + """ + Represents warp events in the game like doorways or warp pads + """ + is_one_way: bool + source_map: str + source_ids: List[int] + dest_map: str + dest_ids: List[int] + parent_region: Optional[str] + + def __init__(self, encoded_string: Optional[str] = None, parent_region: Optional[str] = None) -> None: + if encoded_string is not None: + decoded_warp = Warp.decode(encoded_string) + self.is_one_way = decoded_warp.is_one_way + self.source_map = decoded_warp.source_map + self.source_ids = decoded_warp.source_ids + self.dest_map = decoded_warp.dest_map + self.dest_ids = decoded_warp.dest_ids + self.parent_region = parent_region + + def encode(self) -> str: + """ + Returns a string encoding of this warp + """ + source_ids_string = "" + for source_id in self.source_ids: + source_ids_string += str(source_id) + "," + source_ids_string = source_ids_string[:-1] # Remove last "," + + dest_ids_string = "" + for dest_id in self.dest_ids: + dest_ids_string += str(dest_id) + "," + dest_ids_string = dest_ids_string[:-1] # Remove last "," + + return f"{self.source_map}:{source_ids_string}/{self.dest_map}:{dest_ids_string}{'!' if self.is_one_way else ''}" + + def connects_to(self, other: 'Warp') -> bool: + """ + Returns true if this warp sends the player to `other` + """ + return self.dest_map == other.source_map and set(self.dest_ids) <= set(other.source_ids) + + @staticmethod + def decode(encoded_string: str) -> 'Warp': + """ + Create a Warp object from an encoded string + """ + warp = Warp() + warp.is_one_way = encoded_string.endswith("!") + if warp.is_one_way: + encoded_string = encoded_string[:-1] + + warp_source, warp_dest = encoded_string.split("/") + warp_source_map, warp_source_indices = warp_source.split(":") + warp_dest_map, warp_dest_indices = warp_dest.split(":") + + warp.source_map = warp_source_map + warp.dest_map = warp_dest_map + + warp.source_ids = [int(index) for index in warp_source_indices.split(",")] + warp.dest_ids = [int(index) for index in warp_dest_indices.split(",")] + + return warp + + +class ItemData(NamedTuple): + label: str + item_id: int + classification: ItemClassification + tags: FrozenSet[str] + + +class LocationData(NamedTuple): + name: str + label: str + parent_region: str + default_item: int + rom_address: int + flag: int + tags: FrozenSet[str] + + +class EventData(NamedTuple): + name: str + parent_region: str + + +class RegionData: + name: str + exits: List[str] + warps: List[str] + locations: List[str] + events: List[EventData] + + def __init__(self, name: str): + self.name = name + self.exits = [] + self.warps = [] + self.locations = [] + self.events = [] + + +class BaseStats(NamedTuple): + hp: int + attack: int + defense: int + speed: int + special_attack: int + special_defense: int + + +class LearnsetMove(NamedTuple): + level: int + move_id: int + + +class EvolutionMethodEnum(IntEnum): + LEVEL = 0 + LEVEL_ATK_LT_DEF = 1 + LEVEL_ATK_EQ_DEF = 2 + LEVEL_ATK_GT_DEF = 3 + LEVEL_SILCOON = 4 + LEVEL_CASCOON = 5 + LEVEL_NINJASK = 6 + LEVEL_SHEDINJA = 7 + ITEM = 8 + FRIENDSHIP = 9 + FRIENDSHIP_DAY = 10 + FRIENDSHIP_NIGHT = 11 + + +def _str_to_evolution_method(string: str) -> EvolutionMethodEnum: + if string == "LEVEL": + return EvolutionMethodEnum.LEVEL + if string == "LEVEL_ATK_LT_DEF": + return EvolutionMethodEnum.LEVEL_ATK_LT_DEF + if string == "LEVEL_ATK_EQ_DEF": + return EvolutionMethodEnum.LEVEL_ATK_EQ_DEF + if string == "LEVEL_ATK_GT_DEF": + return EvolutionMethodEnum.LEVEL_ATK_GT_DEF + if string == "LEVEL_SILCOON": + return EvolutionMethodEnum.LEVEL_SILCOON + if string == "LEVEL_CASCOON": + return EvolutionMethodEnum.LEVEL_CASCOON + if string == "LEVEL_NINJASK": + return EvolutionMethodEnum.LEVEL_NINJASK + if string == "LEVEL_SHEDINJA": + return EvolutionMethodEnum.LEVEL_SHEDINJA + if string == "FRIENDSHIP": + return EvolutionMethodEnum.FRIENDSHIP + if string == "FRIENDSHIP_DAY": + return EvolutionMethodEnum.FRIENDSHIP_DAY + if string == "FRIENDSHIP_NIGHT": + return EvolutionMethodEnum.FRIENDSHIP_NIGHT + + +class EvolutionData(NamedTuple): + method: EvolutionMethodEnum + param: int + species_id: int + + +class StaticEncounterData(NamedTuple): + species_id: int + rom_address: int + + +@dataclass +class SpeciesData: + name: str + label: str + species_id: int + base_stats: BaseStats + types: Tuple[int, int] + abilities: Tuple[int, int] + evolutions: List[EvolutionData] + pre_evolution: Optional[int] + catch_rate: int + learnset: List[LearnsetMove] + tm_hm_compatibility: int + learnset_rom_address: int + rom_address: int + + +class AbilityData(NamedTuple): + ability_id: int + label: str + + +class EncounterTableData(NamedTuple): + slots: List[int] + rom_address: int + + +@dataclass +class MapData: + name: str + land_encounters: Optional[EncounterTableData] + water_encounters: Optional[EncounterTableData] + fishing_encounters: Optional[EncounterTableData] + + +class TrainerPokemonDataTypeEnum(IntEnum): + NO_ITEM_DEFAULT_MOVES = 0 + ITEM_DEFAULT_MOVES = 1 + NO_ITEM_CUSTOM_MOVES = 2 + ITEM_CUSTOM_MOVES = 3 + + +def _str_to_pokemon_data_type(string: str) -> TrainerPokemonDataTypeEnum: + if string == "NO_ITEM_DEFAULT_MOVES": + return TrainerPokemonDataTypeEnum.NO_ITEM_DEFAULT_MOVES + if string == "ITEM_DEFAULT_MOVES": + return TrainerPokemonDataTypeEnum.ITEM_DEFAULT_MOVES + if string == "NO_ITEM_CUSTOM_MOVES": + return TrainerPokemonDataTypeEnum.NO_ITEM_CUSTOM_MOVES + if string == "ITEM_CUSTOM_MOVES": + return TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES + + +@dataclass +class TrainerPokemonData: + species_id: int + level: int + moves: Optional[Tuple[int, int, int, int]] + + +@dataclass +class TrainerPartyData: + pokemon: List[TrainerPokemonData] + pokemon_data_type: TrainerPokemonDataTypeEnum + rom_address: int + + +@dataclass +class TrainerData: + trainer_id: int + party: TrainerPartyData + rom_address: int + battle_script_rom_address: int + + +class PokemonEmeraldData: + starters: Tuple[int, int, int] + constants: Dict[str, int] + ram_addresses: Dict[str, int] + rom_addresses: Dict[str, int] + regions: Dict[str, RegionData] + locations: Dict[str, LocationData] + items: Dict[int, ItemData] + species: List[Optional[SpeciesData]] + static_encounters: List[StaticEncounterData] + tmhm_moves: List[int] + abilities: List[AbilityData] + maps: List[MapData] + warps: Dict[str, Warp] + warp_map: Dict[str, Optional[str]] + trainers: List[TrainerData] + + def __init__(self) -> None: + self.starters = (277, 280, 283) + self.constants = {} + self.ram_addresses = {} + self.rom_addresses = {} + self.regions = {} + self.locations = {} + self.items = {} + self.species = [] + self.static_encounters = [] + self.tmhm_moves = [] + self.abilities = [] + self.maps = [] + self.warps = {} + self.warp_map = {} + self.trainers = [] + + +def load_json_data(data_name: str) -> Union[List[Any], Dict[str, Any]]: + return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name).decode('utf-8-sig')) + + +data = PokemonEmeraldData() + +def create_data_copy() -> PokemonEmeraldData: + new_copy = PokemonEmeraldData() + new_copy.species = copy.deepcopy(data.species) + new_copy.tmhm_moves = copy.deepcopy(data.tmhm_moves) + new_copy.maps = copy.deepcopy(data.maps) + new_copy.static_encounters = copy.deepcopy(data.static_encounters) + new_copy.trainers = copy.deepcopy(data.trainers) + + +def _init() -> None: + extracted_data: Dict[str, Any] = load_json_data("extracted_data.json") + data.constants = extracted_data["constants"] + data.ram_addresses = extracted_data["misc_ram_addresses"] + data.rom_addresses = extracted_data["misc_rom_addresses"] + + location_attributes_json = load_json_data("locations.json") + + # Load/merge region json files + region_json_list = [] + for file in pkg_resources.resource_listdir(__name__, "data/regions"): + if not pkg_resources.resource_isdir(__name__, "data/regions/" + file): + region_json_list.append(load_json_data("regions/" + file)) + + regions_json = {} + for region_subset in region_json_list: + for region_name, region_json in region_subset.items(): + if region_name in regions_json: + raise AssertionError("Region [{region_name}] was defined multiple times") + regions_json[region_name] = region_json + + # Create region data + claimed_locations: Set[str] = set() + claimed_warps: Set[str] = set() + + data.regions = {} + for region_name, region_json in regions_json.items(): + new_region = RegionData(region_name) + + # Locations + for location_name in region_json["locations"]: + if location_name in claimed_locations: + raise AssertionError(f"Location [{location_name}] was claimed by multiple regions") + + location_json = extracted_data["locations"][location_name] + new_location = LocationData( + location_name, + location_attributes_json[location_name]["label"], + region_name, + location_json["default_item"], + location_json["rom_address"], + location_json["flag"], + frozenset(location_attributes_json[location_name]["tags"]) + ) + new_region.locations.append(location_name) + data.locations[location_name] = new_location + claimed_locations.add(location_name) + + new_region.locations.sort() + + # Events + for event in region_json["events"]: + new_region.events.append(EventData(event, region_name)) + + # Exits + for region_exit in region_json["exits"]: + new_region.exits.append(region_exit) + + # Warps + for encoded_warp in region_json["warps"]: + if encoded_warp in claimed_warps: + raise AssertionError(f"Warp [{encoded_warp}] was claimed by multiple regions") + new_region.warps.append(encoded_warp) + data.warps[encoded_warp] = Warp(encoded_warp, region_name) + claimed_warps.add(encoded_warp) + + new_region.warps.sort() + + data.regions[region_name] = new_region + + # Create item data + items_json = load_json_data("items.json") + + data.items = {} + for item_constant_name, attributes in items_json.items(): + item_classification = None + if attributes["classification"] == "PROGRESSION": + item_classification = ItemClassification.progression + elif attributes["classification"] == "USEFUL": + item_classification = ItemClassification.useful + elif attributes["classification"] == "FILLER": + item_classification = ItemClassification.filler + elif attributes["classification"] == "TRAP": + item_classification = ItemClassification.trap + else: + raise ValueError(f"Unknown classification {attributes['classification']} for item {item_constant_name}") + + data.items[data.constants[item_constant_name]] = ItemData( + attributes["label"], + data.constants[item_constant_name], + item_classification, + frozenset(attributes["tags"]) + ) + + # Create species data + + # Excludes extras like copies of Unown and special species values like SPECIES_EGG. + all_species: List[Tuple[str, str]] = [ + ("SPECIES_BULBASAUR", "Bulbasaur"), + ("SPECIES_IVYSAUR", "Ivysaur"), + ("SPECIES_VENUSAUR", "Venusaur"), + ("SPECIES_CHARMANDER", "Charmander"), + ("SPECIES_CHARMELEON", "Charmeleon"), + ("SPECIES_CHARIZARD", "Charizard"), + ("SPECIES_SQUIRTLE", "Squirtle"), + ("SPECIES_WARTORTLE", "Wartortle"), + ("SPECIES_BLASTOISE", "Blastoise"), + ("SPECIES_CATERPIE", "Caterpie"), + ("SPECIES_METAPOD", "Metapod"), + ("SPECIES_BUTTERFREE", "Butterfree"), + ("SPECIES_WEEDLE", "Weedle"), + ("SPECIES_KAKUNA", "Kakuna"), + ("SPECIES_BEEDRILL", "Beedrill"), + ("SPECIES_PIDGEY", "Pidgey"), + ("SPECIES_PIDGEOTTO", "Pidgeotto"), + ("SPECIES_PIDGEOT", "Pidgeot"), + ("SPECIES_RATTATA", "Rattata"), + ("SPECIES_RATICATE", "Raticate"), + ("SPECIES_SPEAROW", "Spearow"), + ("SPECIES_FEAROW", "Fearow"), + ("SPECIES_EKANS", "Ekans"), + ("SPECIES_ARBOK", "Arbok"), + ("SPECIES_PIKACHU", "Pikachu"), + ("SPECIES_RAICHU", "Raichu"), + ("SPECIES_SANDSHREW", "Sandshrew"), + ("SPECIES_SANDSLASH", "Sandslash"), + ("SPECIES_NIDORAN_F", "Nidoran Female"), + ("SPECIES_NIDORINA", "Nidorina"), + ("SPECIES_NIDOQUEEN", "Nidoqueen"), + ("SPECIES_NIDORAN_M", "Nidoran Male"), + ("SPECIES_NIDORINO", "Nidorino"), + ("SPECIES_NIDOKING", "Nidoking"), + ("SPECIES_CLEFAIRY", "Clefairy"), + ("SPECIES_CLEFABLE", "Clefable"), + ("SPECIES_VULPIX", "Vulpix"), + ("SPECIES_NINETALES", "Ninetales"), + ("SPECIES_JIGGLYPUFF", "Jigglypuff"), + ("SPECIES_WIGGLYTUFF", "Wigglytuff"), + ("SPECIES_ZUBAT", "Zubat"), + ("SPECIES_GOLBAT", "Golbat"), + ("SPECIES_ODDISH", "Oddish"), + ("SPECIES_GLOOM", "Gloom"), + ("SPECIES_VILEPLUME", "Vileplume"), + ("SPECIES_PARAS", "Paras"), + ("SPECIES_PARASECT", "Parasect"), + ("SPECIES_VENONAT", "Venonat"), + ("SPECIES_VENOMOTH", "Venomoth"), + ("SPECIES_DIGLETT", "Diglett"), + ("SPECIES_DUGTRIO", "Dugtrio"), + ("SPECIES_MEOWTH", "Meowth"), + ("SPECIES_PERSIAN", "Persian"), + ("SPECIES_PSYDUCK", "Psyduck"), + ("SPECIES_GOLDUCK", "Golduck"), + ("SPECIES_MANKEY", "Mankey"), + ("SPECIES_PRIMEAPE", "Primeape"), + ("SPECIES_GROWLITHE", "Growlithe"), + ("SPECIES_ARCANINE", "Arcanine"), + ("SPECIES_POLIWAG", "Poliwag"), + ("SPECIES_POLIWHIRL", "Poliwhirl"), + ("SPECIES_POLIWRATH", "Poliwrath"), + ("SPECIES_ABRA", "Abra"), + ("SPECIES_KADABRA", "Kadabra"), + ("SPECIES_ALAKAZAM", "Alakazam"), + ("SPECIES_MACHOP", "Machop"), + ("SPECIES_MACHOKE", "Machoke"), + ("SPECIES_MACHAMP", "Machamp"), + ("SPECIES_BELLSPROUT", "Bellsprout"), + ("SPECIES_WEEPINBELL", "Weepinbell"), + ("SPECIES_VICTREEBEL", "Victreebel"), + ("SPECIES_TENTACOOL", "Tentacool"), + ("SPECIES_TENTACRUEL", "Tentacruel"), + ("SPECIES_GEODUDE", "Geodude"), + ("SPECIES_GRAVELER", "Graveler"), + ("SPECIES_GOLEM", "Golem"), + ("SPECIES_PONYTA", "Ponyta"), + ("SPECIES_RAPIDASH", "Rapidash"), + ("SPECIES_SLOWPOKE", "Slowpoke"), + ("SPECIES_SLOWBRO", "Slowbro"), + ("SPECIES_MAGNEMITE", "Magnemite"), + ("SPECIES_MAGNETON", "Magneton"), + ("SPECIES_FARFETCHD", "Farfetch'd"), + ("SPECIES_DODUO", "Doduo"), + ("SPECIES_DODRIO", "Dodrio"), + ("SPECIES_SEEL", "Seel"), + ("SPECIES_DEWGONG", "Dewgong"), + ("SPECIES_GRIMER", "Grimer"), + ("SPECIES_MUK", "Muk"), + ("SPECIES_SHELLDER", "Shellder"), + ("SPECIES_CLOYSTER", "Cloyster"), + ("SPECIES_GASTLY", "Gastly"), + ("SPECIES_HAUNTER", "Haunter"), + ("SPECIES_GENGAR", "Gengar"), + ("SPECIES_ONIX", "Onix"), + ("SPECIES_DROWZEE", "Drowzee"), + ("SPECIES_HYPNO", "Hypno"), + ("SPECIES_KRABBY", "Krabby"), + ("SPECIES_KINGLER", "Kingler"), + ("SPECIES_VOLTORB", "Voltorb"), + ("SPECIES_ELECTRODE", "Electrode"), + ("SPECIES_EXEGGCUTE", "Exeggcute"), + ("SPECIES_EXEGGUTOR", "Exeggutor"), + ("SPECIES_CUBONE", "Cubone"), + ("SPECIES_MAROWAK", "Marowak"), + ("SPECIES_HITMONLEE", "Hitmonlee"), + ("SPECIES_HITMONCHAN", "Hitmonchan"), + ("SPECIES_LICKITUNG", "Lickitung"), + ("SPECIES_KOFFING", "Koffing"), + ("SPECIES_WEEZING", "Weezing"), + ("SPECIES_RHYHORN", "Rhyhorn"), + ("SPECIES_RHYDON", "Rhydon"), + ("SPECIES_CHANSEY", "Chansey"), + ("SPECIES_TANGELA", "Tangela"), + ("SPECIES_KANGASKHAN", "Kangaskhan"), + ("SPECIES_HORSEA", "Horsea"), + ("SPECIES_SEADRA", "Seadra"), + ("SPECIES_GOLDEEN", "Goldeen"), + ("SPECIES_SEAKING", "Seaking"), + ("SPECIES_STARYU", "Staryu"), + ("SPECIES_STARMIE", "Starmie"), + ("SPECIES_MR_MIME", "Mr. Mime"), + ("SPECIES_SCYTHER", "Scyther"), + ("SPECIES_JYNX", "Jynx"), + ("SPECIES_ELECTABUZZ", "Electabuzz"), + ("SPECIES_MAGMAR", "Magmar"), + ("SPECIES_PINSIR", "Pinsir"), + ("SPECIES_TAUROS", "Tauros"), + ("SPECIES_MAGIKARP", "Magikarp"), + ("SPECIES_GYARADOS", "Gyarados"), + ("SPECIES_LAPRAS", "Lapras"), + ("SPECIES_DITTO", "Ditto"), + ("SPECIES_EEVEE", "Eevee"), + ("SPECIES_VAPOREON", "Vaporeon"), + ("SPECIES_JOLTEON", "Jolteon"), + ("SPECIES_FLAREON", "Flareon"), + ("SPECIES_PORYGON", "Porygon"), + ("SPECIES_OMANYTE", "Omanyte"), + ("SPECIES_OMASTAR", "Omastar"), + ("SPECIES_KABUTO", "Kabuto"), + ("SPECIES_KABUTOPS", "Kabutops"), + ("SPECIES_AERODACTYL", "Aerodactyl"), + ("SPECIES_SNORLAX", "Snorlax"), + ("SPECIES_ARTICUNO", "Articuno"), + ("SPECIES_ZAPDOS", "Zapdos"), + ("SPECIES_MOLTRES", "Moltres"), + ("SPECIES_DRATINI", "Dratini"), + ("SPECIES_DRAGONAIR", "Dragonair"), + ("SPECIES_DRAGONITE", "Dragonite"), + ("SPECIES_MEWTWO", "Mewtwo"), + ("SPECIES_MEW", "Mew"), + ("SPECIES_CHIKORITA", "Chikorita"), + ("SPECIES_BAYLEEF", "Bayleaf"), + ("SPECIES_MEGANIUM", "Meganium"), + ("SPECIES_CYNDAQUIL", "Cindaquil"), + ("SPECIES_QUILAVA", "Quilava"), + ("SPECIES_TYPHLOSION", "Typhlosion"), + ("SPECIES_TOTODILE", "Totodile"), + ("SPECIES_CROCONAW", "Croconaw"), + ("SPECIES_FERALIGATR", "Feraligatr"), + ("SPECIES_SENTRET", "Sentret"), + ("SPECIES_FURRET", "Furret"), + ("SPECIES_HOOTHOOT", "Hoothoot"), + ("SPECIES_NOCTOWL", "Noctowl"), + ("SPECIES_LEDYBA", "Ledyba"), + ("SPECIES_LEDIAN", "Ledian"), + ("SPECIES_SPINARAK", "Spinarak"), + ("SPECIES_ARIADOS", "Ariados"), + ("SPECIES_CROBAT", "Crobat"), + ("SPECIES_CHINCHOU", "Chinchou"), + ("SPECIES_LANTURN", "Lanturn"), + ("SPECIES_PICHU", "Pichu"), + ("SPECIES_CLEFFA", "Cleffa"), + ("SPECIES_IGGLYBUFF", "Igglybuff"), + ("SPECIES_TOGEPI", "Togepi"), + ("SPECIES_TOGETIC", "Togetic"), + ("SPECIES_NATU", "Natu"), + ("SPECIES_XATU", "Xatu"), + ("SPECIES_MAREEP", "Mareep"), + ("SPECIES_FLAAFFY", "Flaafy"), + ("SPECIES_AMPHAROS", "Ampharos"), + ("SPECIES_BELLOSSOM", "Bellossom"), + ("SPECIES_MARILL", "Marill"), + ("SPECIES_AZUMARILL", "Azumarill"), + ("SPECIES_SUDOWOODO", "Sudowoodo"), + ("SPECIES_POLITOED", "Politoed"), + ("SPECIES_HOPPIP", "Hoppip"), + ("SPECIES_SKIPLOOM", "Skiploom"), + ("SPECIES_JUMPLUFF", "Jumpluff"), + ("SPECIES_AIPOM", "Aipom"), + ("SPECIES_SUNKERN", "Sunkern"), + ("SPECIES_SUNFLORA", "Sunflora"), + ("SPECIES_YANMA", "Yanma"), + ("SPECIES_WOOPER", "Wooper"), + ("SPECIES_QUAGSIRE", "Quagsire"), + ("SPECIES_ESPEON", "Espeon"), + ("SPECIES_UMBREON", "Umbreon"), + ("SPECIES_MURKROW", "Murkrow"), + ("SPECIES_SLOWKING", "Slowking"), + ("SPECIES_MISDREAVUS", "Misdreavus"), + ("SPECIES_UNOWN", "Unown"), + ("SPECIES_WOBBUFFET", "Wobbuffet"), + ("SPECIES_GIRAFARIG", "Girafarig"), + ("SPECIES_PINECO", "Pineco"), + ("SPECIES_FORRETRESS", "Forretress"), + ("SPECIES_DUNSPARCE", "Dunsparce"), + ("SPECIES_GLIGAR", "Gligar"), + ("SPECIES_STEELIX", "Steelix"), + ("SPECIES_SNUBBULL", "Snubbull"), + ("SPECIES_GRANBULL", "Granbull"), + ("SPECIES_QWILFISH", "Qwilfish"), + ("SPECIES_SCIZOR", "Scizor"), + ("SPECIES_SHUCKLE", "Shuckle"), + ("SPECIES_HERACROSS", "Heracross"), + ("SPECIES_SNEASEL", "Sneasel"), + ("SPECIES_TEDDIURSA", "Teddiursa"), + ("SPECIES_URSARING", "Ursaring"), + ("SPECIES_SLUGMA", "Slugma"), + ("SPECIES_MAGCARGO", "Magcargo"), + ("SPECIES_SWINUB", "Swinub"), + ("SPECIES_PILOSWINE", "Piloswine"), + ("SPECIES_CORSOLA", "Corsola"), + ("SPECIES_REMORAID", "Remoraid"), + ("SPECIES_OCTILLERY", "Octillery"), + ("SPECIES_DELIBIRD", "Delibird"), + ("SPECIES_MANTINE", "Mantine"), + ("SPECIES_SKARMORY", "Skarmory"), + ("SPECIES_HOUNDOUR", "Houndour"), + ("SPECIES_HOUNDOOM", "Houndoom"), + ("SPECIES_KINGDRA", "Kingdra"), + ("SPECIES_PHANPY", "Phanpy"), + ("SPECIES_DONPHAN", "Donphan"), + ("SPECIES_PORYGON2", "Porygon2"), + ("SPECIES_STANTLER", "Stantler"), + ("SPECIES_SMEARGLE", "Smeargle"), + ("SPECIES_TYROGUE", "Tyrogue"), + ("SPECIES_HITMONTOP", "Hitmontop"), + ("SPECIES_SMOOCHUM", "Smoochum"), + ("SPECIES_ELEKID", "Elekid"), + ("SPECIES_MAGBY", "Magby"), + ("SPECIES_MILTANK", "Miltank"), + ("SPECIES_BLISSEY", "Blissey"), + ("SPECIES_RAIKOU", "Raikou"), + ("SPECIES_ENTEI", "Entei"), + ("SPECIES_SUICUNE", "Suicune"), + ("SPECIES_LARVITAR", "Larvitar"), + ("SPECIES_PUPITAR", "Pupitar"), + ("SPECIES_TYRANITAR", "Tyranitar"), + ("SPECIES_LUGIA", "Lugia"), + ("SPECIES_HO_OH", "Ho-oh"), + ("SPECIES_CELEBI", "Celebi"), + ("SPECIES_TREECKO", "Treecko"), + ("SPECIES_GROVYLE", "Grovyle"), + ("SPECIES_SCEPTILE", "Sceptile"), + ("SPECIES_TORCHIC", "Torchic"), + ("SPECIES_COMBUSKEN", "Combusken"), + ("SPECIES_BLAZIKEN", "Blaziken"), + ("SPECIES_MUDKIP", "Mudkip"), + ("SPECIES_MARSHTOMP", "Marshtomp"), + ("SPECIES_SWAMPERT", "Swampert"), + ("SPECIES_POOCHYENA", "Poochyena"), + ("SPECIES_MIGHTYENA", "Mightyena"), + ("SPECIES_ZIGZAGOON", "Zigzagoon"), + ("SPECIES_LINOONE", "Linoon"), + ("SPECIES_WURMPLE", "Wurmple"), + ("SPECIES_SILCOON", "Silcoon"), + ("SPECIES_BEAUTIFLY", "Beautifly"), + ("SPECIES_CASCOON", "Cascoon"), + ("SPECIES_DUSTOX", "Dustox"), + ("SPECIES_LOTAD", "Lotad"), + ("SPECIES_LOMBRE", "Lombre"), + ("SPECIES_LUDICOLO", "Ludicolo"), + ("SPECIES_SEEDOT", "Seedot"), + ("SPECIES_NUZLEAF", "Nuzleaf"), + ("SPECIES_SHIFTRY", "Shiftry"), + ("SPECIES_NINCADA", "Nincada"), + ("SPECIES_NINJASK", "Ninjask"), + ("SPECIES_SHEDINJA", "Shedinja"), + ("SPECIES_TAILLOW", "Taillow"), + ("SPECIES_SWELLOW", "Swellow"), + ("SPECIES_SHROOMISH", "Shroomish"), + ("SPECIES_BRELOOM", "Breloom"), + ("SPECIES_SPINDA", "Spinda"), + ("SPECIES_WINGULL", "Wingull"), + ("SPECIES_PELIPPER", "Pelipper"), + ("SPECIES_SURSKIT", "Surskit"), + ("SPECIES_MASQUERAIN", "Masquerain"), + ("SPECIES_WAILMER", "Wailmer"), + ("SPECIES_WAILORD", "Wailord"), + ("SPECIES_SKITTY", "Skitty"), + ("SPECIES_DELCATTY", "Delcatty"), + ("SPECIES_KECLEON", "Kecleon"), + ("SPECIES_BALTOY", "Baltoy"), + ("SPECIES_CLAYDOL", "Claydol"), + ("SPECIES_NOSEPASS", "Nosepass"), + ("SPECIES_TORKOAL", "Torkoal"), + ("SPECIES_SABLEYE", "Sableye"), + ("SPECIES_BARBOACH", "Barboach"), + ("SPECIES_WHISCASH", "Whiscash"), + ("SPECIES_LUVDISC", "Luvdisc"), + ("SPECIES_CORPHISH", "Corphish"), + ("SPECIES_CRAWDAUNT", "Crawdaunt"), + ("SPECIES_FEEBAS", "Feebas"), + ("SPECIES_MILOTIC", "Milotic"), + ("SPECIES_CARVANHA", "Carvanha"), + ("SPECIES_SHARPEDO", "Sharpedo"), + ("SPECIES_TRAPINCH", "Trapinch"), + ("SPECIES_VIBRAVA", "Vibrava"), + ("SPECIES_FLYGON", "Flygon"), + ("SPECIES_MAKUHITA", "Makuhita"), + ("SPECIES_HARIYAMA", "Hariyama"), + ("SPECIES_ELECTRIKE", "Electrike"), + ("SPECIES_MANECTRIC", "Manectric"), + ("SPECIES_NUMEL", "Numel"), + ("SPECIES_CAMERUPT", "Camerupt"), + ("SPECIES_SPHEAL", "Spheal"), + ("SPECIES_SEALEO", "Sealeo"), + ("SPECIES_WALREIN", "Walrein"), + ("SPECIES_CACNEA", "Cacnea"), + ("SPECIES_CACTURNE", "Cacturne"), + ("SPECIES_SNORUNT", "Snorunt"), + ("SPECIES_GLALIE", "Glalie"), + ("SPECIES_LUNATONE", "Lunatone"), + ("SPECIES_SOLROCK", "Solrock"), + ("SPECIES_AZURILL", "Azurill"), + ("SPECIES_SPOINK", "Spoink"), + ("SPECIES_GRUMPIG", "Grumpig"), + ("SPECIES_PLUSLE", "Plusle"), + ("SPECIES_MINUN", "Minun"), + ("SPECIES_MAWILE", "Mawile"), + ("SPECIES_MEDITITE", "Meditite"), + ("SPECIES_MEDICHAM", "Medicham"), + ("SPECIES_SWABLU", "Swablu"), + ("SPECIES_ALTARIA", "Altaria"), + ("SPECIES_WYNAUT", "Wynaut"), + ("SPECIES_DUSKULL", "Duskull"), + ("SPECIES_DUSCLOPS", "Dusclops"), + ("SPECIES_ROSELIA", "Roselia"), + ("SPECIES_SLAKOTH", "Slakoth"), + ("SPECIES_VIGOROTH", "Vigoroth"), + ("SPECIES_SLAKING", "Slaking"), + ("SPECIES_GULPIN", "Gulpin"), + ("SPECIES_SWALOT", "Swalot"), + ("SPECIES_TROPIUS", "Tropius"), + ("SPECIES_WHISMUR", "Whismur"), + ("SPECIES_LOUDRED", "Loudred"), + ("SPECIES_EXPLOUD", "Exploud"), + ("SPECIES_CLAMPERL", "Clamperl"), + ("SPECIES_HUNTAIL", "Huntail"), + ("SPECIES_GOREBYSS", "Gorebyss"), + ("SPECIES_ABSOL", "Absol"), + ("SPECIES_SHUPPET", "Shuppet"), + ("SPECIES_BANETTE", "Banette"), + ("SPECIES_SEVIPER", "Seviper"), + ("SPECIES_ZANGOOSE", "Zangoose"), + ("SPECIES_RELICANTH", "Relicanth"), + ("SPECIES_ARON", "Aron"), + ("SPECIES_LAIRON", "Lairon"), + ("SPECIES_AGGRON", "Aggron"), + ("SPECIES_CASTFORM", "Castform"), + ("SPECIES_VOLBEAT", "Volbeat"), + ("SPECIES_ILLUMISE", "Illumise"), + ("SPECIES_LILEEP", "Lileep"), + ("SPECIES_CRADILY", "Cradily"), + ("SPECIES_ANORITH", "Anorith"), + ("SPECIES_ARMALDO", "Armaldo"), + ("SPECIES_RALTS", "Ralts"), + ("SPECIES_KIRLIA", "Kirlia"), + ("SPECIES_GARDEVOIR", "Gardevoir"), + ("SPECIES_BAGON", "Bagon"), + ("SPECIES_SHELGON", "Shelgon"), + ("SPECIES_SALAMENCE", "Salamence"), + ("SPECIES_BELDUM", "Beldum"), + ("SPECIES_METANG", "Metang"), + ("SPECIES_METAGROSS", "Metagross"), + ("SPECIES_REGIROCK", "Regirock"), + ("SPECIES_REGICE", "Regice"), + ("SPECIES_REGISTEEL", "Registeel"), + ("SPECIES_KYOGRE", "Kyogre"), + ("SPECIES_GROUDON", "Groudon"), + ("SPECIES_RAYQUAZA", "Rayquaza"), + ("SPECIES_LATIAS", "Latias"), + ("SPECIES_LATIOS", "Latios"), + ("SPECIES_JIRACHI", "Jirachi"), + ("SPECIES_DEOXYS", "Deoxys"), + ("SPECIES_CHIMECHO", "Chimecho") + ] + + species_list: List[SpeciesData] = [] + max_species_id = 0 + for species_name, species_label in all_species: + species_id = data.constants[species_name] + max_species_id = max(species_id, max_species_id) + species_data = extracted_data["species"][species_id] + + learnset = [LearnsetMove(item["level"], item["move_id"]) for item in species_data["learnset"]["moves"]] + + species_list.append(SpeciesData( + species_name, + species_label, + species_id, + BaseStats( + species_data["base_stats"][0], + species_data["base_stats"][1], + species_data["base_stats"][2], + species_data["base_stats"][3], + species_data["base_stats"][4], + species_data["base_stats"][5] + ), + (species_data["types"][0], species_data["types"][1]), + (species_data["abilities"][0], species_data["abilities"][1]), + [EvolutionData( + _str_to_evolution_method(evolution_json["method"]), + evolution_json["param"], + evolution_json["species"], + ) for evolution_json in species_data["evolutions"]], + None, + species_data["catch_rate"], + learnset, + int(species_data["tmhm_learnset"], 16), + species_data["learnset"]["rom_address"], + species_data["rom_address"] + )) + + data.species = [None for i in range(max_species_id + 1)] + + for species_data in species_list: + data.species[species_data.species_id] = species_data + + for species in data.species: + if species is not None: + for evolution in species.evolutions: + data.species[evolution.species_id].pre_evolution = species.species_id + + # Create static encounter data + for static_encounter_json in extracted_data["static_encounters"]: + data.static_encounters.append(StaticEncounterData( + static_encounter_json["species"], + static_encounter_json["rom_address"] + )) + + # TM moves + data.tmhm_moves = extracted_data["tmhm_moves"] + + # Create ability data + data.abilities = [AbilityData(data.constants[ability_data[0]], ability_data[1]) for ability_data in [ + ("ABILITY_STENCH", "Stench"), + ("ABILITY_DRIZZLE", "Drizzle"), + ("ABILITY_SPEED_BOOST", "Speed Boost"), + ("ABILITY_BATTLE_ARMOR", "Battle Armor"), + ("ABILITY_STURDY", "Sturdy"), + ("ABILITY_DAMP", "Damp"), + ("ABILITY_LIMBER", "Limber"), + ("ABILITY_SAND_VEIL", "Sand Veil"), + ("ABILITY_STATIC", "Static"), + ("ABILITY_VOLT_ABSORB", "Volt Absorb"), + ("ABILITY_WATER_ABSORB", "Water Absorb"), + ("ABILITY_OBLIVIOUS", "Oblivious"), + ("ABILITY_CLOUD_NINE", "Cloud Nine"), + ("ABILITY_COMPOUND_EYES", "Compound Eyes"), + ("ABILITY_INSOMNIA", "Insomnia"), + ("ABILITY_COLOR_CHANGE", "Color Change"), + ("ABILITY_IMMUNITY", "Immunity"), + ("ABILITY_FLASH_FIRE", "Flash Fire"), + ("ABILITY_SHIELD_DUST", "Shield Dust"), + ("ABILITY_OWN_TEMPO", "Own Tempo"), + ("ABILITY_SUCTION_CUPS", "Suction Cups"), + ("ABILITY_INTIMIDATE", "Intimidate"), + ("ABILITY_SHADOW_TAG", "Shadow Tag"), + ("ABILITY_ROUGH_SKIN", "Rough Skin"), + ("ABILITY_WONDER_GUARD", "Wonder Guard"), + ("ABILITY_LEVITATE", "Levitate"), + ("ABILITY_EFFECT_SPORE", "Effect Spore"), + ("ABILITY_SYNCHRONIZE", "Synchronize"), + ("ABILITY_CLEAR_BODY", "Clear Body"), + ("ABILITY_NATURAL_CURE", "Natural Cure"), + ("ABILITY_LIGHTNING_ROD", "Lightning Rod"), + ("ABILITY_SERENE_GRACE", "Serene Grace"), + ("ABILITY_SWIFT_SWIM", "Swift Swim"), + ("ABILITY_CHLOROPHYLL", "Chlorophyll"), + ("ABILITY_ILLUMINATE", "Illuminate"), + ("ABILITY_TRACE", "Trace"), + ("ABILITY_HUGE_POWER", "Huge Power"), + ("ABILITY_POISON_POINT", "Poison Point"), + ("ABILITY_INNER_FOCUS", "Inner Focus"), + ("ABILITY_MAGMA_ARMOR", "Magma Armor"), + ("ABILITY_WATER_VEIL", "Water Veil"), + ("ABILITY_MAGNET_PULL", "Magnet Pull"), + ("ABILITY_SOUNDPROOF", "Soundproof"), + ("ABILITY_RAIN_DISH", "Rain Dish"), + ("ABILITY_SAND_STREAM", "Sand Stream"), + ("ABILITY_PRESSURE", "Pressure"), + ("ABILITY_THICK_FAT", "Thick Fat"), + ("ABILITY_EARLY_BIRD", "Early Bird"), + ("ABILITY_FLAME_BODY", "Flame Body"), + ("ABILITY_RUN_AWAY", "Run Away"), + ("ABILITY_KEEN_EYE", "Keen Eye"), + ("ABILITY_HYPER_CUTTER", "Hyper Cutter"), + ("ABILITY_PICKUP", "Pickup"), + ("ABILITY_TRUANT", "Truant"), + ("ABILITY_HUSTLE", "Hustle"), + ("ABILITY_CUTE_CHARM", "Cute Charm"), + ("ABILITY_PLUS", "Plus"), + ("ABILITY_MINUS", "Minus"), + ("ABILITY_FORECAST", "Forecast"), + ("ABILITY_STICKY_HOLD", "Sticky Hold"), + ("ABILITY_SHED_SKIN", "Shed Skin"), + ("ABILITY_GUTS", "Guts"), + ("ABILITY_MARVEL_SCALE", "Marvel Scale"), + ("ABILITY_LIQUID_OOZE", "Liquid Ooze"), + ("ABILITY_OVERGROW", "Overgrow"), + ("ABILITY_BLAZE", "Blaze"), + ("ABILITY_TORRENT", "Torrent"), + ("ABILITY_SWARM", "Swarm"), + ("ABILITY_ROCK_HEAD", "Rock Head"), + ("ABILITY_DROUGHT", "Drought"), + ("ABILITY_ARENA_TRAP", "Arena Trap"), + ("ABILITY_VITAL_SPIRIT", "Vital Spirit"), + ("ABILITY_WHITE_SMOKE", "White Smoke"), + ("ABILITY_PURE_POWER", "Pure Power"), + ("ABILITY_SHELL_ARMOR", "Shell Armor"), + ("ABILITY_CACOPHONY", "Cacophony"), + ("ABILITY_AIR_LOCK", "Air Lock") + ]] + + # Create map data + for map_name, map_json in extracted_data["maps"].items(): + land_encounters = None + water_encounters = None + fishing_encounters = None + + if map_json["land_encounters"] is not None: + land_encounters = EncounterTableData( + map_json["land_encounters"]["encounter_slots"], + map_json["land_encounters"]["rom_address"] + ) + if map_json["water_encounters"] is not None: + water_encounters = EncounterTableData( + map_json["water_encounters"]["encounter_slots"], + map_json["water_encounters"]["rom_address"] + ) + if map_json["fishing_encounters"] is not None: + fishing_encounters = EncounterTableData( + map_json["fishing_encounters"]["encounter_slots"], + map_json["fishing_encounters"]["rom_address"] + ) + + data.maps.append(MapData( + map_name, + land_encounters, + water_encounters, + fishing_encounters + )) + + data.maps.sort(key=lambda map: map.name) + + # Create warp map + for warp, destination in extracted_data["warps"].items(): + data.warp_map[warp] = None if destination == "" else destination + + if encoded_warp not in data.warp_map: + data.warp_map[encoded_warp] = None + + # Create trainer data + for i, trainer_json in enumerate(extracted_data["trainers"]): + party_json = trainer_json["party"] + pokemon_data_type = _str_to_pokemon_data_type(trainer_json["pokemon_data_type"]) + data.trainers.append(TrainerData( + i, + TrainerPartyData( + [TrainerPokemonData( + p["species"], + p["level"], + (p["moves"][0], p["moves"][1], p["moves"][2], p["moves"][3]) + ) for p in party_json], + pokemon_data_type, + trainer_json["party_rom_address"] + ), + trainer_json["rom_address"], + trainer_json["battle_script_rom_address"] + )) + + +_init() diff --git a/worlds/pokemon_emerald/data/README.md b/worlds/pokemon_emerald/data/README.md new file mode 100644 index 000000000000..a7c5d3f2932d --- /dev/null +++ b/worlds/pokemon_emerald/data/README.md @@ -0,0 +1,99 @@ +## `regions/` + +These define regions, connections, and where locations are. If you know what you're doing, it should be pretty clear how +this works by taking a quick look through the files. The rest of this section is pretty verbose to cover everything. Not +to say you shouldn't read it, but the tl;dr is: + +- Every map, even trivial ones, gets a region definition, and they cannot be coalesced (because of warp rando) +- Stick to the naming convention for regions and events (look at Route 103 and Petalburg City for guidance) +- Locations and warps can only be claimed by one region +- Events are declared here + +A `Map`, which you will see referenced in `parent_map` attribute in the region JSON, is an id from the source code. +`Map`s are sets of tiles, encounters, warps, events, and so on. Route 103, Littleroot Town, the Oldale Town Mart, the +second floor of Devon Corp, and each level of Victory Road are all examples of `Map`s. You transition between `Map`s by +stepping on a warp (warp pads, doorways, etc...) or walking over a border between `Map`s in the overworld. Some warps +don't go to a different `Map`. + +Regions usually describe physical areas which are subsets of a `Map`. Every `Map` must have one or more defined regions. +A region should not contain area from more than one `Map`. We'll need to draw those lines now even when there is no +logical boundary (like between two the first and second floors of your rival's house), for warp rando. + +Most `Map`s have been split into multiple regions. In the example below, `MAP_ROUTE103` was split into +`REGION_ROUTE_103/WEST`, `REGION_ROUTE_103/WATER`, and `REGION_ROUTE_103/EAST` (this document may be out of date; the +example is demonstrative). Keeping the name consistent with the `Map` name and adding a label suffix for the subarea +makes it clearer where we are in the world and where within a `Map` we're describing. + +Every region (except `Menu`) is configured here. All files in this directory are combined with each other at runtime, +and are only split and ordered for organization. Regions defined in `data/regions/unused` are entirely unused because +they're not yet reachable in the randomizer. They're there for future reference in case we want to pull those maps in +later. Any locations or warps in here should be ignored. Data for a single region looks like this: + +```json +"REGION_ROUTE103/EAST": { + "parent_map": "MAP_ROUTE103", + "locations": [ + "ITEM_ROUTE_103_GUARD_SPEC", + "ITEM_ROUTE_103_PP_UP" + ], + "events": [], + "exits": [ + "REGION_ROUTE103/WATER", + "REGION_ROUTE110/MAIN" + ], + "warps": [ + "MAP_ROUTE103:0/MAP_ALTERING_CAVE:0" + ] +} +``` + +- `[key]`: The name of the object, in this case `REGION_ROUTE103/EAST`, should be the value of `parent_map` where the +`MAP` prefix is replaced with `REGION`. Then there should be a following `/` and a label describing this specific region +within the `Map`. This is not enforced or required by the code, but it makes things much more clear. +- `parent_map`: The name of the `Map` this region exists under. It can relate this region to information like encounter +tables. +- `locations`: Locations contained within this region. This can be anything from an item on the ground to a badge to a +gift from an NPC. Locations themselves are defined in `data/extracted_data.json`, and the names used here should come +directly from it. +- `events`: Events that can be completed in this region. Defeating a gym leader or Aqua/Magma team leader, for example, +can trigger story progression and unblock roads and buildings. Events are defined here and nowhere else, and access +rules are set in `rules.py`. +- `exits`: Names of regions that can be directly accessed from this one. Most often regions within the same `Map`, +neighboring maps in the overworld, or transitions from using HM08 Dive. Most connections between maps/regions come from +warps. Any region in this list should be defined somewhere in `data/regions`. +- `warps`: Warp events contained within this region. Warps are defined in `data/extracted_data.json`, and must exist +there to be referenced here. More on warps in [../README.md](../README.md). + +Think of this data as defining which regions are "claiming" a given location, event, or warp. No more than one region +may claim ownership of a location. Even if some "thing" may happen in two different regions and set the same flag, they +should be defined as two different events and anything conditional on said "thing" happening can check whether either of +the two events is accessible. (e.g. Interacting with the Poke Ball in your rival's room and going back downstairs will +both trigger a conversation with them which enables you to rescue Professor Birch. It's the same "thing" on two +different `Map`s.) + +Conceptually, you shouldn't have to "add" any new regions. You should only have to "split" existing regions. When you +split a region, make sure to correctly reassign `locations`, `events`, `exits`, and `warps` according to which new +region they now exist in. Make sure to define new `exits` to link the new regions to each other if applicable. And +especially remember to rename incoming `exits` defined in other regions which are still pointing to the pre-split +region. `sanity_check.py` should catch you if there are other regions that point to a region that no longer exists, but +if one of your newly-split regions still has the same name as the original, it won't be detected and you may find that +things aren't connected correctly. + +## `extracted_data.json` + +DO NOT TOUCH + +Contains data automatically pulled from the base rom and its source code when it is built. There should be no reason to +manually modify it. Data from this file is piped through `data.py` to create a data object that's more useful and +complete. + +## `items.json` + +A map from items as defined in the `constants` in `extracted_data.json` to useful info like a human-friendly label, the +type of progression it enables, and tags to associate. There are many unused items and extra helper constants in +`extracted_data.json`, so this file contains an exhaustive list of items which can actually be found in the modded game. + +## `locations.json` + +Similar to `items.json`, this associates locations with human-friendly labels and tags that are used for filtering. Any +locations claimed by any region need an entry here. diff --git a/worlds/pokemon_emerald/data/base_patch.bsdiff4 b/worlds/pokemon_emerald/data/base_patch.bsdiff4 new file mode 100644 index 0000000000000000000000000000000000000000..c1843904a9caa81d52b766eb7056bf6e1bbc6546 GIT binary patch literal 209743 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%0DUCS6yUV%C+ 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

9ejY2o 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

Player Options

template file for this game.

-


- -

+
+
+ + +
+
+ + +
+ +

Game Options

diff --git a/docs/world api.md b/docs/world api.md index 67a44c0625d0..4008c9c4dddf 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -73,6 +73,53 @@ for your world specifically on the webhost: `game_info_languages` (optional) List of strings for defining the existing gameinfo pages your game supports. The documents must be prefixed with the same string as defined here. Default already has 'en'. +`options_presets` (optional) A `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values +are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names of +the options and the values are the values to be set for that option. These presets will be available for users to select from on the game's options page. + +Note: The values must be a non-aliased value for the option type and can only include the following option types: + + - If you have a `Range`/`SpecialRange` option, the value should be an `int` between the `range_start` and `range_end` + values. + - If you have a `SpecialRange` option, the value can alternatively be a `str` that is one of the + `special_range_names` keys. + - If you have a `Choice` option, the value should be a `str` that is one of the `option_` values. + - If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`. + - `random` is also a valid value for any of these option types. + +`OptionDict`, `OptionList`, `OptionSet`, `FreeText`, or custom `Option`-derived classes are not supported for presets on the webhost at this time. + +Here is an example of a defined preset: +```python +# presets.py +options_presets = { + "Limited Potential": { + "progression_balancing": 0, + "fairy_chests_per_zone": 2, + "starting_class": "random", + "chests_per_zone": 30, + "vendors": "normal", + "architect": "disabled", + "gold_gain_multiplier": "half", + "number_of_children": 2, + "free_diary_on_generation": False, + "health_pool": 10, + "mana_pool": 10, + "attack_pool": 10, + "magic_damage_pool": 10, + "armor_pool": 5, + "equip_pool": 10, + "crit_chance_pool": 5, + "crit_damage_pool": 5, + } +} + +# __init__.py +class RLWeb(WebWorld): + options_presets = options_presets + # ... +``` + ### MultiWorld Object The `MultiWorld` object references the whole multiworld (all items and locations diff --git a/test/webhost/test_option_presets.py b/test/webhost/test_option_presets.py new file mode 100644 index 000000000000..8c6ebea2088f --- /dev/null +++ b/test/webhost/test_option_presets.py @@ -0,0 +1,63 @@ +import unittest + +from worlds import AutoWorldRegister +from Options import Choice, SpecialRange, Toggle, Range + + +class TestOptionPresets(unittest.TestCase): + def test_option_presets_have_valid_options(self): + """Test that all predefined option presets are valid options.""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + presets = world_type.web.options_presets + for preset_name, preset in presets.items(): + for option_name, option_value in preset.items(): + with self.subTest(game=game_name, preset=preset_name, option=option_name): + try: + option = world_type.options_dataclass.type_hints[option_name].from_any(option_value) + supported_types = [Choice, Toggle, Range, SpecialRange] + if not any([issubclass(option.__class__, t) for t in supported_types]): + self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' " + f"is not a supported type for webhost. " + f"Supported types: {', '.join([t.__name__ for t in supported_types])}") + except AssertionError as ex: + self.fail(f"Option '{option_name}': '{option_value}' in preset '{preset_name}' for game " + f"'{game_name}' is not valid. Error: {ex}") + except KeyError as ex: + self.fail(f"Option '{option_name}' in preset '{preset_name}' for game '{game_name}' is " + f"not a defined option. Error: {ex}") + + def test_option_preset_values_are_explicitly_defined(self): + """Test that option preset values are not a special flavor of 'random' or use from_text to resolve another + value. + """ + for game_name, world_type in AutoWorldRegister.world_types.items(): + presets = world_type.web.options_presets + for preset_name, preset in presets.items(): + for option_name, option_value in preset.items(): + with self.subTest(game=game_name, preset=preset_name, option=option_name): + # Check for non-standard random values. + self.assertFalse( + str(option_value).startswith("random-"), + f"'{option_name}': '{option_value}' in preset '{preset_name}' for game '{game_name}' " + f"is not supported for webhost. Special random values are not supported for presets." + ) + + option = world_type.options_dataclass.type_hints[option_name].from_any(option_value) + + # Check for from_text resolving to a different value. ("random" is allowed though.) + if option_value != "random" and isinstance(option_value, str): + # Allow special named values for SpecialRange option presets. + if isinstance(option, SpecialRange): + self.assertTrue( + option_value in option.special_range_names, + f"Invalid preset '{option_name}': '{option_value}' in preset '{preset_name}' " + f"for game '{game_name}'. Expected {option.special_range_names.keys()} or " + f"{option.range_start}-{option.range_end}." + ) + else: + self.assertTrue( + option.name_lookup.get(option.value, None) == option_value, + f"'{option_name}': '{option_value}' in preset '{preset_name}' for game " + f"'{game_name}' is not supported for webhost. Values must not be resolved to a " + f"different option via option.from_text (or an alias)." + ) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 67403472fc5b..5d0533e068d6 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -186,6 +186,9 @@ class WebWorld: bug_report_page: Optional[str] """display a link to a bug report page, most likely a link to a GitHub issue page.""" + options_presets: Dict[str, Dict[str, Any]] = {} + """A dictionary containing a collection of developer-defined game option presets.""" + class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. diff --git a/worlds/rogue_legacy/Presets.py b/worlds/rogue_legacy/Presets.py new file mode 100644 index 000000000000..a4284e9f7d34 --- /dev/null +++ b/worlds/rogue_legacy/Presets.py @@ -0,0 +1,61 @@ +from typing import Any, Dict + +from .Options import Architect, GoldGainMultiplier, Vendors + +rl_options_presets: Dict[str, Dict[str, Any]] = { + # Example preset using only literal values. + "Unknown Fate": { + "progression_balancing": "random", + "accessibility": "random", + "starting_gender": "random", + "starting_class": "random", + "new_game_plus": "random", + "fairy_chests_per_zone": "random", + "chests_per_zone": "random", + "universal_fairy_chests": "random", + "universal_chests": "random", + "vendors": "random", + "architect": "random", + "architect_fee": "random", + "disable_charon": "random", + "require_purchasing": "random", + "progressive_blueprints": "random", + "gold_gain_multiplier": "random", + "number_of_children": "random", + "free_diary_on_generation": "random", + "khidr": "random", + "alexander": "random", + "leon": "random", + "herodotus": "random", + "health_pool": "random", + "mana_pool": "random", + "attack_pool": "random", + "magic_damage_pool": "random", + "armor_pool": "random", + "equip_pool": "random", + "crit_chance_pool": "random", + "crit_damage_pool": "random", + "allow_default_names": False, + "death_link": "random", + }, + # A preset I actually use, using some literal values and some from the option itself. + "Limited Potential": { + "progression_balancing": "disabled", + "fairy_chests_per_zone": 2, + "starting_class": "random", + "chests_per_zone": 30, + "vendors": Vendors.option_normal, + "architect": Architect.option_disabled, + "gold_gain_multiplier": GoldGainMultiplier.option_half, + "number_of_children": 2, + "free_diary_on_generation": False, + "health_pool": 10, + "mana_pool": 10, + "attack_pool": 10, + "magic_damage_pool": 10, + "armor_pool": 5, + "equip_pool": 10, + "crit_chance_pool": 5, + "crit_damage_pool": 5, + } +} diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index 68a0c856c8ad..c5a8d71b5d63 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -5,6 +5,7 @@ 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 .Presets import rl_options_presets from .Regions import create_regions from .Rules import set_rules @@ -22,6 +23,7 @@ class RLWeb(WebWorld): )] bug_report_page = "https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=" \ "report-an-issue---.md&title=%5BIssue%5D" + options_presets = rl_options_presets class RLWorld(World): From 185a5192481a20ddd8877f58398f13becf38bbf8 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 16 Nov 2023 04:55:18 -0600 Subject: [PATCH 041/104] Core: fix item links around core changes (#2452) --- BaseClasses.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 4ce99b698019..b25d998311a1 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -113,6 +113,11 @@ def extend(self, regions: Iterable[Region]): for region in regions: self.region_cache[region.player][region.name] = region + def add_group(self, new_id: int): + self.region_cache[new_id] = {} + self.entrance_cache[new_id] = {} + self.location_cache[new_id] = {} + def __iter__(self) -> Iterator[Region]: for regions in self.region_cache.values(): yield from regions.values() @@ -220,6 +225,7 @@ def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tu return group_id, group new_id: int = self.players + len(self.groups) + 1 + self.regions.add_group(new_id) self.game[new_id] = game self.player_types[new_id] = NetUtils.SlotType.group world_type = AutoWorld.AutoWorldRegister.world_types[game] @@ -617,7 +623,7 @@ class CollectionState(): additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = [] def __init__(self, parent: MultiWorld): - self.prog_items = {player: Counter() for player in parent.player_ids} + self.prog_items = {player: Counter() for player in parent.get_all_ids()} self.multiworld = parent self.reachable_regions = {player: set() for player in parent.get_all_ids()} self.blocked_connections = {player: set() for player in parent.get_all_ids()} From 790f192dedd454b2aac027ea6d392c55af1461ae Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 18 Nov 2023 12:29:35 -0600 Subject: [PATCH 042/104] WebHost: Refactor `tracker.py`, removal of dead code, and tweaks to layouts of some tracker pages. (#2438) --- WebHostLib/customserver.py | 6 +- WebHostLib/static/assets/trackerCommon.js | 19 +- WebHostLib/static/styles/tracker.css | 141 +- WebHostLib/templates/genericTracker.html | 113 +- WebHostLib/templates/hintTable.html | 28 - WebHostLib/templates/lttpMultiTracker.html | 171 - WebHostLib/templates/multiTracker.html | 92 - .../templates/multiTrackerNavigation.html | 9 - WebHostLib/templates/multitracker.html | 144 + .../templates/multitrackerHintTable.html | 37 + .../templates/multitrackerNavigation.html | 16 + .../multitracker__ALinkToThePast.html | 205 + ...acker.html => multitracker__Factorio.html} | 21 +- .../templates/tracker__ALinkToThePast.html | 154 + ...racker.html => tracker__ChecksFinder.html} | 5 + ...ftTracker.html => tracker__Minecraft.html} | 7 +- .../templates/tracker__OcarinaOfTime.html | 185 + ...=> tracker__Starcraft2WingsOfLiberty.html} | 5 + ...racker.html => tracker__SuperMetroid.html} | 5 + ...Tracker.html => tracker__Timespinner.html} | 11 +- WebHostLib/tracker.py | 3537 +++++++++-------- worlds/__init__.py | 55 +- 22 files changed, 2839 insertions(+), 2127 deletions(-) delete mode 100644 WebHostLib/templates/hintTable.html delete mode 100644 WebHostLib/templates/lttpMultiTracker.html delete mode 100644 WebHostLib/templates/multiTracker.html delete mode 100644 WebHostLib/templates/multiTrackerNavigation.html create mode 100644 WebHostLib/templates/multitracker.html create mode 100644 WebHostLib/templates/multitrackerHintTable.html create mode 100644 WebHostLib/templates/multitrackerNavigation.html create mode 100644 WebHostLib/templates/multitracker__ALinkToThePast.html rename WebHostLib/templates/{multiFactorioTracker.html => multitracker__Factorio.html} (79%) create mode 100644 WebHostLib/templates/tracker__ALinkToThePast.html rename WebHostLib/templates/{checksfinderTracker.html => tracker__ChecksFinder.html} (82%) rename WebHostLib/templates/{minecraftTracker.html => tracker__Minecraft.html} (94%) create mode 100644 WebHostLib/templates/tracker__OcarinaOfTime.html rename WebHostLib/templates/{sc2wolTracker.html => tracker__Starcraft2WingsOfLiberty.html} (99%) rename WebHostLib/templates/{supermetroidTracker.html => tracker__SuperMetroid.html} (94%) rename WebHostLib/templates/{timespinnerTracker.html => tracker__Timespinner.html} (95%) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 6d633314b2be..998fec5e738d 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -27,8 +27,10 @@ class CustomClientMessageProcessor(ClientMessageProcessor): ctx: WebHostContext - def _cmd_video(self, platform, user): - """Set a link for your name in the WebHostLib tracker pointing to a video stream""" + def _cmd_video(self, platform: str, user: str): + """Set a link for your name in the WebHostLib tracker pointing to a video stream. + Currently, only YouTube and Twitch platforms are supported. + """ if platform.lower().startswith("t"): # twitch self.ctx.video[self.client.team, self.client.slot] = "Twitch", user self.ctx.save() diff --git a/WebHostLib/static/assets/trackerCommon.js b/WebHostLib/static/assets/trackerCommon.js index cb16a4de782d..b8e089ece5d3 100644 --- a/WebHostLib/static/assets/trackerCommon.js +++ b/WebHostLib/static/assets/trackerCommon.js @@ -4,13 +4,20 @@ const adjustTableHeight = () => { return; const upperDistance = tablesContainer.getBoundingClientRect().top; - const containerHeight = window.innerHeight - upperDistance; - tablesContainer.style.maxHeight = `calc(${containerHeight}px - 1rem)`; - const tableWrappers = document.getElementsByClassName('table-wrapper'); - for(let i=0; i < tableWrappers.length; i++){ - const maxHeight = (window.innerHeight - upperDistance) / 2; - tableWrappers[i].style.maxHeight = `calc(${maxHeight}px - 1rem)`; + for (let i = 0; i < tableWrappers.length; i++) { + // Ensure we are starting from maximum size prior to calculation. + tableWrappers[i].style.height = null; + tableWrappers[i].style.maxHeight = null; + + // Set as a reasonable height, but still allows the user to resize element if they desire. + const currentHeight = tableWrappers[i].offsetHeight; + const maxHeight = (window.innerHeight - upperDistance) / Math.min(tableWrappers.length, 4); + if (currentHeight > maxHeight) { + tableWrappers[i].style.height = `calc(${maxHeight}px - 1rem)`; + } + + tableWrappers[i].style.maxHeight = `${currentHeight}px`; } }; diff --git a/WebHostLib/static/styles/tracker.css b/WebHostLib/static/styles/tracker.css index 0cc2ede59fe3..8fcb0c923012 100644 --- a/WebHostLib/static/styles/tracker.css +++ b/WebHostLib/static/styles/tracker.css @@ -7,81 +7,119 @@ width: calc(100% - 1rem); } -#tracker-wrapper a{ +#tracker-wrapper a { color: #234ae4; text-decoration: none; cursor: pointer; } -.table-wrapper{ - overflow-y: auto; - overflow-x: auto; - margin-bottom: 1rem; -} - -#tracker-header-bar{ +#tracker-header-bar { display: flex; flex-direction: row; justify-content: flex-start; + align-content: center; line-height: 20px; + gap: 0.5rem; + margin-bottom: 1rem; } -#tracker-header-bar .info{ +#tracker-header-bar .info { color: #ffffff; + padding: 2px; + flex-grow: 1; + align-self: center; + text-align: justify; +} + +#tracker-navigation { + display: flex; + flex-wrap: wrap; + margin: 0 0.5rem 0.5rem 0.5rem; + user-select: none; + height: 2rem; +} + +.tracker-navigation-bar { + display: flex; + background-color: #b0a77d; + border-radius: 4px; +} + +.tracker-navigation-button { + display: flex; + justify-content: center; + align-items: center; + margin: 4px; + padding-left: 12px; + padding-right: 12px; + border-radius: 4px; + text-align: center; + font-size: 14px; + color: black !important; + font-weight: lighter; +} + +.tracker-navigation-button:hover { + background-color: #e2eabb !important; +} + +.tracker-navigation-button.selected { + background-color: rgb(220, 226, 189); +} + +.table-wrapper { + overflow-y: auto; + overflow-x: auto; + margin-bottom: 1rem; + resize: vertical; } -#search{ +#search { border: 1px solid #000000; border-radius: 3px; padding: 3px; width: 200px; - margin-bottom: 0.5rem; - margin-right: 1rem; -} - -#multi-stream-link{ - margin-right: 1rem; } -div.dataTables_wrapper.no-footer .dataTables_scrollBody{ +div.dataTables_wrapper.no-footer .dataTables_scrollBody { border: none; } -table.dataTable{ +table.dataTable { color: #000000; } -table.dataTable thead{ +table.dataTable thead { font-family: LexendDeca-Regular, sans-serif; } -table.dataTable tbody, table.dataTable tfoot{ +table.dataTable tbody, table.dataTable tfoot { background-color: #dce2bd; font-family: LexendDeca-Light, sans-serif; } -table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{ +table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover { background-color: #e2eabb; } -table.dataTable tbody td, table.dataTable tfoot td{ +table.dataTable tbody td, table.dataTable tfoot td { padding: 4px 6px; } -table.dataTable, table.dataTable.no-footer{ +table.dataTable, table.dataTable.no-footer { border-left: 1px solid #bba967; width: calc(100% - 2px) !important; font-size: 1rem; } -table.dataTable thead th{ +table.dataTable thead th { position: -webkit-sticky; position: sticky; background-color: #b0a77d; top: 0; } -table.dataTable thead th.upper-row{ +table.dataTable thead th.upper-row { position: -webkit-sticky; position: sticky; background-color: #b0a77d; @@ -89,7 +127,7 @@ table.dataTable thead th.upper-row{ top: 0; } -table.dataTable thead th.lower-row{ +table.dataTable thead th.lower-row { position: -webkit-sticky; position: sticky; background-color: #b0a77d; @@ -97,59 +135,32 @@ table.dataTable thead th.lower-row{ top: 46px; } -table.dataTable tbody td, table.dataTable tfoot td{ +table.dataTable tbody td, table.dataTable tfoot td { border: 1px solid #bba967; } -table.dataTable tfoot td{ +table.dataTable tfoot td { font-weight: bold; } -div.dataTables_scrollBody{ +div.dataTables_scrollBody { background-color: inherit !important; } -table.dataTable .center-column{ +table.dataTable .center-column { text-align: center; } -img.alttp-sprite { +img.icon-sprite { height: auto; max-height: 32px; min-height: 14px; } -.item-acquired{ +.item-acquired { background-color: #d3c97d; } -#tracker-navigation { - display: inline-flex; - background-color: #b0a77d; - margin: 0.5rem; - border-radius: 4px; -} - -.tracker-navigation-button { - display: block; - margin: 4px; - padding-left: 12px; - padding-right: 12px; - border-radius: 4px; - text-align: center; - font-size: 14px; - color: #000; - font-weight: lighter; -} - -.tracker-navigation-button:hover { - background-color: #e2eabb !important; -} - -.tracker-navigation-button.selected { - background-color: rgb(220, 226, 189); -} - @media all and (max-width: 1700px) { table.dataTable thead th.upper-row{ position: -webkit-sticky; @@ -159,7 +170,7 @@ img.alttp-sprite { top: 0; } - table.dataTable thead th.lower-row{ + table.dataTable thead th.lower-row { position: -webkit-sticky; position: sticky; background-color: #b0a77d; @@ -167,11 +178,11 @@ img.alttp-sprite { top: 37px; } - table.dataTable, table.dataTable.no-footer{ + table.dataTable, table.dataTable.no-footer { font-size: 0.8rem; } - img.alttp-sprite { + img.icon-sprite { height: auto; max-height: 24px; min-height: 10px; @@ -187,7 +198,7 @@ img.alttp-sprite { top: 0; } - table.dataTable thead th.lower-row{ + table.dataTable thead th.lower-row { position: -webkit-sticky; position: sticky; background-color: #b0a77d; @@ -195,11 +206,11 @@ img.alttp-sprite { top: 32px; } - table.dataTable, table.dataTable.no-footer{ + table.dataTable, table.dataTable.no-footer { font-size: 0.6rem; } - img.alttp-sprite { + img.icon-sprite { height: auto; max-height: 20px; min-height: 10px; diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html index 1c2fcd44c0dd..5a533204083b 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -1,36 +1,57 @@ -{% extends 'tablepage.html' %} +{% extends "tablepage.html" %} {% block head %} {{ super() }} {{ player_name }}'s Tracker - - - + + + {% endblock %} {% block body %} - {% include 'header/dirtHeader.html' %} -
+ {% include "header/dirtHeader.html" %} + +
+
+ + 🡸 Return to Multiworld Tracker + + {% if game_specific_tracker %} + + Game-Specific Tracker + + {% endif %} +
+
+ +
- - This tracker will automatically update itself periodically. + +
This tracker will automatically update itself periodically.
+
- + - {% for id, count in inventory.items() %} - - - - - + {% for id, count in inventory.items() if count > 0 %} + + + + + {%- endfor -%} @@ -39,24 +60,62 @@
Item AmountOrder ReceivedLast Order Received
{{ id | item_name }}{{ count }}{{received_items[id]}}
{{ item_id_to_name[game][id] }}{{ count }}{{ received_items[id] }}
- - - - + + + + - {% for name in checked_locations %} + + {%- for location in locations -%} + + + + + {%- endfor -%} + + +
LocationChecked
LocationChecked
{{ location_id_to_name[game][location] }} + {% if location in checked_locations %}✔{% endif %} +
+
+
+ + - - + + + + + + + - {%- endfor -%} - {% for name in not_checked_locations %} + + + {%- for hint in hints -%} - - + + + + + + + - {%- endfor -%} + {%- endfor -%}
{{ name | location_name}}FinderReceiverItemLocationGameEntranceFound
{{ name | location_name}} + {% if hint.finding_player == player %} + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% else %} + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% endif %} + + {% if hint.receiving_player == player %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% else %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% endif %} + {{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}{{ games[(team, hint.finding_player)] }}{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}{% if hint.found %}✔{% endif %}
diff --git a/WebHostLib/templates/hintTable.html b/WebHostLib/templates/hintTable.html deleted file mode 100644 index 00b74111ea51..000000000000 --- a/WebHostLib/templates/hintTable.html +++ /dev/null @@ -1,28 +0,0 @@ -{% for team, hints in hints.items() %} -
- - - - - - - - - - - - - {%- for hint in hints -%} - - - - - - - - - {%- endfor -%} - -
FinderReceiverItemLocationEntranceFound
{{ long_player_names[team, hint.finding_player] }}{{ long_player_names[team, hint.receiving_player] }}{{ hint.item|item_name }}{{ hint.location|location_name }}{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}{% if hint.found %}✔{% endif %}
-
-{% endfor %} \ No newline at end of file diff --git a/WebHostLib/templates/lttpMultiTracker.html b/WebHostLib/templates/lttpMultiTracker.html deleted file mode 100644 index 8eb471be390d..000000000000 --- a/WebHostLib/templates/lttpMultiTracker.html +++ /dev/null @@ -1,171 +0,0 @@ -{% extends 'tablepage.html' %} -{% block head %} - {{ super() }} - ALttP Multiworld Tracker - - - - -{% endblock %} - -{% block body %} - {% include 'header/dirtHeader.html' %} - {% include 'multiTrackerNavigation.html' %} -
-
- - - - Multistream - - - Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically. -
-
- {% for team, players in inventory.items() %} -
- - - - - - {%- for name in tracking_names -%} - {%- if name in icons -%} - - {%- else -%} - - {%- endif -%} - {%- endfor -%} - - - - {%- for player, items in players.items() -%} - - - {%- if (team, loop.index) in video -%} - {%- if video[(team, loop.index)][0] == "Twitch" -%} - - {%- elif video[(team, loop.index)][0] == "Youtube" -%} - - {%- endif -%} - {%- else -%} - - {%- endif -%} - {%- for id in tracking_ids -%} - {%- if items[id] -%} - - {%- else -%} - - {%- endif -%} - {% endfor %} - - {%- endfor -%} - -
#Name - {{ name|e }} - {{ name|e }}
{{ loop.index }} - - {{ player_names[(team, loop.index)] }} - ▶️ - - {{ player_names[(team, loop.index)] }} - ▶️{{ player_names[(team, loop.index)] }} - {% if id in multi_items %}{{ items[id] }}{% else %}✔️{% endif %}
-
- {% endfor %} - - {% for team, players in checks_done.items() %} -
- - - - - - {% for area in ordered_areas %} - {% set colspan = 1 %} - {% if area in key_locations %} - {% set colspan = colspan + 1 %} - {% endif %} - {% if area in big_key_locations %} - {% set colspan = colspan + 1 %} - {% endif %} - {% if area in icons %} - - {%- else -%} - - {%- endif -%} - {%- endfor -%} - - - - - {% for area in ordered_areas %} - - {% if area in key_locations %} - - {% endif %} - {% if area in big_key_locations %} - - {%- endif -%} - {%- endfor -%} - - - - {%- for player, checks in players.items() -%} - - - - {%- for area in ordered_areas -%} - {% if player in checks_in_area and area in checks_in_area[player] %} - {%- set checks_done = checks[area] -%} - {%- set checks_total = checks_in_area[player][area] -%} - {%- if checks_done == checks_total -%} - - {%- else -%} - - {%- endif -%} - {%- if area in key_locations -%} - - {%- endif -%} - {%- if area in big_key_locations -%} - - {%- endif -%} - {% else %} - - {%- if area in key_locations -%} - - {%- endif -%} - {%- if area in big_key_locations -%} - - {%- endif -%} - {% endif %} - {%- endfor -%} - - {%- if activity_timers[(team, player)] -%} - - {%- else -%} - - {%- endif -%} - - {%- endfor -%} - -
#Name - {{ area }}{{ area }}%Last
Activity
- Checks - - Small Key - - Big Key -
{{ loop.index }}{{ player_names[(team, loop.index)]|e }} - {{ checks_done }}/{{ checks_total }}{{ checks_done }}/{{ checks_total }}{{ inventory[team][player][small_key_ids[area]] }}{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}{{ "{0:.2f}".format(percent_total_checks_done[team][player]) }}{{ activity_timers[(team, player)].total_seconds() }}None
-
- {% endfor %} - {% include "hintTable.html" with context %} -
-
-{% endblock %} diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html deleted file mode 100644 index 1a3d353de11a..000000000000 --- a/WebHostLib/templates/multiTracker.html +++ /dev/null @@ -1,92 +0,0 @@ -{% extends 'tablepage.html' %} -{% block head %} - {{ super() }} - Multiworld Tracker - - -{% endblock %} - -{% block body %} - {% include 'header/dirtHeader.html' %} - {% include 'multiTrackerNavigation.html' %} -
-
- - - - Multistream - - - Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically. -
-
- {% for team, players in checks_done.items() %} -
- - - - - - - - {% block custom_table_headers %} - {# implement this block in game-specific multi trackers #} - {% endblock %} - - - - - - - {%- for player, checks in players.items() -%} - - - - - - {% block custom_table_row scoped %} - {# implement this block in game-specific multi trackers #} - {% endblock %} - - - {%- if activity_timers[team, player] -%} - - {%- else -%} - - {%- endif -%} - - {%- endfor -%} - - {% if not self.custom_table_headers() | trim %} - - - - - - - - - - - - {% endif %} -
#NameGameStatusChecks%Last
Activity
{{ loop.index }}{{ player_names[(team, loop.index)]|e }}{{ games[player] }}{{ {0: "Disconnected", 5: "Connected", 10: "Ready", 20: "Playing", - 30: "Goal Completed"}.get(states[team, player], "Unknown State") }} - {{ checks["Total"] }}/{{ locations[player] | length }} - {{ "{0:.2f}".format(percent_total_checks_done[team][player]) }}{{ activity_timers[team, player].total_seconds() }}None
TotalAll Games{{ completed_worlds }}/{{ players|length }} Complete{{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }} - {% if total_locations[team] == 0 %} - 100 - {% else %} - {{ "{0:.2f}".format(players.values()|sum(attribute='Total') / total_locations[team] * 100) }} - {% endif %} -
-
- {% endfor %} - {% include "hintTable.html" with context %} -
-
-{% endblock %} diff --git a/WebHostLib/templates/multiTrackerNavigation.html b/WebHostLib/templates/multiTrackerNavigation.html deleted file mode 100644 index 7fc405b6fbd2..000000000000 --- a/WebHostLib/templates/multiTrackerNavigation.html +++ /dev/null @@ -1,9 +0,0 @@ -{%- if enabled_multiworld_trackers|length > 1 -%} -
- {% for enabled_tracker in enabled_multiworld_trackers %} - {% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %} - {{ enabled_tracker.name }} - {% endfor %} -
-{%- endif -%} diff --git a/WebHostLib/templates/multitracker.html b/WebHostLib/templates/multitracker.html new file mode 100644 index 000000000000..b16d4714ec6a --- /dev/null +++ b/WebHostLib/templates/multitracker.html @@ -0,0 +1,144 @@ +{% extends "tablepage.html" %} +{% block head %} + {{ super() }} + Multiworld Tracker + + +{% endblock %} + +{% block body %} + {% include "header/dirtHeader.html" %} + {% include "multitrackerNavigation.html" %} + +
+
+ + + + +
+ Clicking on a slot's number will bring up the slot-specific tracker. + This tracker will automatically update itself periodically. +
+
+ +
+ {%- for team, players in room_players.items() -%} +
+ + + + + + {% if current_tracker == "Generic" %}{% endif %} + + {% block custom_table_headers %} + {# Implement this block in game-specific multi-trackers. #} + {% endblock %} + + + + + + + {%- for player in players -%} + {%- if current_tracker == "Generic" or games[(team, player)] == current_tracker -%} + + + + {%- if current_tracker == "Generic" -%} + + {%- endif -%} + + + {% block custom_table_row scoped %} + {# Implement this block in game-specific multi-trackers. #} + {% endblock %} + + {% set location_count = locations[(team, player)] | length %} + + + + + {%- if activity_timers[(team, player)] -%} + + {%- else -%} + + {%- endif -%} + + {%- endif -%} + {%- endfor -%} + + + {%- if not self.custom_table_headers() | trim -%} + + + + + + + + + + + {%- endif -%} +
#NameGameStatusChecks%Last
Activity
+ + {{ player }} + + {{ player_names_with_alias[(team, player)] | e }}{{ games[(team, player)] }} + {{ + { + 0: "Disconnected", + 5: "Connected", + 10: "Ready", + 20: "Playing", + 30: "Goal Completed" + }.get(states[(team, player)], "Unknown State") + }} + + {{ locations_complete[(team, player)] }}/{{ location_count }} + + {%- if locations[(team, player)] | length > 0 -%} + {% set percentage_of_completion = locations_complete[(team, player)] / location_count * 100 %} + {{ "{0:.2f}".format(percentage_of_completion) }} + {%- else -%} + 100.00 + {%- endif -%} + {{ activity_timers[(team, player)].total_seconds() }}None
TotalAll Games{{ completed_worlds[team] }}/{{ players | length }} Complete + {{ total_team_locations_complete[team] }}/{{ total_team_locations[team] }} + + {%- if total_team_locations[team] == 0 -%} + 100 + {%- else -%} + {{ "{0:.2f}".format(total_team_locations_complete[team] / total_team_locations[team] * 100) }} + {%- endif -%} +
+
+ + {%- endfor -%} + + {% block custom_tables %} + {# Implement this block to create custom tables in game-specific multi-trackers. #} + {% endblock %} + + {% include "multitrackerHintTable.html" with context %} +
+
+{% endblock %} diff --git a/WebHostLib/templates/multitrackerHintTable.html b/WebHostLib/templates/multitrackerHintTable.html new file mode 100644 index 000000000000..a931e9b04845 --- /dev/null +++ b/WebHostLib/templates/multitrackerHintTable.html @@ -0,0 +1,37 @@ +{% for team, hints in hints.items() %} +
+ + + + + + + + + + + + + + {%- for hint in hints -%} + {%- + if current_tracker == "Generic" or ( + games[(team, hint.finding_player)] == current_tracker or + games[(team, hint.receiving_player)] == current_tracker + ) + -%} + + + + + + + + + + {% endif %} + {%- endfor -%} + +
FinderReceiverItemLocationGameEntranceFound
{{ player_names_with_alias[(team, hint.finding_player)] }}{{ player_names_with_alias[(team, hint.receiving_player)] }}{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}{{ games[(team, hint.finding_player)] }}{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}{% if hint.found %}✔{% endif %}
+
+{% endfor %} diff --git a/WebHostLib/templates/multitrackerNavigation.html b/WebHostLib/templates/multitrackerNavigation.html new file mode 100644 index 000000000000..1256181b27d3 --- /dev/null +++ b/WebHostLib/templates/multitrackerNavigation.html @@ -0,0 +1,16 @@ +{% if enabled_trackers | length > 1 %} +
+ {# Multitracker game navigation. #} +
+ {%- for game_tracker in enabled_trackers -%} + {%- set tracker_url = url_for("get_multiworld_tracker", tracker=room.tracker, game=game_tracker) -%} + + {{ game_tracker }} + + {%- endfor -%} +
+
+{% endif %} diff --git a/WebHostLib/templates/multitracker__ALinkToThePast.html b/WebHostLib/templates/multitracker__ALinkToThePast.html new file mode 100644 index 000000000000..8cea5ba05785 --- /dev/null +++ b/WebHostLib/templates/multitracker__ALinkToThePast.html @@ -0,0 +1,205 @@ +{% extends "multitracker.html" %} +{% block head %} + {{ super() }} + + +{% endblock %} + +{# List all tracker-relevant icons. Format: (Name, Image URL) #} +{%- set icons = { + "Blue Shield": "https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png", + "Red Shield": "https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png", + "Mirror Shield": "https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png", + "Fighter Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920", + "Master Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920", + "Tempered Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920", + "Golden Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920", + "Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c", + "Silver Bow": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920", + "Green Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920", + "Blue Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920", + "Red Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920", + "Power Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920", + "Titan Mitts": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920", + "Progressive Sword": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725", + "Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9", + "Progressive Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920", + "Flippers": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920", + "Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e", + "Progressive Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed", + "Blue Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e", + "Red Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400", + "Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b", + "Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59", + "Magic Powder": "https://www.zeldadungeon.net/wiki/images/thumb/6/62/MagicPowder-ALttP-Sprite.png/86px-MagicPowder-ALttP-Sprite.png", + "Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0", + "Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc", + "Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26", + "Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5", + "Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879", + "Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce", + "Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500", + "Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05", + "Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390", + "Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6", + "Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744", + "Bottle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b", + "Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943", + "Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54", + "Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832", + "Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc", + "Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48", + "Triforce Piece": "https://www.zeldadungeon.net/wiki/images/thumb/5/54/Triforce_Fragment_-_BS_Zelda.png/62px-Triforce_Fragment_-_BS_Zelda.png", + "Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e", + "Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d", + "Chest": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda", + "Light World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6", + "Dark World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc", + "Hyrule Castle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be", + "Agahnims Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5", + "Desert Palace": "https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png", + "Eastern Palace": "https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png", + "Tower of Hera": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7", + "Palace of Darkness": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022", + "Swamp Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5", + "Skull Woods": "https://alttp-wiki.net/images/6/6a/Mothula.png", + "Thieves Town": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222", + "Ice Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0", + "Misery Mire": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8", + "Turtle Rock": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be", + "Ganons Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74", +} -%} + +{%- block custom_table_headers %} +{#- macro that creates a table header with display name and image -#} +{%- macro make_header(name, img_src) %} + + {{ name }} + +{% endmacro -%} + +{#- call the macro to build the table header -#} +{%- for name in tracking_names %} + {%- if name in icons -%} + + {{ name | e }} + + {%- endif %} +{% endfor -%} +{% endblock %} + +{# build each row of custom entries #} +{% block custom_table_row scoped %} + {%- for id in tracking_ids -%} +{# {{ checks }}#} + {%- if inventories[(team, player)][id] -%} + + {% if id in multi_items %}{{ inventories[(team, player)][id] }}{% else %}✔️{% endif %} + + {%- else -%} + + {%- endif -%} + {% endfor %} +{% endblock %} + +{% block custom_tables %} + +{% for team, _ in total_team_locations.items() %} +
+ + + + + + {% for area in ordered_areas %} + {% set colspan = 1 %} + {% if area in key_locations %} + {% set colspan = colspan + 1 %} + {% endif %} + {% if area in big_key_locations %} + {% set colspan = colspan + 1 %} + {% endif %} + {% if area in icons %} + + {%- else -%} + + {%- endif -%} + {%- endfor -%} + + + + + {% for area in ordered_areas %} + + {% if area in key_locations %} + + {% endif %} + {% if area in big_key_locations %} + + {%- endif -%} + {%- endfor -%} + + + + {%- for (checks_team, player), area_checks in checks_done.items() if games[(team, player)] == current_tracker and team == checks_team -%} + + + + {%- for area in ordered_areas -%} + {% if (team, player) in checks_in_area and area in checks_in_area[(team, player)] %} + {%- set checks_done = area_checks[area] -%} + {%- set checks_total = checks_in_area[(team, player)][area] -%} + {%- if checks_done == checks_total -%} + + {%- else -%} + + {%- endif -%} + {%- if area in key_locations -%} + + {%- endif -%} + {%- if area in big_key_locations -%} + + {%- endif -%} + {% else %} + + {%- if area in key_locations -%} + + {%- endif -%} + {%- if area in big_key_locations -%} + + {%- endif -%} + {% endif %} + {%- endfor -%} + + + + {%- if activity_timers[(team, player)] -%} + + {%- else -%} + + {%- endif -%} + + {%- endfor -%} + +
#Name + {{ area }}{{ area }}%Last
Activity
+ Checks + + Small Key + + Big Key +
{{ player }}{{ player_names_with_alias[(team, player)] | e }} + {{ checks_done }}/{{ checks_total }}{{ checks_done }}/{{ checks_total }}{{ inventories[(team, player)][small_key_ids[area]] }}{% if inventories[(team, player)][big_key_ids[area]] %}✔️{% endif %} + {% set location_count = locations[(team, player)] | length %} + {%- if locations[(team, player)] | length > 0 -%} + {% set percentage_of_completion = locations_complete[(team, player)] / location_count * 100 %} + {{ "{0:.2f}".format(percentage_of_completion) }} + {%- else -%} + 100.00 + {%- endif -%} + {{ activity_timers[(team, player)].total_seconds() }}None
+
+{% endfor %} + +{% endblock %} diff --git a/WebHostLib/templates/multiFactorioTracker.html b/WebHostLib/templates/multitracker__Factorio.html similarity index 79% rename from WebHostLib/templates/multiFactorioTracker.html rename to WebHostLib/templates/multitracker__Factorio.html index 389a79d411b5..a7ad824db41f 100644 --- a/WebHostLib/templates/multiFactorioTracker.html +++ b/WebHostLib/templates/multitracker__Factorio.html @@ -1,4 +1,4 @@ -{% extends "multiTracker.html" %} +{% extends "multitracker.html" %} {# establish the to be tracked data. Display Name, factorio/AP internal name, display image #} {%- set science_packs = [ ("Logistic Science Pack", "logistic-science-pack", @@ -14,12 +14,12 @@ ("Space Science Pack", "space-science-pack", "https://wiki.factorio.com/images/thumb/Space_science_pack.png/32px-Space_science_pack.png"), ] -%} + {%- block custom_table_headers %} {#- macro that creates a table header with display name and image -#} {%- macro make_header(name, img_src) %} - {{ name }} + {{ name }} {% endmacro -%} {#- call the macro to build the table header -#} @@ -27,16 +27,15 @@ {{ make_header(name, img_src) }} {% endfor -%} {% endblock %} + {% block custom_table_row scoped %} -{% if games[player] == "Factorio" %} - {%- set player_inventory = named_inventory[team][player] -%} + {%- set player_inventory = inventories[(team, player)] -%} {%- set prog_science = player_inventory["progressive-science-pack"] -%} {%- for name, internal_name, img_src in science_packs %} - {% if player_inventory[internal_name] or prog_science > loop.index0 %}✔{% endif %} + {% if player_inventory[internal_name] or prog_science > loop.index0 %} + ✔️ + {% else %} + + {% endif %} {% endfor -%} -{% else %} - {%- for _ in science_packs %} - ❌ - {% endfor -%} -{% endif %} {% endblock%} diff --git a/WebHostLib/templates/tracker__ALinkToThePast.html b/WebHostLib/templates/tracker__ALinkToThePast.html new file mode 100644 index 000000000000..b7bae26fd35b --- /dev/null +++ b/WebHostLib/templates/tracker__ALinkToThePast.html @@ -0,0 +1,154 @@ +{%- set icons = { + "Blue Shield": "https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png", + "Red Shield": "https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png", + "Mirror Shield": "https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png", + "Fighter Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920", + "Master Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920", + "Tempered Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920", + "Golden Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920", + "Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c", + "Silver Bow": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920", + "Green Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920", + "Blue Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920", + "Red Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920", + "Power Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920", + "Titan Mitts": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920", + "Progressive Sword": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725", + "Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9", + "Progressive Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920", + "Flippers": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920", + "Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e", + "Progressive Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed", + "Blue Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e", + "Red Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400", + "Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b", + "Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59", + "Magic Powder": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec", + "Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0", + "Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc", + "Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26", + "Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5", + "Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879", + "Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce", + "Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500", + "Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05", + "Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390", + "Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6", + "Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744", + "Bottle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b", + "Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943", + "Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54", + "Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832", + "Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc", + "Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48", + "Triforce Piece": "https://www.zeldadungeon.net/wiki/images/thumb/5/54/Triforce_Fragment_-_BS_Zelda.png/62px-Triforce_Fragment_-_BS_Zelda.png", + "Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e", + "Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d", + "Chest": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda", + "Light World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6", + "Dark World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc", + "Hyrule Castle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be", + "Agahnims Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5", + "Desert Palace": "https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png", + "Eastern Palace": "https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png", + "Tower of Hera": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7", + "Palace of Darkness": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022", + "Swamp Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5", + "Skull Woods": "https://alttp-wiki.net/images/6/6a/Mothula.png", + "Thieves Town": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222", + "Ice Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0", + "Misery Mire": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8", + "Turtle Rock": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be", + "Ganons Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74", +} -%} + + + + + {{ player_name }}'s Tracker + + + + + + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + {% if key_locations and "Universal" not in key_locations %} + + {% endif %} + {% if big_key_locations %} + + {% endif %} + + {% for area in sp_areas %} + + + + {% if key_locations and "Universal" not in key_locations %} + + {% endif %} + {% if big_key_locations %} + + {% endif %} + + {% endfor %} +
{{ area }}{{ checks_done[area] }} / {{ checks_in_area[area] }} + {{ inventory[small_key_ids[area]] if area in key_locations else '—' }} + + {{ '✔' if area in big_key_locations and inventory[big_key_ids[area]] else ('—' if area not in big_key_locations else '') }} +
+
+ + diff --git a/WebHostLib/templates/checksfinderTracker.html b/WebHostLib/templates/tracker__ChecksFinder.html similarity index 82% rename from WebHostLib/templates/checksfinderTracker.html rename to WebHostLib/templates/tracker__ChecksFinder.html index 5df77f5e74d0..f0995c854838 100644 --- a/WebHostLib/templates/checksfinderTracker.html +++ b/WebHostLib/templates/tracker__ChecksFinder.html @@ -7,6 +7,11 @@ + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + +
diff --git a/WebHostLib/templates/minecraftTracker.html b/WebHostLib/templates/tracker__Minecraft.html similarity index 94% rename from WebHostLib/templates/minecraftTracker.html rename to WebHostLib/templates/tracker__Minecraft.html index 9f5022b4cc43..248f2778bda1 100644 --- a/WebHostLib/templates/minecraftTracker.html +++ b/WebHostLib/templates/tracker__Minecraft.html @@ -8,13 +8,18 @@ + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + +
-
diff --git a/WebHostLib/templates/tracker__OcarinaOfTime.html b/WebHostLib/templates/tracker__OcarinaOfTime.html new file mode 100644 index 000000000000..41b76816cfca --- /dev/null +++ b/WebHostLib/templates/tracker__OcarinaOfTime.html @@ -0,0 +1,185 @@ + + + + {{ player_name }}'s Tracker + + + + + + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
{{ hookshot_length }}
+
+
+
+ +
{{ bottle_count if bottle_count > 0 else '' }}
+
+
+
+ +
{{ wallet_size }}
+
+
+
+ +
Zelda
+
+
+
+ +
Epona
+
+
+
+ +
Saria
+
+
+
+ +
Sun
+
+
+
+ +
Time
+
+
+
+ +
Storms
+
+
+
+ +
{{ token_count }}
+
+
+
+ +
Min
+
+
+
+ +
Bol
+
+
+
+ +
Ser
+
+
+
+ +
Req
+
+
+
+ +
Noc
+
+
+
+ +
Pre
+
+
+
+ +
{{ piece_count if piece_count > 0 else '' }}
+
+
+ + + + + + + + {% for area in checks_done %} + + + + + + + + {% for location in location_info[area] %} + + + + + + + {% endfor %} + + {% endfor %} +
Items
{{ area }} {{'▼' if area != 'Total'}}{{ small_key_counts.get(area, '-') }}{{ boss_key_counts.get(area, '-') }}{{ checks_done[area] }} / {{ checks_in_area[area] }}
{{ location }}{{ '✔' if location_info[area][location] else '' }}
+
+ + diff --git a/WebHostLib/templates/sc2wolTracker.html b/WebHostLib/templates/tracker__Starcraft2WingsOfLiberty.html similarity index 99% rename from WebHostLib/templates/sc2wolTracker.html rename to WebHostLib/templates/tracker__Starcraft2WingsOfLiberty.html index 49c31a579544..c27f690dfd36 100644 --- a/WebHostLib/templates/sc2wolTracker.html +++ b/WebHostLib/templates/tracker__Starcraft2WingsOfLiberty.html @@ -8,6 +8,11 @@ + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + +
diff --git a/WebHostLib/templates/supermetroidTracker.html b/WebHostLib/templates/tracker__SuperMetroid.html similarity index 94% rename from WebHostLib/templates/supermetroidTracker.html rename to WebHostLib/templates/tracker__SuperMetroid.html index 342f75642fcc..0c648176513f 100644 --- a/WebHostLib/templates/supermetroidTracker.html +++ b/WebHostLib/templates/tracker__SuperMetroid.html @@ -7,6 +7,11 @@ + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + +
diff --git a/WebHostLib/templates/timespinnerTracker.html b/WebHostLib/templates/tracker__Timespinner.html similarity index 95% rename from WebHostLib/templates/timespinnerTracker.html rename to WebHostLib/templates/tracker__Timespinner.html index f02ec6daab77..b118c3383344 100644 --- a/WebHostLib/templates/timespinnerTracker.html +++ b/WebHostLib/templates/tracker__Timespinner.html @@ -7,6 +7,11 @@ + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + +
@@ -51,16 +56,16 @@
{% if 'DownloadableItems' in options %}
- {% endif %} + {% endif %}
{% if 'DownloadableItems' in options %}
- {% endif %} + {% endif %}
{% if 'EyeSpy' in options %}
- {% endif %} + {% endif %}
diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 55b98df59e42..8a7155afec6b 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1,1773 +1,1960 @@ -import collections import datetime -import typing -from typing import Counter, Optional, Dict, Any, Tuple, List +from dataclasses import dataclass +from typing import Any, Callable, Dict, List, Optional, Set, Tuple from uuid import UUID from flask import render_template -from jinja2 import pass_context, runtime from werkzeug.exceptions import abort from MultiServer import Context, get_saving_second -from NetUtils import ClientStatus, SlotType, NetworkSlot +from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType from Utils import restricted_loads -from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, games -from worlds.alttp import Items from . import app, cache from .models import GameDataPackage, Room -alttp_icons = { - "Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png", - "Red Shield": r"https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png", - "Mirror Shield": r"https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png", - "Fighter Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920", - "Master Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920", - "Tempered Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920", - "Golden Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920", - "Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c", - "Silver Bow": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920", - "Green Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920", - "Blue Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920", - "Red Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920", - "Power Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920", - "Titan Mitts": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920", - "Progressive Sword": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725", - "Pegasus Boots": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9", - "Progressive Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920", - "Flippers": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920", - "Moon Pearl": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e", - "Progressive Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed", - "Blue Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e", - "Red Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400", - "Hookshot": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b", - "Mushroom": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59", - "Magic Powder": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec", - "Fire Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0", - "Ice Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc", - "Bombos": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26", - "Ether": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5", - "Quake": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879", - "Lamp": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce", - "Hammer": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500", - "Shovel": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05", - "Flute": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390", - "Bug Catching Net": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6", - "Book of Mudora": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744", - "Bottle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b", - "Cane of Somaria": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943", - "Cane of Byrna": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54", - "Cape": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832", - "Magic Mirror": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc", - "Triforce": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48", - "Small Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e", - "Big Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d", - "Chest": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda", - "Light World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6", - "Dark World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc", - "Hyrule Castle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be", - "Agahnims Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5", - "Desert Palace": r"https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png", - "Eastern Palace": r"https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png", - "Tower of Hera": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7", - "Palace of Darkness": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022", - "Swamp Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5", - "Skull Woods": r"https://alttp-wiki.net/images/6/6a/Mothula.png", - "Thieves Town": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222", - "Ice Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0", - "Misery Mire": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8", - "Turtle Rock": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be", - "Ganons Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74" -} - - -def get_alttp_id(item_name): - return Items.item_table[item_name][2] - - -links = {"Bow": "Progressive Bow", - "Silver Arrows": "Progressive Bow", - "Silver Bow": "Progressive Bow", - "Progressive Bow (Alt)": "Progressive Bow", - "Bottle (Red Potion)": "Bottle", - "Bottle (Green Potion)": "Bottle", - "Bottle (Blue Potion)": "Bottle", - "Bottle (Fairy)": "Bottle", - "Bottle (Bee)": "Bottle", - "Bottle (Good Bee)": "Bottle", - "Fighter Sword": "Progressive Sword", - "Master Sword": "Progressive Sword", - "Tempered Sword": "Progressive Sword", - "Golden Sword": "Progressive Sword", - "Power Glove": "Progressive Glove", - "Titans Mitts": "Progressive Glove" - } - -levels = {"Fighter Sword": 1, - "Master Sword": 2, - "Tempered Sword": 3, - "Golden Sword": 4, - "Power Glove": 1, - "Titans Mitts": 2, - "Bow": 1, - "Silver Bow": 2} - -multi_items = {get_alttp_id(name) for name in ("Progressive Sword", "Progressive Bow", "Bottle", "Progressive Glove")} -links = {get_alttp_id(key): get_alttp_id(value) for key, value in links.items()} -levels = {get_alttp_id(key): value for key, value in levels.items()} - -tracking_names = ["Progressive Sword", "Progressive Bow", "Book of Mudora", "Hammer", - "Hookshot", "Magic Mirror", "Flute", - "Pegasus Boots", "Progressive Glove", "Flippers", "Moon Pearl", "Blue Boomerang", - "Red Boomerang", "Bug Catching Net", "Cape", "Shovel", "Lamp", - "Mushroom", "Magic Powder", - "Cane of Somaria", "Cane of Byrna", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", - "Bottle", "Triforce"] - -default_locations = { - 'Light World': {1572864, 1572865, 60034, 1572867, 1572868, 60037, 1572869, 1572866, 60040, 59788, 60046, 60175, - 1572880, 60049, 60178, 1572883, 60052, 60181, 1572885, 60055, 60184, 191256, 60058, 60187, 1572884, - 1572886, 1572887, 1572906, 60202, 60205, 59824, 166320, 1010170, 60208, 60211, 60214, 60217, 59836, - 60220, 60223, 59839, 1573184, 60226, 975299, 1573188, 1573189, 188229, 60229, 60232, 1573193, - 1573194, 60235, 1573187, 59845, 59854, 211407, 60238, 59857, 1573185, 1573186, 1572882, 212328, - 59881, 59761, 59890, 59770, 193020, 212605}, - 'Dark World': {59776, 59779, 975237, 1572870, 60043, 1572881, 60190, 60193, 60196, 60199, 60840, 1573190, 209095, - 1573192, 1573191, 60241, 60244, 60247, 60250, 59884, 59887, 60019, 60022, 60028, 60031}, - 'Desert Palace': {1573216, 59842, 59851, 59791, 1573201, 59830}, - 'Eastern Palace': {1573200, 59827, 59893, 59767, 59833, 59773}, - 'Hyrule Castle': {60256, 60259, 60169, 60172, 59758, 59764, 60025, 60253}, - 'Agahnims Tower': {60082, 60085}, - 'Tower of Hera': {1573218, 59878, 59821, 1573202, 59896, 59899}, - 'Swamp Palace': {60064, 60067, 60070, 59782, 59785, 60073, 60076, 60079, 1573204, 60061}, - 'Thieves Town': {59905, 59908, 59911, 59914, 59917, 59920, 59923, 1573206}, - 'Skull Woods': {59809, 59902, 59848, 59794, 1573205, 59800, 59803, 59806}, - 'Ice Palace': {59872, 59875, 59812, 59818, 59860, 59797, 1573207, 59869}, - 'Misery Mire': {60001, 60004, 60007, 60010, 60013, 1573208, 59866, 59998}, - 'Turtle Rock': {59938, 59941, 59944, 1573209, 59947, 59950, 59953, 59956, 59926, 59929, 59932, 59935}, - 'Palace of Darkness': {59968, 59971, 59974, 59977, 59980, 59983, 59986, 1573203, 59989, 59959, 59992, 59962, 59995, - 59965}, - 'Ganons Tower': {60160, 60163, 60166, 60088, 60091, 60094, 60097, 60100, 60103, 60106, 60109, 60112, 60115, 60118, - 60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157}, - 'Total': set()} - -key_only_locations = { - 'Light World': set(), - 'Dark World': set(), - 'Desert Palace': {0x140031, 0x14002b, 0x140061, 0x140028}, - 'Eastern Palace': {0x14005b, 0x140049}, - 'Hyrule Castle': {0x140037, 0x140034, 0x14000d, 0x14003d}, - 'Agahnims Tower': {0x140061, 0x140052}, - 'Tower of Hera': set(), - 'Swamp Palace': {0x140019, 0x140016, 0x140013, 0x140010, 0x14000a}, - 'Thieves Town': {0x14005e, 0x14004f}, - 'Skull Woods': {0x14002e, 0x14001c}, - 'Ice Palace': {0x140004, 0x140022, 0x140025, 0x140046}, - 'Misery Mire': {0x140055, 0x14004c, 0x140064}, - 'Turtle Rock': {0x140058, 0x140007}, - 'Palace of Darkness': set(), - 'Ganons Tower': {0x140040, 0x140043, 0x14003a, 0x14001f}, - 'Total': set() -} - -location_to_area = {} -for area, locations in default_locations.items(): - for location in locations: - location_to_area[location] = area - -for area, locations in key_only_locations.items(): - for location in locations: - location_to_area[location] = area - -checks_in_area = {area: len(checks) for area, checks in default_locations.items()} -checks_in_area["Total"] = 216 - -ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace', - 'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', - 'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total") - -tracking_ids = [] - -for item in tracking_names: - tracking_ids.append(get_alttp_id(item)) - -small_key_ids = {} -big_key_ids = {} -ids_small_key = {} -ids_big_key = {} - -for item_name, data in Items.item_table.items(): - if "Key" in item_name: - area = item_name.split("(")[1][:-1] - if "Small" in item_name: - small_key_ids[area] = data[2] - ids_small_key[data[2]] = area - else: - big_key_ids[area] = data[2] - ids_big_key[data[2]] = area - -# cleanup global namespace -del item_name -del data -del item - - -def attribute_item_solo(inventory, item): - """Adds item to inventory counter, converts everything to progressive.""" - target_item = links.get(item, item) - if item in levels: # non-progressive - inventory[target_item] = max(inventory[target_item], levels[item]) - else: - inventory[target_item] += 1 +# Multisave is currently updated, at most, every minute. +TRACKER_CACHE_TIMEOUT_IN_SECONDS = 60 +_multidata_cache = {} +_multiworld_trackers: Dict[str, Callable] = {} +_player_trackers: Dict[str, Callable] = {} -@app.template_filter() -def render_timedelta(delta: datetime.timedelta): - hours, minutes = divmod(delta.total_seconds() / 60, 60) - hours = str(int(hours)) - minutes = str(int(minutes)).zfill(2) - return f"{hours}:{minutes}" +TeamPlayer = Tuple[int, int] +ItemMetadata = Tuple[int, int, int] -@pass_context -def get_location_name(context: runtime.Context, loc: int) -> str: - # once all rooms embed data package, the chain lookup can be dropped - context_locations = context.get("custom_locations", {}) - return collections.ChainMap(context_locations, lookup_any_location_id_to_name).get(loc, loc) +def _cache_results(func: Callable) -> Callable: + """Stores the results of any computationally expensive methods after the initial call in TrackerData. + If called again, returns the cached result instead, as results will not change for the lifetime of TrackerData. + """ + def method_wrapper(self: "TrackerData", *args): + cache_key = f"{func.__name__}{''.join(f'_[{arg.__repr__()}]' for arg in args)}" + if cache_key in self._tracker_cache: + return self._tracker_cache[cache_key] + result = func(self, *args) + self._tracker_cache[cache_key] = result + return result -@pass_context -def get_item_name(context: runtime.Context, item: int) -> str: - context_items = context.get("custom_items", {}) - return collections.ChainMap(context_items, lookup_any_item_id_to_name).get(item, item) + return method_wrapper + + +@dataclass +class TrackerData: + """A helper dataclass that is instantiated each time an HTTP request comes in for tracker data. + + Provides helper methods to lazily load necessary data that each tracker require and caches any results so any + subsequent helper method calls do not need to recompute results during the lifetime of this instance. + """ + room: Room + _multidata: Dict[str, Any] + _multisave: Dict[str, Any] + _tracker_cache: Dict[str, Any] + + def __init__(self, room: Room): + """Initialize a new RoomMultidata object for the current room.""" + self.room = room + self._multidata = Context.decompress(room.seed.multidata) + self._multisave = restricted_loads(room.multisave) if room.multisave else {} + self._tracker_cache = {} + + self.item_name_to_id: Dict[str, Dict[str, int]] = {} + self.location_name_to_id: Dict[str, Dict[str, int]] = {} + + # Generate inverse lookup tables from data package, useful for trackers. + self.item_id_to_name: Dict[str, Dict[int, str]] = {} + self.location_id_to_name: Dict[str, Dict[int, str]] = {} + for game, game_package in self._multidata["datapackage"].items(): + game_package = restricted_loads(GameDataPackage.get(checksum=game_package["checksum"]).data) + self.item_id_to_name[game] = {id: name for name, id in game_package["item_name_to_id"].items()} + self.location_id_to_name[game] = {id: name for name, id in game_package["location_name_to_id"].items()} + + # Normal lookup tables as well. + self.item_name_to_id[game] = game_package["item_name_to_id"] + self.location_name_to_id[game] = game_package["item_name_to_id"] + + def get_seed_name(self) -> str: + """Retrieves the seed name.""" + return self._multidata["seed_name"] + + def get_slot_data(self, team: int, player: int) -> Dict[str, Any]: + """Retrieves the slot data for a given player.""" + return self._multidata["slot_data"][player] + + def get_slot_info(self, team: int, player: int) -> NetworkSlot: + """Retrieves the NetworkSlot data for a given player.""" + return self._multidata["slot_info"][player] + + def get_player_name(self, team: int, player: int) -> str: + """Retrieves the slot name for a given player.""" + return self.get_slot_info(team, player).name + + def get_player_game(self, team: int, player: int) -> str: + """Retrieves the game for a given player.""" + return self.get_slot_info(team, player).game + + def get_player_locations(self, team: int, player: int) -> Dict[int, ItemMetadata]: + """Retrieves all locations with their containing item's metadata for a given player.""" + return self._multidata["locations"][player] + + def get_player_starting_inventory(self, team: int, player: int) -> List[int]: + """Retrieves a list of all item codes a given slot starts with.""" + return self._multidata["precollected_items"][player] + + def get_player_checked_locations(self, team: int, player: int) -> Set[int]: + """Retrieves the set of all locations marked complete by this player.""" + return self._multisave.get("location_checks", {}).get((team, player), set()) + + @_cache_results + def get_player_missing_locations(self, team: int, player: int) -> Set[int]: + """Retrieves the set of all locations not marked complete by this player.""" + return set(self.get_player_locations(team, player)) - self.get_player_checked_locations(team, player) + + def get_player_received_items(self, team: int, player: int) -> List[NetworkItem]: + """Returns all items received to this player in order of received.""" + return self._multisave.get("received_items", {}).get((team, player, True), []) + + @_cache_results + def get_player_inventory_counts(self, team: int, player: int) -> Dict[int, int]: + """Retrieves a dictionary of all items received by their id and their received count.""" + items = self.get_player_received_items(team, player) + inventory = {item: 0 for item in self.item_id_to_name[self.get_player_game(team, player)]} + for item in items: + inventory[item.item] += 1 + + return inventory + + @_cache_results + def get_player_hints(self, team: int, player: int) -> Set[Hint]: + """Retrieves a set of all hints relevant for a particular player.""" + return self._multisave.get("hints", {}).get((team, player), set()) + + @_cache_results + def get_player_last_activity(self, team: int, player: int) -> Optional[datetime.timedelta]: + """Retrieves the relative timedelta for when a particular player was last active. + Returns None if no activity was ever recorded. + """ + return self.get_room_last_activity().get((team, player), None) + + def get_player_client_status(self, team: int, player: int) -> ClientStatus: + """Retrieves the ClientStatus of a particular player.""" + return self._multisave.get("client_game_state", {}).get((team, player), ClientStatus.CLIENT_UNKNOWN) + + def get_player_alias(self, team: int, player: int) -> Optional[str]: + """Returns the alias of a particular player, if any.""" + return self._multisave.get("name_aliases", {}).get((team, player), None) + + @_cache_results + def get_team_completed_worlds_count(self) -> Dict[int, int]: + """Retrieves a dictionary of number of completed worlds per team.""" + return { + team: sum( + self.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL + for player in players if self.get_slot_info(team, player).type == SlotType.player + ) for team, players in self.get_team_players().items() + } + @_cache_results + def get_team_hints(self) -> Dict[int, Set[Hint]]: + """Retrieves a dictionary of all hints per team.""" + hints = {} + for team, players in self.get_team_players().items(): + hints[team] = set() + for player in players: + hints[team] |= self.get_player_hints(team, player) + + return hints + + @_cache_results + def get_team_locations_total_count(self) -> Dict[int, int]: + """Retrieves a dictionary of total player locations each team has.""" + return { + team: sum(len(self.get_player_locations(team, player)) for player in players) + for team, players in self.get_team_players().items() + } -app.jinja_env.filters["location_name"] = get_location_name -app.jinja_env.filters["item_name"] = get_item_name + @_cache_results + def get_team_locations_checked_count(self) -> Dict[int, int]: + """Retrieves a dictionary of checked player locations each team has.""" + return { + team: sum(len(self.get_player_checked_locations(team, player)) for player in players) + for team, players in self.get_team_players().items() + } + # TODO: Change this method to properly build for each team once teams are properly implemented, as they don't + # currently exist in multidata to easily look up, so these are all assuming only 1 team: Team #0 + @_cache_results + def get_team_players(self) -> Dict[int, List[int]]: + """Retrieves a dictionary of all players ids on each team.""" + return { + 0: [player for player, slot_info in self._multidata["slot_info"].items()] + } -_multidata_cache = {} + @_cache_results + def get_room_saving_second(self) -> int: + """Retrieves the saving second value for this seed. + Useful for knowing when the multisave gets updated so trackers can attempt to update. + """ + return get_saving_second(self.get_seed_name()) -def get_location_table(checks_table: dict) -> dict: - loc_to_area = {} - for area, locations in checks_table.items(): - if area == "Total": - continue - for location in locations: - loc_to_area[location] = area - return loc_to_area + @_cache_results + def get_room_locations(self) -> Dict[TeamPlayer, Dict[int, ItemMetadata]]: + """Retrieves a dictionary of all locations and their associated item metadata per player.""" + return { + (team, player): self.get_player_locations(team, player) + for team, players in self.get_team_players().items() for player in players + } + @_cache_results + def get_room_games(self) -> Dict[TeamPlayer, str]: + """Retrieves a dictionary of games for each player.""" + return { + (team, player): self.get_player_game(team, player) + for team, players in self.get_team_players().items() for player in players + } -def get_static_room_data(room: Room): - result = _multidata_cache.get(room.seed.id, None) - if result: - return result - multidata = Context.decompress(room.seed.multidata) - # in > 100 players this can take a bit of time and is the main reason for the cache - locations: Dict[int, Dict[int, Tuple[int, int, int]]] = multidata['locations'] - names: List[List[str]] = multidata.get("names", []) - games = multidata.get("games", {}) - groups = {} - custom_locations = {} - custom_items = {} - if "slot_info" in multidata: - slot_info_dict: Dict[int, NetworkSlot] = multidata["slot_info"] - games = {slot: slot_info.game for slot, slot_info in slot_info_dict.items()} - groups = {slot: slot_info.group_members for slot, slot_info in slot_info_dict.items() - if slot_info.type == SlotType.group} - names = [[slot_info.name for slot, slot_info in sorted(slot_info_dict.items())]] - for game in games.values(): - if game not in multidata["datapackage"]: - continue - game_data = multidata["datapackage"][game] - if "checksum" in game_data: - if network_data_package["games"].get(game, {}).get("checksum") == game_data["checksum"]: - # non-custom. remove from multidata - # network_data_package import could be skipped once all rooms embed data package - del multidata["datapackage"][game] - continue - else: - game_data = restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data) - custom_locations.update( - {id_: name for name, id_ in game_data["location_name_to_id"].items()}) - custom_items.update( - {id_: name for name, id_ in game_data["item_name_to_id"].items()}) - - seed_checks_in_area = checks_in_area.copy() - - use_door_tracker = False - if "tags" in multidata: - use_door_tracker = "DR" in multidata["tags"] - if use_door_tracker: - for area, checks in key_only_locations.items(): - seed_checks_in_area[area] += len(checks) - seed_checks_in_area["Total"] = 249 - - player_checks_in_area = { - playernumber: { - areaname: len(multidata["checks_in_area"][playernumber][areaname]) if areaname != "Total" else - multidata["checks_in_area"][playernumber]["Total"] - for areaname in ordered_areas + @_cache_results + def get_room_locations_complete(self) -> Dict[TeamPlayer, int]: + """Retrieves a dictionary of all locations complete per player.""" + return { + (team, player): len(self.get_player_checked_locations(team, player)) + for team, players in self.get_team_players().items() for player in players } - for playernumber in multidata["checks_in_area"] - } - player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber]) - for playernumber in multidata["checks_in_area"]} - saving_second = get_saving_second(multidata["seed_name"]) - result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \ - multidata["precollected_items"], games, multidata["slot_data"], groups, saving_second, \ - custom_locations, custom_items - _multidata_cache[room.seed.id] = result - return result + @_cache_results + def get_room_client_statuses(self) -> Dict[TeamPlayer, ClientStatus]: + """Retrieves a dictionary of all ClientStatus values per player.""" + return { + (team, player): self.get_player_client_status(team, player) + for team, players in self.get_team_players().items() for player in players + } + + @_cache_results + def get_room_long_player_names(self) -> Dict[TeamPlayer, str]: + """Retrieves a dictionary of names with aliases for each player.""" + long_player_names = {} + for team, players in self.get_team_players().items(): + for player in players: + alias = self.get_player_alias(team, player) + if alias: + long_player_names[team, player] = f"{alias} ({self.get_player_name(team, player)})" + else: + long_player_names[team, player] = self.get_player_name(team, player) + + return long_player_names + @_cache_results + def get_room_last_activity(self) -> Dict[TeamPlayer, datetime.timedelta]: + """Retrieves a dictionary of all players and the timedelta from now to their last activity. + Does not include players who have no activity recorded. + """ + last_activity: Dict[TeamPlayer, datetime.timedelta] = {} + now = datetime.datetime.utcnow() + for (team, player), timestamp in self._multisave.get("client_activity_timers", []): + last_activity[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) -@app.route('/tracker///') -def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False): - key = f"{tracker}_{tracked_team}_{tracked_player}_{want_generic}" + return last_activity + + @_cache_results + def get_room_videos(self) -> Dict[TeamPlayer, Tuple[str, str]]: + """Retrieves a dictionary of any players who have video streaming enabled and their feeds. + + Only supported platforms are Twitch and YouTube. + """ + video_feeds = {} + for (team, player), video_data in self._multisave.get("video", []): + video_feeds[team, player] = video_data + + return video_feeds + + +@app.route("/tracker///") +def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> str: + key = f"{tracker}_{tracked_team}_{tracked_player}_{generic}" tracker_page = cache.get(key) if tracker_page: return tracker_page - timeout, tracker_page = _get_player_tracker(tracker, tracked_team, tracked_player, want_generic) + + timeout, tracker_page = get_timeout_and_tracker(tracker, tracked_team, tracked_player, generic) cache.set(key, tracker_page, timeout) return tracker_page -def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool): - # Team and player must be positive and greater than zero - if tracked_team < 0 or tracked_player < 1: - abort(404) +@app.route("/generic_tracker///") +def get_generic_game_tracker(tracker: UUID, tracked_team: int, tracked_player: int) -> str: + return get_player_tracker(tracker, tracked_team, tracked_player, True) + - room: Optional[Room] = Room.get(tracker=tracker) +@app.route("/tracker/", defaults={"game": "Generic"}) +@app.route("/tracker//") +@cache.memoize(timeout=TRACKER_CACHE_TIMEOUT_IN_SECONDS) +def get_multiworld_tracker(tracker: UUID, game: str): + # Room must exist. + room = Room.get(tracker=tracker) if not room: abort(404) - # Collect seed information and pare it down to a single player - locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ - precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ - get_static_room_data(room) - player_name = names[tracked_team][tracked_player - 1] - location_to_area = player_location_to_area.get(tracked_player, {}) - inventory = collections.Counter() - checks_done = {loc_name: 0 for loc_name in default_locations} - - # Add starting items to inventory - starting_items = precollected_items[tracked_player] - if starting_items: - for item_id in starting_items: - attribute_item_solo(inventory, item_id) - - if room.multisave: - multisave: Dict[str, Any] = restricted_loads(room.multisave) - else: - multisave: Dict[str, Any] = {} - - slots_aimed_at_player = {tracked_player} - for group_id, group_members in groups.items(): - if tracked_player in group_members: - slots_aimed_at_player.add(group_id) - - # Add items to player inventory - for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}).items(): - # Skip teams and players not matching the request - player_locations = locations[ms_player] - if ms_team == tracked_team: - # If the player does not have the item, do nothing - for location in locations_checked: - if location in player_locations: - item, recipient, flags = player_locations[location] - if recipient in slots_aimed_at_player: # a check done for the tracked player - attribute_item_solo(inventory, item) - if ms_player == tracked_player: # a check done by the tracked player - area_name = location_to_area.get(location, None) - if area_name: - checks_done[area_name] += 1 - checks_done["Total"] += 1 - specific_tracker = game_specific_trackers.get(games[tracked_player], None) - if specific_tracker and not want_generic: - tracker = specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, - seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second) - else: - tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, - player_name, seed_checks_in_area, checks_done, saving_second, - custom_locations, custom_items) + tracker_data = TrackerData(room) + enabled_trackers = list(get_enabled_multiworld_trackers(room).keys()) + if game not in _multiworld_trackers: + return render_generic_multiworld_tracker(tracker_data, enabled_trackers) - return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker + return _multiworld_trackers[game](tracker_data, enabled_trackers) -@app.route('/generic_tracker///') -def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int): - return get_player_tracker(tracker, tracked_team, tracked_player, True) +def get_timeout_and_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool) -> Tuple[int, str]: + # Room must exist. + room = Room.get(tracker=tracker) + if not room: + abort(404) + tracker_data = TrackerData(room) -def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, player_name: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, - saving_second: int) -> str: + # Load and render the game-specific player tracker, or fallback to generic tracker if none exists. + game_specific_tracker = _player_trackers.get(tracker_data.get_player_game(tracked_team, tracked_player), None) + if game_specific_tracker and not generic: + tracker = game_specific_tracker(tracker_data, tracked_team, tracked_player) + else: + tracker = render_generic_tracker(tracker_data, tracked_team, tracked_player) - # Note the presence of the triforce item - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - if game_state == 30: - inventory[106] = 1 # Triforce + return (tracker_data.get_room_saving_second() - datetime.datetime.now().second) % 60 or 60, tracker - # Progressive items need special handling for icons and class - progressive_items = { - "Progressive Sword": 94, - "Progressive Glove": 97, - "Progressive Bow": 100, - "Progressive Mail": 96, - "Progressive Shield": 95, - } - progressive_names = { - "Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'], - "Progressive Glove": [None, 'Power Glove', 'Titan Mitts'], - "Progressive Bow": [None, "Bow", "Silver Bow"], - "Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"], - "Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"] - } - # Determine which icon to use - display_data = {} - for item_name, item_id in progressive_items.items(): - level = min(inventory[item_id], len(progressive_names[item_name]) - 1) - display_name = progressive_names[item_name][level] - acquired = True - if not display_name: - acquired = False - display_name = progressive_names[item_name][level + 1] - base_name = item_name.split(maxsplit=1)[1].lower() - display_data[base_name + "_acquired"] = acquired - display_data[base_name + "_url"] = alttp_icons[display_name] - - # The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should? - sp_areas = ordered_areas[2:15] - - player_big_key_locations = set() - player_small_key_locations = set() - for loc_data in locations.values(): - for values in loc_data.values(): - item_id, item_player, flags = values - if item_player == player: - if item_id in ids_big_key: - player_big_key_locations.add(ids_big_key[item_id]) - elif item_id in ids_small_key: - player_small_key_locations.add(ids_small_key[item_id]) - - return render_template("lttpTracker.html", inventory=inventory, - player_name=player_name, room=room, icons=alttp_icons, checks_done=checks_done, - checks_in_area=seed_checks_in_area[player], - acquired_items={lookup_any_item_id_to_name[id] for id in inventory}, - small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas, - key_locations=player_small_key_locations, - big_key_locations=player_big_key_locations, - **display_data) - - -def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, - saving_second: int) -> str: - - icons = { - "Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png", - "Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png", - "Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png", - "Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png", - "Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png", - "Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png", - "Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png", - "Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png", - "Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png", - "Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png", - "Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png", - "Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png", - "Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png", - "Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png", - "Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png", - "Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png", - "Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png", - "Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png", - "Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png", - "Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png", - "Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png", - "Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif", - "Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png", - "Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif", - "Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png", - "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png", - "Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png", - "Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png", - "Saddle": "https://i.imgur.com/2QtDyR0.png", - "Channeling Book": "https://i.imgur.com/J3WsYZw.png", - "Silk Touch Book": "https://i.imgur.com/iqERxHQ.png", - "Piercing IV Book": "https://i.imgur.com/OzJptGz.png", - } +def get_enabled_multiworld_trackers(room: Room) -> Dict[str, Callable]: + # Render the multitracker for any games that exist in the current room if they are defined. + enabled_trackers = {} + for game_name, endpoint in _multiworld_trackers.items(): + if any(slot.game == game_name for slot in room.seed.slots): + enabled_trackers[game_name] = endpoint - minecraft_location_ids = { - "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070, - 42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077], - "Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021, - 42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014], - "The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046], - "Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42109, 42035, 42016, 42020, - 42048, 42054, 42068, 42043, 42106, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42105, 42099, 42103, 42110, 42100], - "Husbandry": [42065, 42067, 42078, 42022, 42113, 42107, 42007, 42079, 42013, 42028, 42036, 42108, 42111, 42112, - 42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095], - "Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091], + # We resort the tracker to have Generic first, then lexicographically each enabled game. + return { + "Generic": render_generic_multiworld_tracker, + **{key: enabled_trackers[key] for key in sorted(enabled_trackers.keys())}, } - display_data = {} - # Determine display for progressive items - progressive_items = { - "Progressive Tools": 45013, - "Progressive Weapons": 45012, - "Progressive Armor": 45014, - "Progressive Resource Crafting": 45001 - } - progressive_names = { - "Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"], - "Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"], - "Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"], - "Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"] - } - for item_name, item_id in progressive_items.items(): - level = min(inventory[item_id], len(progressive_names[item_name]) - 1) - display_name = progressive_names[item_name][level] - base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_') - display_data[base_name + "_url"] = icons[display_name] - - # Multi-items - multi_items = { - "3 Ender Pearls": 45029, - "8 Netherite Scrap": 45015, - "Dragon Egg Shard": 45043 - } - for item_name, item_id in multi_items.items(): - base_name = item_name.split()[-1].lower() - count = inventory[item_id] - if count >= 0: - display_data[base_name + "_count"] = count +def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + game = tracker_data.get_player_game(team, player) + + # Add received index to all received items, excluding starting inventory. + received_items_in_order = {} + for received_index, network_item in enumerate(tracker_data.get_player_received_items(team, player), start=1): + received_items_in_order[network_item.item] = received_index + + return render_template( + template_name_or_list="genericTracker.html", + game_specific_tracker=game in _player_trackers, + room=tracker_data.room, + team=team, + player=player, + player_name=tracker_data.get_room_long_player_names()[team, player], + inventory=tracker_data.get_player_inventory_counts(team, player), + locations=tracker_data.get_player_locations(team, player), + checked_locations=tracker_data.get_player_checked_locations(team, player), + received_items=received_items_in_order, + saving_second=tracker_data.get_room_saving_second(), + game=game, + games=tracker_data.get_room_games(), + player_names_with_alias=tracker_data.get_room_long_player_names(), + location_id_to_name=tracker_data.location_id_to_name, + item_id_to_name=tracker_data.item_id_to_name, + hints=tracker_data.get_player_hints(team, player), + ) - # Victory condition - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - display_data['game_finished'] = game_state == 30 - - # Turn location IDs into advancement tab counts - checked_locations = multisave.get("location_checks", {}).get((team, player), set()) - lookup_name = lambda id: lookup_any_location_id_to_name[id] - location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} - for tab_name, tab_locations in minecraft_location_ids.items()} - checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) - for tab_name, tab_locations in minecraft_location_ids.items()} - checks_done['Total'] = len(checked_locations) - checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()} - checks_in_area['Total'] = sum(checks_in_area.values()) - - return render_template("minecraftTracker.html", - inventory=inventory, icons=icons, - acquired_items={lookup_any_item_id_to_name[id] for id in inventory if - id in lookup_any_item_id_to_name}, - player=player, team=team, room=room, player_name=playerName, saving_second = saving_second, - checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, - **display_data) - - -def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, - saving_second: int) -> str: - - icons = { - "Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png", - "Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png", - "Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png", - "Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png", - "Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png", - "Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png", - "Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png", - "Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png", - "Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png", - "Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png", - "Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png", - "Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png", - "Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png", - "Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png", - "Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png", - "Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png", - "Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png", - "Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png", - "Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png", - "Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png", - "Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png", - "Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png", - "Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png", - "Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png", - "Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png", - "Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png", - "Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png", - "Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png", - "Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png", - "Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png", - "Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png", - "Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png", - "Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png", - "Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png", - "Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png", - "Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png", - "Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png", - "Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png", - "Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png", - "Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", - "Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", - "Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", - "Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", - "Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", - "Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", - "Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png", - "Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png", - "Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png", - "Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png", - "Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png", - "Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png", - "Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png", - "Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png", - } - display_data = {} +def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_trackers: List[str]) -> str: + return render_template( + "multitracker.html", + enabled_trackers=enabled_trackers, + current_tracker="Generic", + room=tracker_data.room, + room_players=tracker_data.get_team_players(), + locations=tracker_data.get_room_locations(), + locations_complete=tracker_data.get_room_locations_complete(), + total_team_locations=tracker_data.get_team_locations_total_count(), + total_team_locations_complete=tracker_data.get_team_locations_checked_count(), + player_names_with_alias=tracker_data.get_room_long_player_names(), + completed_worlds=tracker_data.get_team_completed_worlds_count(), + games=tracker_data.get_room_games(), + states=tracker_data.get_room_client_statuses(), + hints=tracker_data.get_team_hints(), + activity_timers=tracker_data.get_room_last_activity(), + videos=tracker_data.get_room_videos(), + item_id_to_name=tracker_data.item_id_to_name, + location_id_to_name=tracker_data.location_id_to_name, + ) - # Determine display for progressive items - progressive_items = { - "Progressive Hookshot": 66128, - "Progressive Strength Upgrade": 66129, - "Progressive Wallet": 66133, - "Progressive Scale": 66134, - "Magic Meter": 66138, - "Ocarina": 66139, - } - progressive_names = { - "Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"], - "Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets", "Golden Gauntlets"], - "Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"], - "Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"], - "Magic Meter": ["Small Magic", "Small Magic", "Large Magic"], - "Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"] - } +# TODO: This is a temporary solution until a proper Tracker API can be implemented for tracker templates and data to +# live in their respective world folders. +import collections - for item_name, item_id in progressive_items.items(): - level = min(inventory[item_id], len(progressive_names[item_name])-1) - display_name = progressive_names[item_name][level] - if item_name.startswith("Progressive"): - base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_') - else: - base_name = item_name.lower().replace(' ', '_') - display_data[base_name+"_url"] = icons[display_name] - - if base_name == "hookshot": - display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level) - if base_name == "wallet": - display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level) - - # Determine display for bottles. Show letter if it's obtained, determine bottle count - bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148] - display_data['bottle_count'] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4) - display_data['bottle_url'] = icons['Rutos Letter'] if inventory[66021] > 0 else icons['Bottle'] - - # Determine bombchu display - display_data['has_bombchus'] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137])) - - # Multi-items - multi_items = { - "Gold Skulltula Token": 66091, - "Triforce Piece": 66202, - } - for item_name, item_id in multi_items.items(): - base_name = item_name.split()[-1].lower() - display_data[base_name+"_count"] = inventory[item_id] - - # Gather dungeon locations - area_id_ranges = { - "Overworld": ((67000, 67263), (67269, 67280), (67747, 68024), (68054, 68062)), - "Deku Tree": ((67281, 67303), (68063, 68077)), - "Dodongo's Cavern": ((67304, 67334), (68078, 68160)), - "Jabu Jabu's Belly": ((67335, 67359), (68161, 68188)), - "Bottom of the Well": ((67360, 67384), (68189, 68230)), - "Forest Temple": ((67385, 67420), (68231, 68281)), - "Fire Temple": ((67421, 67457), (68282, 68350)), - "Water Temple": ((67458, 67484), (68351, 68483)), - "Shadow Temple": ((67485, 67532), (68484, 68565)), - "Spirit Temple": ((67533, 67582), (68566, 68625)), - "Ice Cavern": ((67583, 67596), (68626, 68649)), - "Gerudo Training Ground": ((67597, 67635), (68650, 68656)), - "Thieves' Hideout": ((67264, 67268), (68025, 68053)), - "Ganon's Castle": ((67636, 67673), (68657, 68705)), - } +from worlds import network_data_package - def lookup_and_trim(id, area): - full_name = lookup_any_location_id_to_name[id] - if 'Ganons Tower' in full_name: - return full_name - if area not in ["Overworld", "Thieves' Hideout"]: - # trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC - return full_name[len(area):] - return full_name - - checked_locations = multisave.get("location_checks", {}).get((team, player), set()).intersection(set(locations[player])) - location_info = {} - checks_done = {} - checks_in_area = {} - for area, ranges in area_id_ranges.items(): - location_info[area] = {} - checks_done[area] = 0 - checks_in_area[area] = 0 - for r in ranges: - min_id, max_id = r - for id in range(min_id, max_id+1): - if id in locations[player]: - checked = id in checked_locations - location_info[area][lookup_and_trim(id, area)] = checked - checks_in_area[area] += 1 - checks_done[area] += checked - - checks_done['Total'] = sum(checks_done.values()) - checks_in_area['Total'] = sum(checks_in_area.values()) - - # Give skulltulas on non-tracked locations - non_tracked_locations = multisave.get("location_checks", {}).get((team, player), set()).difference(set(locations[player])) - for id in non_tracked_locations: - if "GS" in lookup_and_trim(id, ''): - display_data["token_count"] += 1 - - oot_y = '✔' - oot_x = '✕' - - # Gather small and boss key info - small_key_counts = { - "Forest Temple": oot_y if inventory[66203] else inventory[66175], - "Fire Temple": oot_y if inventory[66204] else inventory[66176], - "Water Temple": oot_y if inventory[66205] else inventory[66177], - "Spirit Temple": oot_y if inventory[66206] else inventory[66178], - "Shadow Temple": oot_y if inventory[66207] else inventory[66179], - "Bottom of the Well": oot_y if inventory[66208] else inventory[66180], - "Gerudo Training Ground": oot_y if inventory[66209] else inventory[66181], - "Thieves' Hideout": oot_y if inventory[66210] else inventory[66182], - "Ganon's Castle": oot_y if inventory[66211] else inventory[66183], - } - boss_key_counts = { - "Forest Temple": oot_y if inventory[66149] else oot_x, - "Fire Temple": oot_y if inventory[66150] else oot_x, - "Water Temple": oot_y if inventory[66151] else oot_x, - "Spirit Temple": oot_y if inventory[66152] else oot_x, - "Shadow Temple": oot_y if inventory[66153] else oot_x, - "Ganon's Castle": oot_y if inventory[66154] else oot_x, - } - # Victory condition - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - display_data['game_finished'] = game_state == 30 - - return render_template("ootTracker.html", - inventory=inventory, player=player, team=team, room=room, player_name=playerName, - icons=icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory}, - checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, - small_key_counts=small_key_counts, boss_key_counts=boss_key_counts, - **display_data) - - -def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], - slot_data: Dict[str, Any], saving_second: int) -> str: - - icons = { - "Timespinner Wheel": "https://timespinnerwiki.com/mediawiki/images/7/76/Timespinner_Wheel.png", - "Timespinner Spindle": "https://timespinnerwiki.com/mediawiki/images/1/1a/Timespinner_Spindle.png", - "Timespinner Gear 1": "https://timespinnerwiki.com/mediawiki/images/3/3c/Timespinner_Gear_1.png", - "Timespinner Gear 2": "https://timespinnerwiki.com/mediawiki/images/e/e9/Timespinner_Gear_2.png", - "Timespinner Gear 3": "https://timespinnerwiki.com/mediawiki/images/2/22/Timespinner_Gear_3.png", - "Talaria Attachment": "https://timespinnerwiki.com/mediawiki/images/6/61/Talaria_Attachment.png", - "Succubus Hairpin": "https://timespinnerwiki.com/mediawiki/images/4/49/Succubus_Hairpin.png", - "Lightwall": "https://timespinnerwiki.com/mediawiki/images/0/03/Lightwall.png", - "Celestial Sash": "https://timespinnerwiki.com/mediawiki/images/f/f1/Celestial_Sash.png", - "Twin Pyramid Key": "https://timespinnerwiki.com/mediawiki/images/4/49/Twin_Pyramid_Key.png", - "Security Keycard D": "https://timespinnerwiki.com/mediawiki/images/1/1b/Security_Keycard_D.png", - "Security Keycard C": "https://timespinnerwiki.com/mediawiki/images/e/e5/Security_Keycard_C.png", - "Security Keycard B": "https://timespinnerwiki.com/mediawiki/images/f/f6/Security_Keycard_B.png", - "Security Keycard A": "https://timespinnerwiki.com/mediawiki/images/b/b9/Security_Keycard_A.png", - "Library Keycard V": "https://timespinnerwiki.com/mediawiki/images/5/50/Library_Keycard_V.png", - "Tablet": "https://timespinnerwiki.com/mediawiki/images/a/a0/Tablet.png", - "Elevator Keycard": "https://timespinnerwiki.com/mediawiki/images/5/55/Elevator_Keycard.png", - "Oculus Ring": "https://timespinnerwiki.com/mediawiki/images/8/8d/Oculus_Ring.png", - "Water Mask": "https://timespinnerwiki.com/mediawiki/images/0/04/Water_Mask.png", - "Gas Mask": "https://timespinnerwiki.com/mediawiki/images/2/2e/Gas_Mask.png", - "Djinn Inferno": "https://timespinnerwiki.com/mediawiki/images/f/f6/Djinn_Inferno.png", - "Pyro Ring": "https://timespinnerwiki.com/mediawiki/images/2/2c/Pyro_Ring.png", - "Infernal Flames": "https://timespinnerwiki.com/mediawiki/images/1/1f/Infernal_Flames.png", - "Fire Orb": "https://timespinnerwiki.com/mediawiki/images/3/3e/Fire_Orb.png", - "Royal Ring": "https://timespinnerwiki.com/mediawiki/images/f/f3/Royal_Ring.png", - "Plasma Geyser": "https://timespinnerwiki.com/mediawiki/images/1/12/Plasma_Geyser.png", - "Plasma Orb": "https://timespinnerwiki.com/mediawiki/images/4/44/Plasma_Orb.png", - "Kobo": "https://timespinnerwiki.com/mediawiki/images/c/c6/Familiar_Kobo.png", - "Merchant Crow": "https://timespinnerwiki.com/mediawiki/images/4/4e/Familiar_Crow.png", - } +if "Factorio" in network_data_package["games"]: + def render_Factorio_multiworld_tracker(tracker_data: TrackerData, enabled_trackers: List[str]): + inventories: Dict[TeamPlayer, Dict[int, int]] = { + (team, player): { + tracker_data.item_id_to_name["Factorio"][item_id]: count + for item_id, count in tracker_data.get_player_inventory_counts(team, player).items() + } for team, players in tracker_data.get_team_players().items() for player in players + if tracker_data.get_player_game(team, player) == "Factorio" + } - timespinner_location_ids = { - "Present": [ - 1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009, - 1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019, - 1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029, - 1337030, 1337031, 1337032, 1337033, 1337034, 1337035, 1337036, 1337037, 1337038, 1337039, - 1337040, 1337041, 1337042, 1337043, 1337044, 1337045, 1337046, 1337047, 1337048, 1337049, - 1337050, 1337051, 1337052, 1337053, 1337054, 1337055, 1337056, 1337057, 1337058, 1337059, - 1337060, 1337061, 1337062, 1337063, 1337064, 1337065, 1337066, 1337067, 1337068, 1337069, - 1337070, 1337071, 1337072, 1337073, 1337074, 1337075, 1337076, 1337077, 1337078, 1337079, - 1337080, 1337081, 1337082, 1337083, 1337084, 1337085], - "Past": [ - 1337086, 1337087, 1337088, 1337089, - 1337090, 1337091, 1337092, 1337093, 1337094, 1337095, 1337096, 1337097, 1337098, 1337099, - 1337100, 1337101, 1337102, 1337103, 1337104, 1337105, 1337106, 1337107, 1337108, 1337109, - 1337110, 1337111, 1337112, 1337113, 1337114, 1337115, 1337116, 1337117, 1337118, 1337119, - 1337120, 1337121, 1337122, 1337123, 1337124, 1337125, 1337126, 1337127, 1337128, 1337129, - 1337130, 1337131, 1337132, 1337133, 1337134, 1337135, 1337136, 1337137, 1337138, 1337139, - 1337140, 1337141, 1337142, 1337143, 1337144, 1337145, 1337146, 1337147, 1337148, 1337149, - 1337150, 1337151, 1337152, 1337153, 1337154, 1337155, - 1337171, 1337172, 1337173, 1337174, 1337175], - "Ancient Pyramid": [ - 1337236, - 1337246, 1337247, 1337248, 1337249] - } + return render_template( + "multitracker__Factorio.html", + enabled_trackers=enabled_trackers, + current_tracker="Factorio", + room=tracker_data.room, + room_players=tracker_data.get_team_players(), + locations=tracker_data.get_room_locations(), + locations_complete=tracker_data.get_room_locations_complete(), + total_team_locations=tracker_data.get_team_locations_total_count(), + total_team_locations_complete=tracker_data.get_team_locations_checked_count(), + player_names_with_alias=tracker_data.get_room_long_player_names(), + completed_worlds=tracker_data.get_team_completed_worlds_count(), + games=tracker_data.get_room_games(), + states=tracker_data.get_room_client_statuses(), + hints=tracker_data.get_team_hints(), + activity_timers=tracker_data.get_room_last_activity(), + videos=tracker_data.get_room_videos(), + item_id_to_name=tracker_data.item_id_to_name, + location_id_to_name=tracker_data.location_id_to_name, + inventories=inventories, + ) - if(slot_data["DownloadableItems"]): - timespinner_location_ids["Present"] += [ - 1337156, 1337157, 1337159, - 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169, - 1337170] - if(slot_data["Cantoran"]): - timespinner_location_ids["Past"].append(1337176) - if(slot_data["LoreChecks"]): - timespinner_location_ids["Present"] += [ - 1337177, 1337178, 1337179, - 1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187] - timespinner_location_ids["Past"] += [ - 1337188, 1337189, - 1337190, 1337191, 1337192, 1337193, 1337194, 1337195, 1337196, 1337197, 1337198] - if(slot_data["GyreArchives"]): - timespinner_location_ids["Ancient Pyramid"] += [ - 1337237, 1337238, 1337239, - 1337240, 1337241, 1337242, 1337243, 1337244, 1337245] - - display_data = {} - - # Victory condition - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - display_data['game_finished'] = game_state == 30 - - # Turn location IDs into advancement tab counts - checked_locations = multisave.get("location_checks", {}).get((team, player), set()) - lookup_name = lambda id: lookup_any_location_id_to_name[id] - location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} - for tab_name, tab_locations in timespinner_location_ids.items()} - checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) - for tab_name, tab_locations in timespinner_location_ids.items()} - checks_done['Total'] = len(checked_locations) - checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in timespinner_location_ids.items()} - checks_in_area['Total'] = sum(checks_in_area.values()) - acquired_items = {lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name} - options = {k for k, v in slot_data.items() if v} - - return render_template("timespinnerTracker.html", - inventory=inventory, icons=icons, acquired_items=acquired_items, - player=player, team=team, room=room, player_name=playerName, - checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, - options=options, **display_data) - -def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, - saving_second: int) -> str: - - icons = { - "Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ETank.png", - "Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Missile.png", - "Super Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Super.png", - "Power Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/PowerBomb.png", - "Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Bomb.png", - "Charge Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Charge.png", - "Ice Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Ice.png", - "Hi-Jump Boots": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/HiJump.png", - "Speed Booster": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpeedBooster.png", - "Wave Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Wave.png", - "Spazer": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Spazer.png", - "Spring Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpringBall.png", - "Varia Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Varia.png", - "Plasma Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Plasma.png", - "Grappling Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Grapple.png", - "Morph Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Morph.png", - "Reserve Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Reserve.png", - "Gravity Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Gravity.png", - "X-Ray Scope": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/XRayScope.png", - "Space Jump": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpaceJump.png", - "Screw Attack": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ScrewAttack.png", - "Nothing": "", - "No Energy": "", - "Kraid": "", - "Phantoon": "", - "Draygon": "", - "Ridley": "", - "Mother Brain": "", - } + _multiworld_trackers["Factorio"] = render_Factorio_multiworld_tracker - multi_items = { - "Energy Tank": 83000, - "Missile": 83001, - "Super Missile": 83002, - "Power Bomb": 83003, - "Reserve Tank": 83020, - } +if "A Link to the Past" in network_data_package["games"]: + def render_ALinkToThePast_multiworld_tracker(tracker_data: TrackerData, enabled_trackers: List[str]): + # Helper objects. + alttp_id_lookup = tracker_data.item_name_to_id["A Link to the Past"] - supermetroid_location_ids = { - 'Crateria/Blue Brinstar': [82005, 82007, 82008, 82026, 82029, - 82000, 82004, 82006, 82009, 82010, - 82011, 82012, 82027, 82028, 82034, - 82036, 82037], - 'Green/Pink Brinstar': [82017, 82023, 82030, 82033, 82035, - 82013, 82014, 82015, 82016, 82018, - 82019, 82021, 82022, 82024, 82025, - 82031], - 'Red Brinstar': [82038, 82042, 82039, 82040, 82041], - 'Kraid': [82043, 82048, 82044], - 'Norfair': [82050, 82053, 82061, 82066, 82068, - 82049, 82051, 82054, 82055, 82056, - 82062, 82063, 82064, 82065, 82067], - 'Lower Norfair': [82078, 82079, 82080, 82070, 82071, - 82073, 82074, 82075, 82076, 82077], - 'Crocomire': [82052, 82060, 82057, 82058, 82059], - 'Wrecked Ship': [82129, 82132, 82134, 82135, 82001, - 82002, 82003, 82128, 82130, 82131, - 82133], - 'West Maridia': [82138, 82136, 82137, 82139, 82140, - 82141, 82142], - 'East Maridia': [82143, 82145, 82150, 82152, 82154, - 82144, 82146, 82147, 82148, 82149, - 82151], - } + multi_items = { + alttp_id_lookup[name] + for name in ("Progressive Sword", "Progressive Bow", "Bottle", "Progressive Glove", "Triforce Piece") + } + links = { + "Bow": "Progressive Bow", + "Silver Arrows": "Progressive Bow", + "Silver Bow": "Progressive Bow", + "Progressive Bow (Alt)": "Progressive Bow", + "Bottle (Red Potion)": "Bottle", + "Bottle (Green Potion)": "Bottle", + "Bottle (Blue Potion)": "Bottle", + "Bottle (Fairy)": "Bottle", + "Bottle (Bee)": "Bottle", + "Bottle (Good Bee)": "Bottle", + "Fighter Sword": "Progressive Sword", + "Master Sword": "Progressive Sword", + "Tempered Sword": "Progressive Sword", + "Golden Sword": "Progressive Sword", + "Power Glove": "Progressive Glove", + "Titans Mitts": "Progressive Glove", + } + links = {alttp_id_lookup[key]: alttp_id_lookup[value] for key, value in links.items()} + levels = { + "Fighter Sword": 1, + "Master Sword": 2, + "Tempered Sword": 3, + "Golden Sword": 4, + "Power Glove": 1, + "Titans Mitts": 2, + "Bow": 1, + "Silver Bow": 2, + "Triforce Piece": 90, + } + tracking_names = [ + "Progressive Sword", "Progressive Bow", "Book of Mudora", "Hammer", "Hookshot", "Magic Mirror", "Flute", + "Pegasus Boots", "Progressive Glove", "Flippers", "Moon Pearl", "Blue Boomerang", "Red Boomerang", + "Bug Catching Net", "Cape", "Shovel", "Lamp", "Mushroom", "Magic Powder", "Cane of Somaria", + "Cane of Byrna", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", "Bottle", "Triforce Piece", "Triforce", + ] + default_locations = { + "Light World": { + 1572864, 1572865, 60034, 1572867, 1572868, 60037, 1572869, 1572866, 60040, 59788, 60046, 60175, + 1572880, 60049, 60178, 1572883, 60052, 60181, 1572885, 60055, 60184, 191256, 60058, 60187, 1572884, + 1572886, 1572887, 1572906, 60202, 60205, 59824, 166320, 1010170, 60208, 60211, 60214, 60217, 59836, + 60220, 60223, 59839, 1573184, 60226, 975299, 1573188, 1573189, 188229, 60229, 60232, 1573193, + 1573194, 60235, 1573187, 59845, 59854, 211407, 60238, 59857, 1573185, 1573186, 1572882, 212328, + 59881, 59761, 59890, 59770, 193020, 212605 + }, + "Dark World": { + 59776, 59779, 975237, 1572870, 60043, 1572881, 60190, 60193, 60196, 60199, 60840, 1573190, 209095, + 1573192, 1573191, 60241, 60244, 60247, 60250, 59884, 59887, 60019, 60022, 60028, 60031 + }, + "Desert Palace": {1573216, 59842, 59851, 59791, 1573201, 59830}, + "Eastern Palace": {1573200, 59827, 59893, 59767, 59833, 59773}, + "Hyrule Castle": {60256, 60259, 60169, 60172, 59758, 59764, 60025, 60253}, + "Agahnims Tower": {60082, 60085}, + "Tower of Hera": {1573218, 59878, 59821, 1573202, 59896, 59899}, + "Swamp Palace": {60064, 60067, 60070, 59782, 59785, 60073, 60076, 60079, 1573204, 60061}, + "Thieves Town": {59905, 59908, 59911, 59914, 59917, 59920, 59923, 1573206}, + "Skull Woods": {59809, 59902, 59848, 59794, 1573205, 59800, 59803, 59806}, + "Ice Palace": {59872, 59875, 59812, 59818, 59860, 59797, 1573207, 59869}, + "Misery Mire": {60001, 60004, 60007, 60010, 60013, 1573208, 59866, 59998}, + "Turtle Rock": {59938, 59941, 59944, 1573209, 59947, 59950, 59953, 59956, 59926, 59929, 59932, 59935}, + "Palace of Darkness": { + 59968, 59971, 59974, 59977, 59980, 59983, 59986, 1573203, 59989, 59959, 59992, 59962, 59995, + 59965 + }, + "Ganons Tower": { + 60160, 60163, 60166, 60088, 60091, 60094, 60097, 60100, 60103, 60106, 60109, 60112, 60115, 60118, + 60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157 + }, + "Total": set() + } + key_only_locations = { + "Light World": set(), + "Dark World": set(), + "Desert Palace": {0x140031, 0x14002b, 0x140061, 0x140028}, + "Eastern Palace": {0x14005b, 0x140049}, + "Hyrule Castle": {0x140037, 0x140034, 0x14000d, 0x14003d}, + "Agahnims Tower": {0x140061, 0x140052}, + "Tower of Hera": set(), + "Swamp Palace": {0x140019, 0x140016, 0x140013, 0x140010, 0x14000a}, + "Thieves Town": {0x14005e, 0x14004f}, + "Skull Woods": {0x14002e, 0x14001c}, + "Ice Palace": {0x140004, 0x140022, 0x140025, 0x140046}, + "Misery Mire": {0x140055, 0x14004c, 0x140064}, + "Turtle Rock": {0x140058, 0x140007}, + "Palace of Darkness": set(), + "Ganons Tower": {0x140040, 0x140043, 0x14003a, 0x14001f}, + "Total": set() + } + location_to_area = {} + for area, locations in default_locations.items(): + for location in locations: + location_to_area[location] = area + for area, locations in key_only_locations.items(): + for location in locations: + location_to_area[location] = area + + checks_in_area = {area: len(checks) for area, checks in default_locations.items()} + checks_in_area["Total"] = 216 + ordered_areas = ( + "Light World", "Dark World", "Hyrule Castle", "Agahnims Tower", "Eastern Palace", "Desert Palace", + "Tower of Hera", "Palace of Darkness", "Swamp Palace", "Skull Woods", "Thieves Town", "Ice Palace", + "Misery Mire", "Turtle Rock", "Ganons Tower", "Total" + ) - display_data = {} - - - for item_name, item_id in multi_items.items(): - base_name = item_name.split()[0].lower() - display_data[base_name+"_count"] = inventory[item_id] - - # Victory condition - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - display_data['game_finished'] = game_state == 30 - - # Turn location IDs into advancement tab counts - checked_locations = multisave.get("location_checks", {}).get((team, player), set()) - lookup_name = lambda id: lookup_any_location_id_to_name[id] - location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} - for tab_name, tab_locations in supermetroid_location_ids.items()} - checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) - for tab_name, tab_locations in supermetroid_location_ids.items()} - checks_done['Total'] = len(checked_locations) - checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()} - checks_in_area['Total'] = sum(checks_in_area.values()) - - return render_template("supermetroidTracker.html", - inventory=inventory, icons=icons, - acquired_items={lookup_any_item_id_to_name[id] for id in inventory if - id in lookup_any_item_id_to_name}, - player=player, team=team, room=room, player_name=playerName, - checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, - **display_data) - -def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], - slot_data: Dict, saving_second: int) -> str: - - SC2WOL_LOC_ID_OFFSET = 1000 - SC2WOL_ITEM_ID_OFFSET = 1000 - - - icons = { - "Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png", - "Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png", - "Starting Supply": "https://static.wikia.nocookie.net/starcraft/images/d/d3/TerranSupply_SC2_Icon1.gif", - - "Infantry Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel1.png", - "Infantry Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel2.png", - "Infantry Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel3.png", - "Infantry Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel1.png", - "Infantry Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel2.png", - "Infantry Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel3.png", - "Vehicle Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel1.png", - "Vehicle Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel2.png", - "Vehicle Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel3.png", - "Vehicle Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel1.png", - "Vehicle Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel2.png", - "Vehicle Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel3.png", - "Ship Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel1.png", - "Ship Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel2.png", - "Ship Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel3.png", - "Ship Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel1.png", - "Ship Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel2.png", - "Ship Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel3.png", - - "Bunker": "https://static.wikia.nocookie.net/starcraft/images/c/c5/Bunker_SC2_Icon1.jpg", - "Missile Turret": "https://static.wikia.nocookie.net/starcraft/images/5/5f/MissileTurret_SC2_Icon1.jpg", - "Sensor Tower": "https://static.wikia.nocookie.net/starcraft/images/d/d2/SensorTower_SC2_Icon1.jpg", - - "Projectile Accelerator (Bunker)": "https://0rganics.org/archipelago/sc2wol/ProjectileAccelerator.png", - "Neosteel Bunker (Bunker)": "https://0rganics.org/archipelago/sc2wol/NeosteelBunker.png", - "Titanium Housing (Missile Turret)": "https://0rganics.org/archipelago/sc2wol/TitaniumHousing.png", - "Hellstorm Batteries (Missile Turret)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png", - "Advanced Construction (SCV)": "https://0rganics.org/archipelago/sc2wol/AdvancedConstruction.png", - "Dual-Fusion Welders (SCV)": "https://0rganics.org/archipelago/sc2wol/Dual-FusionWelders.png", - "Fire-Suppression System (Building)": "https://0rganics.org/archipelago/sc2wol/Fire-SuppressionSystem.png", - "Orbital Command (Building)": "https://0rganics.org/archipelago/sc2wol/OrbitalCommandCampaign.png", - - "Marine": "https://static.wikia.nocookie.net/starcraft/images/4/47/Marine_SC2_Icon1.jpg", - "Medic": "https://static.wikia.nocookie.net/starcraft/images/7/74/Medic_SC2_Rend1.jpg", - "Firebat": "https://static.wikia.nocookie.net/starcraft/images/3/3c/Firebat_SC2_Rend1.jpg", - "Marauder": "https://static.wikia.nocookie.net/starcraft/images/b/ba/Marauder_SC2_Icon1.jpg", - "Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg", - - "Stimpack (Marine)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", - "Super Stimpack (Marine)": "/static/static/icons/sc2/superstimpack.png", - "Combat Shield (Marine)": "https://0rganics.org/archipelago/sc2wol/CombatShieldCampaign.png", - "Laser Targeting System (Marine)": "/static/static/icons/sc2/lasertargetingsystem.png", - "Magrail Munitions (Marine)": "/static/static/icons/sc2/magrailmunitions.png", - "Optimized Logistics (Marine)": "/static/static/icons/sc2/optimizedlogistics.png", - "Advanced Medic Facilities (Medic)": "https://0rganics.org/archipelago/sc2wol/AdvancedMedicFacilities.png", - "Stabilizer Medpacks (Medic)": "https://0rganics.org/archipelago/sc2wol/StabilizerMedpacks.png", - "Restoration (Medic)": "/static/static/icons/sc2/restoration.png", - "Optical Flare (Medic)": "/static/static/icons/sc2/opticalflare.png", - "Optimized Logistics (Medic)": "/static/static/icons/sc2/optimizedlogistics.png", - "Incinerator Gauntlets (Firebat)": "https://0rganics.org/archipelago/sc2wol/IncineratorGauntlets.png", - "Juggernaut Plating (Firebat)": "https://0rganics.org/archipelago/sc2wol/JuggernautPlating.png", - "Stimpack (Firebat)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", - "Super Stimpack (Firebat)": "/static/static/icons/sc2/superstimpack.png", - "Optimized Logistics (Firebat)": "/static/static/icons/sc2/optimizedlogistics.png", - "Concussive Shells (Marauder)": "https://0rganics.org/archipelago/sc2wol/ConcussiveShellsCampaign.png", - "Kinetic Foam (Marauder)": "https://0rganics.org/archipelago/sc2wol/KineticFoam.png", - "Stimpack (Marauder)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", - "Super Stimpack (Marauder)": "/static/static/icons/sc2/superstimpack.png", - "Laser Targeting System (Marauder)": "/static/static/icons/sc2/lasertargetingsystem.png", - "Magrail Munitions (Marauder)": "/static/static/icons/sc2/magrailmunitions.png", - "Internal Tech Module (Marauder)": "/static/static/icons/sc2/internalizedtechmodule.png", - "U-238 Rounds (Reaper)": "https://0rganics.org/archipelago/sc2wol/U-238Rounds.png", - "G-4 Clusterbomb (Reaper)": "https://0rganics.org/archipelago/sc2wol/G-4Clusterbomb.png", - "Stimpack (Reaper)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", - "Super Stimpack (Reaper)": "/static/static/icons/sc2/superstimpack.png", - "Laser Targeting System (Reaper)": "/static/static/icons/sc2/lasertargetingsystem.png", - "Advanced Cloaking Field (Reaper)": "/static/static/icons/sc2/terran-cloak-color.png", - "Spider Mines (Reaper)": "/static/static/icons/sc2/spidermine.png", - "Combat Drugs (Reaper)": "/static/static/icons/sc2/reapercombatdrugs.png", - - "Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg", - "Vulture": "https://static.wikia.nocookie.net/starcraft/images/d/da/Vulture_WoL.jpg", - "Goliath": "https://static.wikia.nocookie.net/starcraft/images/e/eb/Goliath_WoL.jpg", - "Diamondback": "https://static.wikia.nocookie.net/starcraft/images/a/a6/Diamondback_WoL.jpg", - "Siege Tank": "https://static.wikia.nocookie.net/starcraft/images/5/57/SiegeTank_SC2_Icon1.jpg", - - "Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png", - "Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png", - "Hellbat Aspect (Hellion)": "/static/static/icons/sc2/hellionbattlemode.png", - "Smart Servos (Hellion)": "/static/static/icons/sc2/transformationservos.png", - "Optimized Logistics (Hellion)": "/static/static/icons/sc2/optimizedlogistics.png", - "Jump Jets (Hellion)": "/static/static/icons/sc2/jumpjets.png", - "Stimpack (Hellion)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", - "Super Stimpack (Hellion)": "/static/static/icons/sc2/superstimpack.png", - "Cerberus Mine (Spider Mine)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png", - "High Explosive Munition (Spider Mine)": "/static/static/icons/sc2/high-explosive-spidermine.png", - "Replenishable Magazine (Vulture)": "https://0rganics.org/archipelago/sc2wol/ReplenishableMagazine.png", - "Ion Thrusters (Vulture)": "/static/static/icons/sc2/emergencythrusters.png", - "Auto Launchers (Vulture)": "/static/static/icons/sc2/jotunboosters.png", - "Multi-Lock Weapons System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Multi-LockWeaponsSystem.png", - "Ares-Class Targeting System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Ares-ClassTargetingSystem.png", - "Jump Jets (Goliath)": "/static/static/icons/sc2/jumpjets.png", - "Optimized Logistics (Goliath)": "/static/static/icons/sc2/optimizedlogistics.png", - "Tri-Lithium Power Cell (Diamondback)": "https://0rganics.org/archipelago/sc2wol/Tri-LithiumPowerCell.png", - "Shaped Hull (Diamondback)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png", - "Hyperfluxor (Diamondback)": "/static/static/icons/sc2/hyperfluxor.png", - "Burst Capacitors (Diamondback)": "/static/static/icons/sc2/burstcapacitors.png", - "Optimized Logistics (Diamondback)": "/static/static/icons/sc2/optimizedlogistics.png", - "Maelstrom Rounds (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/MaelstromRounds.png", - "Shaped Blast (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/ShapedBlast.png", - "Jump Jets (Siege Tank)": "/static/static/icons/sc2/jumpjets.png", - "Spider Mines (Siege Tank)": "/static/static/icons/sc2/siegetank-spidermines.png", - "Smart Servos (Siege Tank)": "/static/static/icons/sc2/transformationservos.png", - "Graduating Range (Siege Tank)": "/static/static/icons/sc2/siegetankrange.png", - "Laser Targeting System (Siege Tank)": "/static/static/icons/sc2/lasertargetingsystem.png", - "Advanced Siege Tech (Siege Tank)": "/static/static/icons/sc2/improvedsiegemode.png", - "Internal Tech Module (Siege Tank)": "/static/static/icons/sc2/internalizedtechmodule.png", - - "Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg", - "Wraith": "https://static.wikia.nocookie.net/starcraft/images/7/75/Wraith_WoL.jpg", - "Viking": "https://static.wikia.nocookie.net/starcraft/images/2/2a/Viking_SC2_Icon1.jpg", - "Banshee": "https://static.wikia.nocookie.net/starcraft/images/3/32/Banshee_SC2_Icon1.jpg", - "Battlecruiser": "https://static.wikia.nocookie.net/starcraft/images/f/f5/Battlecruiser_SC2_Icon1.jpg", - - "Rapid Deployment Tube (Medivac)": "https://0rganics.org/archipelago/sc2wol/RapidDeploymentTube.png", - "Advanced Healing AI (Medivac)": "https://0rganics.org/archipelago/sc2wol/AdvancedHealingAI.png", - "Expanded Hull (Medivac)": "/static/static/icons/sc2/neosteelfortifiedarmor.png", - "Afterburners (Medivac)": "/static/static/icons/sc2/medivacemergencythrusters.png", - "Tomahawk Power Cells (Wraith)": "https://0rganics.org/archipelago/sc2wol/TomahawkPowerCells.png", - "Displacement Field (Wraith)": "https://0rganics.org/archipelago/sc2wol/DisplacementField.png", - "Advanced Laser Technology (Wraith)": "/static/static/icons/sc2/improvedburstlaser.png", - "Ripwave Missiles (Viking)": "https://0rganics.org/archipelago/sc2wol/RipwaveMissiles.png", - "Phobos-Class Weapons System (Viking)": "https://0rganics.org/archipelago/sc2wol/Phobos-ClassWeaponsSystem.png", - "Smart Servos (Viking)": "/static/static/icons/sc2/transformationservos.png", - "Magrail Munitions (Viking)": "/static/static/icons/sc2/magrailmunitions.png", - "Cross-Spectrum Dampeners (Banshee)": "/static/static/icons/sc2/crossspectrumdampeners.png", - "Advanced Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png", - "Shockwave Missile Battery (Banshee)": "https://0rganics.org/archipelago/sc2wol/ShockwaveMissileBattery.png", - "Hyperflight Rotors (Banshee)": "/static/static/icons/sc2/hyperflightrotors.png", - "Laser Targeting System (Banshee)": "/static/static/icons/sc2/lasertargetingsystem.png", - "Internal Tech Module (Banshee)": "/static/static/icons/sc2/internalizedtechmodule.png", - "Missile Pods (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/MissilePods.png", - "Defensive Matrix (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png", - "Tactical Jump (Battlecruiser)": "/static/static/icons/sc2/warpjump.png", - "Cloak (Battlecruiser)": "/static/static/icons/sc2/terran-cloak-color.png", - "ATX Laser Battery (Battlecruiser)": "/static/static/icons/sc2/specialordance.png", - "Optimized Logistics (Battlecruiser)": "/static/static/icons/sc2/optimizedlogistics.png", - "Internal Tech Module (Battlecruiser)": "/static/static/icons/sc2/internalizedtechmodule.png", - - "Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg", - "Spectre": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Spectre_WoL.jpg", - "Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg", - - "Widow Mine": "/static/static/icons/sc2/widowmine.png", - "Cyclone": "/static/static/icons/sc2/cyclone.png", - "Liberator": "/static/static/icons/sc2/liberator.png", - "Valkyrie": "/static/static/icons/sc2/valkyrie.png", - - "Ocular Implants (Ghost)": "https://0rganics.org/archipelago/sc2wol/OcularImplants.png", - "Crius Suit (Ghost)": "https://0rganics.org/archipelago/sc2wol/CriusSuit.png", - "EMP Rounds (Ghost)": "/static/static/icons/sc2/terran-emp-color.png", - "Lockdown (Ghost)": "/static/static/icons/sc2/lockdown.png", - "Psionic Lash (Spectre)": "https://0rganics.org/archipelago/sc2wol/PsionicLash.png", - "Nyx-Class Cloaking Module (Spectre)": "https://0rganics.org/archipelago/sc2wol/Nyx-ClassCloakingModule.png", - "Impaler Rounds (Spectre)": "/static/static/icons/sc2/impalerrounds.png", - "330mm Barrage Cannon (Thor)": "https://0rganics.org/archipelago/sc2wol/330mmBarrageCannon.png", - "Immortality Protocol (Thor)": "https://0rganics.org/archipelago/sc2wol/ImmortalityProtocol.png", - "High Impact Payload (Thor)": "/static/static/icons/sc2/thorsiegemode.png", - "Smart Servos (Thor)": "/static/static/icons/sc2/transformationservos.png", - - "Optimized Logistics (Predator)": "/static/static/icons/sc2/optimizedlogistics.png", - "Drilling Claws (Widow Mine)": "/static/static/icons/sc2/drillingclaws.png", - "Concealment (Widow Mine)": "/static/static/icons/sc2/widowminehidden.png", - "Black Market Launchers (Widow Mine)": "/static/static/icons/sc2/widowmine-attackrange.png", - "Executioner Missiles (Widow Mine)": "/static/static/icons/sc2/widowmine-deathblossom.png", - "Mag-Field Accelerators (Cyclone)": "/static/static/icons/sc2/magfieldaccelerator.png", - "Mag-Field Launchers (Cyclone)": "/static/static/icons/sc2/cyclonerangeupgrade.png", - "Targeting Optics (Cyclone)": "/static/static/icons/sc2/targetingoptics.png", - "Rapid Fire Launchers (Cyclone)": "/static/static/icons/sc2/ripwavemissiles.png", - "Bio Mechanical Repair Drone (Raven)": "/static/static/icons/sc2/biomechanicaldrone.png", - "Spider Mines (Raven)": "/static/static/icons/sc2/siegetank-spidermines.png", - "Railgun Turret (Raven)": "/static/static/icons/sc2/autoturretblackops.png", - "Hunter-Seeker Weapon (Raven)": "/static/static/icons/sc2/specialordance.png", - "Interference Matrix (Raven)": "/static/static/icons/sc2/interferencematrix.png", - "Anti-Armor Missile (Raven)": "/static/static/icons/sc2/shreddermissile.png", - "Internal Tech Module (Raven)": "/static/static/icons/sc2/internalizedtechmodule.png", - "EMP Shockwave (Science Vessel)": "/static/static/icons/sc2/staticempblast.png", - "Defensive Matrix (Science Vessel)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png", - "Advanced Ballistics (Liberator)": "/static/static/icons/sc2/advanceballistics.png", - "Raid Artillery (Liberator)": "/static/static/icons/sc2/terrandefendermodestructureattack.png", - "Cloak (Liberator)": "/static/static/icons/sc2/terran-cloak-color.png", - "Laser Targeting System (Liberator)": "/static/static/icons/sc2/lasertargetingsystem.png", - "Optimized Logistics (Liberator)": "/static/static/icons/sc2/optimizedlogistics.png", - "Enhanced Cluster Launchers (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png", - "Shaped Hull (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png", - "Burst Lasers (Valkyrie)": "/static/static/icons/sc2/improvedburstlaser.png", - "Afterburners (Valkyrie)": "/static/static/icons/sc2/medivacemergencythrusters.png", - - "War Pigs": "https://static.wikia.nocookie.net/starcraft/images/e/ed/WarPigs_SC2_Icon1.jpg", - "Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg", - "Hammer Securities": "https://static.wikia.nocookie.net/starcraft/images/3/3b/HammerSecurity_SC2_Icon1.jpg", - "Spartan Company": "https://static.wikia.nocookie.net/starcraft/images/b/be/SpartanCompany_SC2_Icon1.jpg", - "Siege Breakers": "https://static.wikia.nocookie.net/starcraft/images/3/31/SiegeBreakers_SC2_Icon1.jpg", - "Hel's Angel": "https://static.wikia.nocookie.net/starcraft/images/6/63/HelsAngels_SC2_Icon1.jpg", - "Dusk Wings": "https://static.wikia.nocookie.net/starcraft/images/5/52/DuskWings_SC2_Icon1.jpg", - "Jackson's Revenge": "https://static.wikia.nocookie.net/starcraft/images/9/95/JacksonsRevenge_SC2_Icon1.jpg", - - "Ultra-Capacitors": "https://static.wikia.nocookie.net/starcraft/images/2/23/SC2_Lab_Ultra_Capacitors_Icon.png", - "Vanadium Plating": "https://static.wikia.nocookie.net/starcraft/images/6/67/SC2_Lab_VanPlating_Icon.png", - "Orbital Depots": "https://static.wikia.nocookie.net/starcraft/images/0/01/SC2_Lab_Orbital_Depot_Icon.png", - "Micro-Filtering": "https://static.wikia.nocookie.net/starcraft/images/2/20/SC2_Lab_MicroFilter_Icon.png", - "Automated Refinery": "https://static.wikia.nocookie.net/starcraft/images/7/71/SC2_Lab_Auto_Refinery_Icon.png", - "Command Center Reactor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/SC2_Lab_CC_Reactor_Icon.png", - "Raven": "https://static.wikia.nocookie.net/starcraft/images/1/19/SC2_Lab_Raven_Icon.png", - "Science Vessel": "https://static.wikia.nocookie.net/starcraft/images/c/c3/SC2_Lab_SciVes_Icon.png", - "Tech Reactor": "https://static.wikia.nocookie.net/starcraft/images/c/c5/SC2_Lab_Tech_Reactor_Icon.png", - "Orbital Strike": "https://static.wikia.nocookie.net/starcraft/images/d/df/SC2_Lab_Orb_Strike_Icon.png", - - "Shrike Turret (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png", - "Fortified Bunker (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png", - "Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png", - "Perdition Turret": "https://static.wikia.nocookie.net/starcraft/images/a/af/SC2_Lab_PerdTurret_Icon.png", - "Predator": "https://static.wikia.nocookie.net/starcraft/images/8/83/SC2_Lab_Predator_Icon.png", - "Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png", - "Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png", - "Regenerative Bio-Steel Level 1": "/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png", - "Regenerative Bio-Steel Level 2": "/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png", - "Hive Mind Emulator": "https://static.wikia.nocookie.net/starcraft/images/b/bc/SC2_Lab_Hive_Emulator_Icon.png", - "Psi Disrupter": "https://static.wikia.nocookie.net/starcraft/images/c/cf/SC2_Lab_Psi_Disruptor_Icon.png", - - "Zealot": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Icon_Protoss_Zealot.jpg", - "Stalker": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Icon_Protoss_Stalker.jpg", - "High Templar": "https://static.wikia.nocookie.net/starcraft/images/a/a0/Icon_Protoss_High_Templar.jpg", - "Dark Templar": "https://static.wikia.nocookie.net/starcraft/images/9/90/Icon_Protoss_Dark_Templar.jpg", - "Immortal": "https://static.wikia.nocookie.net/starcraft/images/c/c1/Icon_Protoss_Immortal.jpg", - "Colossus": "https://static.wikia.nocookie.net/starcraft/images/4/40/Icon_Protoss_Colossus.jpg", - "Phoenix": "https://static.wikia.nocookie.net/starcraft/images/b/b1/Icon_Protoss_Phoenix.jpg", - "Void Ray": "https://static.wikia.nocookie.net/starcraft/images/1/1d/VoidRay_SC2_Rend1.jpg", - "Carrier": "https://static.wikia.nocookie.net/starcraft/images/2/2c/Icon_Protoss_Carrier.jpg", - - "Nothing": "", - } - sc2wol_location_ids = { - "Liberation Day": range(SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 200), - "The Outlaws": range(SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 300), - "Zero Hour": range(SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 400), - "Evacuation": range(SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 500), - "Outbreak": range(SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 600), - "Safe Haven": range(SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 700), - "Haven's Fall": range(SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 800), - "Smash and Grab": range(SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 900), - "The Dig": range(SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 1000), - "The Moebius Factor": range(SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1100), - "Supernova": range(SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1200), - "Maw of the Void": range(SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1300), - "Devil's Playground": range(SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1400), - "Welcome to the Jungle": range(SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1500), - "Breakout": range(SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1600), - "Ghost of a Chance": range(SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1700), - "The Great Train Robbery": range(SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1800), - "Cutthroat": range(SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1900), - "Engine of Destruction": range(SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 2000), - "Media Blitz": range(SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2100), - "Piercing the Shroud": range(SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2200), - "Whispers of Doom": range(SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2300), - "A Sinister Turn": range(SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2400), - "Echoes of the Future": range(SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2500), - "In Utter Darkness": range(SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2600), - "Gates of Hell": range(SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2700), - "Belly of the Beast": range(SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2800), - "Shatter the Sky": range(SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2900), - } + player_checks_in_area = { + (team, player): { + area_name: len(tracker_data._multidata["checks_in_area"][player][area_name]) + if area_name != "Total" else tracker_data._multidata["checks_in_area"][player]["Total"] + for area_name in ordered_areas + } + for team, players in tracker_data.get_team_players().items() + for player in players + if tracker_data.get_slot_info(team, player).type != SlotType.group and + tracker_data.get_slot_info(team, player).game == "A Link to the Past" + } - display_data = {} + tracking_ids = [] + for item in tracking_names: + tracking_ids.append(alttp_id_lookup[item]) + + # Can't wait to get this into the apworld. Oof. + from worlds.alttp import Items + + small_key_ids = {} + big_key_ids = {} + ids_small_key = {} + ids_big_key = {} + for item_name, data in Items.item_table.items(): + if "Key" in item_name: + area = item_name.split("(")[1][:-1] + if "Small" in item_name: + small_key_ids[area] = data[2] + ids_small_key[data[2]] = area + else: + big_key_ids[area] = data[2] + ids_big_key[data[2]] = area - # Grouped Items - grouped_item_ids = { - "Progressive Weapon Upgrade": 107 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Armor Upgrade": 108 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Infantry Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Vehicle Upgrade": 110 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Ship Upgrade": 111 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Weapon/Armor Upgrade": 112 + SC2WOL_ITEM_ID_OFFSET - } - grouped_item_replacements = { - "Progressive Weapon Upgrade": ["Progressive Infantry Weapon", "Progressive Vehicle Weapon", "Progressive Ship Weapon"], - "Progressive Armor Upgrade": ["Progressive Infantry Armor", "Progressive Vehicle Armor", "Progressive Ship Armor"], - "Progressive Infantry Upgrade": ["Progressive Infantry Weapon", "Progressive Infantry Armor"], - "Progressive Vehicle Upgrade": ["Progressive Vehicle Weapon", "Progressive Vehicle Armor"], - "Progressive Ship Upgrade": ["Progressive Ship Weapon", "Progressive Ship Armor"] - } - grouped_item_replacements["Progressive Weapon/Armor Upgrade"] = grouped_item_replacements["Progressive Weapon Upgrade"] + grouped_item_replacements["Progressive Armor Upgrade"] - replacement_item_ids = { - "Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET, - } - for grouped_item_name, grouped_item_id in grouped_item_ids.items(): - count: int = inventory[grouped_item_id] - if count > 0: - for replacement_item in grouped_item_replacements[grouped_item_name]: - replacement_id: int = replacement_item_ids[replacement_item] - inventory[replacement_id] = count - - # Determine display for progressive items - progressive_items = { - "Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Marine)": 208 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Firebat)": 226 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Marauder)": 228 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Reaper)": 250 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Hellion)": 259 + SC2WOL_ITEM_ID_OFFSET, - "Progressive High Impact Payload (Thor)": 361 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Cross-Spectrum Dampeners (Banshee)": 316 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Regenerative Bio-Steel": 617 + SC2WOL_ITEM_ID_OFFSET - } - progressive_names = { - "Progressive Infantry Weapon": ["Infantry Weapons Level 1", "Infantry Weapons Level 1", "Infantry Weapons Level 2", "Infantry Weapons Level 3"], - "Progressive Infantry Armor": ["Infantry Armor Level 1", "Infantry Armor Level 1", "Infantry Armor Level 2", "Infantry Armor Level 3"], - "Progressive Vehicle Weapon": ["Vehicle Weapons Level 1", "Vehicle Weapons Level 1", "Vehicle Weapons Level 2", "Vehicle Weapons Level 3"], - "Progressive Vehicle Armor": ["Vehicle Armor Level 1", "Vehicle Armor Level 1", "Vehicle Armor Level 2", "Vehicle Armor Level 3"], - "Progressive Ship Weapon": ["Ship Weapons Level 1", "Ship Weapons Level 1", "Ship Weapons Level 2", "Ship Weapons Level 3"], - "Progressive Ship Armor": ["Ship Armor Level 1", "Ship Armor Level 1", "Ship Armor Level 2", "Ship Armor Level 3"], - "Progressive Stimpack (Marine)": ["Stimpack (Marine)", "Stimpack (Marine)", "Super Stimpack (Marine)"], - "Progressive Stimpack (Firebat)": ["Stimpack (Firebat)", "Stimpack (Firebat)", "Super Stimpack (Firebat)"], - "Progressive Stimpack (Marauder)": ["Stimpack (Marauder)", "Stimpack (Marauder)", "Super Stimpack (Marauder)"], - "Progressive Stimpack (Reaper)": ["Stimpack (Reaper)", "Stimpack (Reaper)", "Super Stimpack (Reaper)"], - "Progressive Stimpack (Hellion)": ["Stimpack (Hellion)", "Stimpack (Hellion)", "Super Stimpack (Hellion)"], - "Progressive High Impact Payload (Thor)": ["High Impact Payload (Thor)", "High Impact Payload (Thor)", "Smart Servos (Thor)"], - "Progressive Cross-Spectrum Dampeners (Banshee)": ["Cross-Spectrum Dampeners (Banshee)", "Cross-Spectrum Dampeners (Banshee)", "Advanced Cross-Spectrum Dampeners (Banshee)"], - "Progressive Regenerative Bio-Steel": ["Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 2"] - } - for item_name, item_id in progressive_items.items(): - level = min(inventory[item_id], len(progressive_names[item_name]) - 1) - display_name = progressive_names[item_name][level] - base_name = (item_name.split(maxsplit=1)[1].lower() - .replace(' ', '_') - .replace("-", "") - .replace("(", "") - .replace(")", "")) - display_data[base_name + "_level"] = level - display_data[base_name + "_url"] = icons[display_name] - display_data[base_name + "_name"] = display_name - - # Multi-items - multi_items = { - "+15 Starting Minerals": 800 + SC2WOL_ITEM_ID_OFFSET, - "+15 Starting Vespene": 801 + SC2WOL_ITEM_ID_OFFSET, - "+2 Starting Supply": 802 + SC2WOL_ITEM_ID_OFFSET - } - for item_name, item_id in multi_items.items(): - base_name = item_name.split()[-1].lower() - count = inventory[item_id] - if base_name == "supply": - count = count * 2 - display_data[base_name + "_count"] = count - else: - count = count * 15 - display_data[base_name + "_count"] = count + def _get_location_table(checks_table: dict) -> dict: + loc_to_area = {} + for area, locations in checks_table.items(): + if area == "Total": + continue + for location in locations: + loc_to_area[location] = area + return loc_to_area + + player_location_to_area = { + (team, player): _get_location_table(tracker_data._multidata["checks_in_area"][player]) + for team, players in tracker_data.get_team_players().items() + for player in players + if tracker_data.get_slot_info(team, player).type != SlotType.group and + tracker_data.get_slot_info(team, player).game == "A Link to the Past" + } - # Victory condition - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - display_data['game_finished'] = game_state == 30 - - # Turn location IDs into mission objective counts - checked_locations = multisave.get("location_checks", {}).get((team, player), set()) - lookup_name = lambda id: lookup_any_location_id_to_name[id] - location_info = {mission_name: {lookup_name(id): (id in checked_locations) for id in mission_locations if id in set(locations[player])} for mission_name, mission_locations in sc2wol_location_ids.items()} - checks_done = {mission_name: len([id for id in mission_locations if id in checked_locations and id in set(locations[player])]) for mission_name, mission_locations in sc2wol_location_ids.items()} - checks_done['Total'] = len(checked_locations) - checks_in_area = {mission_name: len([id for id in mission_locations if id in set(locations[player])]) for mission_name, mission_locations in sc2wol_location_ids.items()} - checks_in_area['Total'] = sum(checks_in_area.values()) - - return render_template("sc2wolTracker.html", - inventory=inventory, icons=icons, - acquired_items={lookup_any_item_id_to_name[id] for id in inventory if - id in lookup_any_item_id_to_name}, - player=player, team=team, room=room, player_name=playerName, - checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, - **display_data) - -def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, saving_second: int) -> str: - - icons = { - "Checks Available": "https://0rganics.org/archipelago/cf/spr_tiles_3.png", - "Map Width": "https://0rganics.org/archipelago/cf/spr_tiles_4.png", - "Map Height": "https://0rganics.org/archipelago/cf/spr_tiles_5.png", - "Map Bombs": "https://0rganics.org/archipelago/cf/spr_tiles_6.png", - - "Nothing": "", - } + checks_done: Dict[TeamPlayer, Dict[str: int]] = { + (team, player): {location_name: 0 for location_name in default_locations} + for team, players in tracker_data.get_team_players().items() + for player in players + if tracker_data.get_slot_info(team, player).type != SlotType.group and + tracker_data.get_slot_info(team, player).game == "A Link to the Past" + } - checksfinder_location_ids = { - "Tile 1": 81000, - "Tile 2": 81001, - "Tile 3": 81002, - "Tile 4": 81003, - "Tile 5": 81004, - "Tile 6": 81005, - "Tile 7": 81006, - "Tile 8": 81007, - "Tile 9": 81008, - "Tile 10": 81009, - "Tile 11": 81010, - "Tile 12": 81011, - "Tile 13": 81012, - "Tile 14": 81013, - "Tile 15": 81014, - "Tile 16": 81015, - "Tile 17": 81016, - "Tile 18": 81017, - "Tile 19": 81018, - "Tile 20": 81019, - "Tile 21": 81020, - "Tile 22": 81021, - "Tile 23": 81022, - "Tile 24": 81023, - "Tile 25": 81024, - } + inventories: Dict[TeamPlayer, Dict[int, int]] = {} + player_big_key_locations = {(player): set() for player in tracker_data.get_team_players()[0]} + player_small_key_locations = {player: set() for player in tracker_data.get_team_players()[0]} + group_big_key_locations = set() + group_key_locations = set() + + for (team, player), locations in checks_done.items(): + # Check if game complete. + if tracker_data.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL: + inventories[team, player][106] = 1 # Triforce + + # Count number of locations checked. + for location in tracker_data.get_player_checked_locations(team, player): + checks_done[team, player][player_location_to_area[team, player][location]] += 1 + checks_done[team, player]["Total"] += 1 + + # Count keys. + for location, (item, receiving, _) in tracker_data.get_player_locations(team, player).items(): + if item in ids_big_key: + player_big_key_locations[receiving].add(ids_big_key[item]) + elif item in ids_small_key: + player_small_key_locations[receiving].add(ids_small_key[item]) + + # Iterate over received items and build inventory/key counts. + inventories[team, player] = collections.Counter() + for network_item in tracker_data.get_player_received_items(team, player): + target_item = links.get(network_item.item, network_item.item) + if network_item.item in levels: # non-progressive + inventories[team, player][target_item] = (max(inventories[team, player][target_item], levels[network_item.item])) + else: + inventories[team, player][target_item] += 1 + + group_key_locations |= player_small_key_locations[player] + group_big_key_locations |= player_big_key_locations[player] + + return render_template( + "multitracker__ALinkToThePast.html", + enabled_trackers=enabled_trackers, + current_tracker="A Link to the Past", + room=tracker_data.room, + room_players=tracker_data.get_team_players(), + locations=tracker_data.get_room_locations(), + locations_complete=tracker_data.get_room_locations_complete(), + total_team_locations=tracker_data.get_team_locations_total_count(), + total_team_locations_complete=tracker_data.get_team_locations_checked_count(), + player_names_with_alias=tracker_data.get_room_long_player_names(), + completed_worlds=tracker_data.get_team_completed_worlds_count(), + games=tracker_data.get_room_games(), + states=tracker_data.get_room_client_statuses(), + hints=tracker_data.get_team_hints(), + activity_timers=tracker_data.get_room_last_activity(), + videos=tracker_data.get_room_videos(), + item_id_to_name=tracker_data.item_id_to_name, + location_id_to_name=tracker_data.location_id_to_name, + inventories=inventories, + tracking_names=tracking_names, + tracking_ids=tracking_ids, + multi_items=multi_items, + checks_done=checks_done, + ordered_areas=ordered_areas, + checks_in_area=player_checks_in_area, + key_locations=group_key_locations, + big_key_locations=group_big_key_locations, + small_key_ids=small_key_ids, + big_key_ids=big_key_ids, + ) - display_data = {} + def render_ALinkToThePast_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + # Helper objects. + alttp_id_lookup = tracker_data.item_name_to_id["A Link to the Past"] + + links = { + "Bow": "Progressive Bow", + "Silver Arrows": "Progressive Bow", + "Silver Bow": "Progressive Bow", + "Progressive Bow (Alt)": "Progressive Bow", + "Bottle (Red Potion)": "Bottle", + "Bottle (Green Potion)": "Bottle", + "Bottle (Blue Potion)": "Bottle", + "Bottle (Fairy)": "Bottle", + "Bottle (Bee)": "Bottle", + "Bottle (Good Bee)": "Bottle", + "Fighter Sword": "Progressive Sword", + "Master Sword": "Progressive Sword", + "Tempered Sword": "Progressive Sword", + "Golden Sword": "Progressive Sword", + "Power Glove": "Progressive Glove", + "Titans Mitts": "Progressive Glove", + } + links = {alttp_id_lookup[key]: alttp_id_lookup[value] for key, value in links.items()} + levels = { + "Fighter Sword": 1, + "Master Sword": 2, + "Tempered Sword": 3, + "Golden Sword": 4, + "Power Glove": 1, + "Titans Mitts": 2, + "Bow": 1, + "Silver Bow": 2, + "Triforce Piece": 90, + } + tracking_names = [ + "Progressive Sword", "Progressive Bow", "Book of Mudora", "Hammer", "Hookshot", "Magic Mirror", "Flute", + "Pegasus Boots", "Progressive Glove", "Flippers", "Moon Pearl", "Blue Boomerang", "Red Boomerang", + "Bug Catching Net", "Cape", "Shovel", "Lamp", "Mushroom", "Magic Powder", "Cane of Somaria", + "Cane of Byrna", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", "Bottle", "Triforce Piece", "Triforce", + ] + default_locations = { + "Light World": { + 1572864, 1572865, 60034, 1572867, 1572868, 60037, 1572869, 1572866, 60040, 59788, 60046, 60175, + 1572880, 60049, 60178, 1572883, 60052, 60181, 1572885, 60055, 60184, 191256, 60058, 60187, 1572884, + 1572886, 1572887, 1572906, 60202, 60205, 59824, 166320, 1010170, 60208, 60211, 60214, 60217, 59836, + 60220, 60223, 59839, 1573184, 60226, 975299, 1573188, 1573189, 188229, 60229, 60232, 1573193, + 1573194, 60235, 1573187, 59845, 59854, 211407, 60238, 59857, 1573185, 1573186, 1572882, 212328, + 59881, 59761, 59890, 59770, 193020, 212605 + }, + "Dark World": { + 59776, 59779, 975237, 1572870, 60043, 1572881, 60190, 60193, 60196, 60199, 60840, 1573190, 209095, + 1573192, 1573191, 60241, 60244, 60247, 60250, 59884, 59887, 60019, 60022, 60028, 60031 + }, + "Desert Palace": {1573216, 59842, 59851, 59791, 1573201, 59830}, + "Eastern Palace": {1573200, 59827, 59893, 59767, 59833, 59773}, + "Hyrule Castle": {60256, 60259, 60169, 60172, 59758, 59764, 60025, 60253}, + "Agahnims Tower": {60082, 60085}, + "Tower of Hera": {1573218, 59878, 59821, 1573202, 59896, 59899}, + "Swamp Palace": {60064, 60067, 60070, 59782, 59785, 60073, 60076, 60079, 1573204, 60061}, + "Thieves Town": {59905, 59908, 59911, 59914, 59917, 59920, 59923, 1573206}, + "Skull Woods": {59809, 59902, 59848, 59794, 1573205, 59800, 59803, 59806}, + "Ice Palace": {59872, 59875, 59812, 59818, 59860, 59797, 1573207, 59869}, + "Misery Mire": {60001, 60004, 60007, 60010, 60013, 1573208, 59866, 59998}, + "Turtle Rock": {59938, 59941, 59944, 1573209, 59947, 59950, 59953, 59956, 59926, 59929, 59932, 59935}, + "Palace of Darkness": { + 59968, 59971, 59974, 59977, 59980, 59983, 59986, 1573203, 59989, 59959, 59992, 59962, 59995, + 59965 + }, + "Ganons Tower": { + 60160, 60163, 60166, 60088, 60091, 60094, 60097, 60100, 60103, 60106, 60109, 60112, 60115, 60118, + 60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157 + }, + "Total": set() + } + key_only_locations = { + "Light World": set(), + "Dark World": set(), + "Desert Palace": {0x140031, 0x14002b, 0x140061, 0x140028}, + "Eastern Palace": {0x14005b, 0x140049}, + "Hyrule Castle": {0x140037, 0x140034, 0x14000d, 0x14003d}, + "Agahnims Tower": {0x140061, 0x140052}, + "Tower of Hera": set(), + "Swamp Palace": {0x140019, 0x140016, 0x140013, 0x140010, 0x14000a}, + "Thieves Town": {0x14005e, 0x14004f}, + "Skull Woods": {0x14002e, 0x14001c}, + "Ice Palace": {0x140004, 0x140022, 0x140025, 0x140046}, + "Misery Mire": {0x140055, 0x14004c, 0x140064}, + "Turtle Rock": {0x140058, 0x140007}, + "Palace of Darkness": set(), + "Ganons Tower": {0x140040, 0x140043, 0x14003a, 0x14001f}, + "Total": set() + } + location_to_area = {} + for area, locations in default_locations.items(): + for checked_location in locations: + location_to_area[checked_location] = area + for area, locations in key_only_locations.items(): + for checked_location in locations: + location_to_area[checked_location] = area + + checks_in_area = {area: len(checks) for area, checks in default_locations.items()} + checks_in_area["Total"] = 216 + ordered_areas = ( + "Light World", "Dark World", "Hyrule Castle", "Agahnims Tower", "Eastern Palace", "Desert Palace", + "Tower of Hera", "Palace of Darkness", "Swamp Palace", "Skull Woods", "Thieves Town", "Ice Palace", + "Misery Mire", "Turtle Rock", "Ganons Tower", "Total" + ) - # Multi-items - multi_items = { - "Map Width": 80000, - "Map Height": 80001, - "Map Bombs": 80002 - } - for item_name, item_id in multi_items.items(): - base_name = item_name.split()[-1].lower() - count = inventory[item_id] - display_data[base_name + "_count"] = count - display_data[base_name + "_display"] = count + 5 - - # Get location info - checked_locations = multisave.get("location_checks", {}).get((team, player), set()) - lookup_name = lambda id: lookup_any_location_id_to_name[id] - location_info = {tile_name: {lookup_name(tile_location): (tile_location in checked_locations)} for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in set(locations[player])} - checks_done = {tile_name: len([tile_location]) for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in checked_locations and tile_location in set(locations[player])} - checks_done['Total'] = len(checked_locations) - checks_in_area = checks_done - - # Calculate checks available - display_data["checks_unlocked"] = min(display_data["width_count"] + display_data["height_count"] + display_data["bombs_count"] + 5, 25) - display_data["checks_available"] = max(display_data["checks_unlocked"] - len(checked_locations), 0) - - # Victory condition - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - display_data['game_finished'] = game_state == 30 - - return render_template("checksfinderTracker.html", - inventory=inventory, icons=icons, - acquired_items={lookup_any_item_id_to_name[id] for id in inventory if - id in lookup_any_item_id_to_name}, - player=player, team=team, room=room, player_name=playerName, - checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, - **display_data) - -def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], - saving_second: int, custom_locations: Dict[int, str], custom_items: Dict[int, str]) -> str: - - checked_locations = multisave.get("location_checks", {}).get((team, player), set()) - player_received_items = {} - if multisave.get('version', 0) > 0: - ordered_items = multisave.get('received_items', {}).get((team, player, True), []) - else: - ordered_items = multisave.get('received_items', {}).get((team, player), []) - - # add numbering to all items but starter_inventory - for order_index, networkItem in enumerate(ordered_items, start=1): - player_received_items[networkItem.item] = order_index - - return render_template("genericTracker.html", - inventory=inventory, - player=player, team=team, room=room, player_name=playerName, - checked_locations=checked_locations, - not_checked_locations=set(locations[player]) - checked_locations, - received_items=player_received_items, saving_second=saving_second, - custom_items=custom_items, custom_locations=custom_locations) - - -def get_enabled_multiworld_trackers(room: Room, current: str): - enabled = [ - { - "name": "Generic", - "endpoint": "get_multiworld_tracker", - "current": current == "Generic" - } - ] - for game_name, endpoint in multi_trackers.items(): - if any(slot.game == game_name for slot in room.seed.slots) or current == game_name: - enabled.append({ - "name": game_name, - "endpoint": endpoint.__name__, - "current": current == game_name} - ) - return enabled - - -def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]: - room: Room = Room.get(tracker=tracker) - if not room: - return None + tracking_ids = [] + for item in tracking_names: + tracking_ids.append(alttp_id_lookup[item]) + + # Can't wait to get this into the apworld. Oof. + from worlds.alttp import Items + + small_key_ids = {} + big_key_ids = {} + ids_small_key = {} + ids_big_key = {} + for item_name, data in Items.item_table.items(): + if "Key" in item_name: + area = item_name.split("(")[1][:-1] + if "Small" in item_name: + small_key_ids[area] = data[2] + ids_small_key[data[2]] = area + else: + big_key_ids[area] = data[2] + ids_big_key[data[2]] = area + + inventory = collections.Counter() + checks_done = {loc_name: 0 for loc_name in default_locations} + player_big_key_locations = set() + player_small_key_locations = set() + + player_locations = tracker_data.get_player_locations(team, player) + for checked_location in tracker_data.get_player_checked_locations(team, player): + if checked_location in player_locations: + area_name = location_to_area.get(checked_location, None) + if area_name: + checks_done[area_name] += 1 + + checks_done["Total"] += 1 + + for received_item in tracker_data.get_player_received_items(team, player): + target_item = links.get(received_item.item, received_item.item) + if received_item.item in levels: # non-progressive + inventory[target_item] = max(inventory[target_item], levels[received_item.item]) + else: + inventory[target_item] += 1 + + for location, (item_id, _, _) in player_locations.items(): + if item_id in ids_big_key: + player_big_key_locations.add(ids_big_key[item_id]) + elif item_id in ids_small_key: + player_small_key_locations.add(ids_small_key[item_id]) + + # Note the presence of the triforce item + if tracker_data.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL: + inventory[106] = 1 # Triforce + + # Progressive items need special handling for icons and class + progressive_items = { + "Progressive Sword": 94, + "Progressive Glove": 97, + "Progressive Bow": 100, + "Progressive Mail": 96, + "Progressive Shield": 95, + } + progressive_names = { + "Progressive Sword": [None, "Fighter Sword", "Master Sword", "Tempered Sword", "Golden Sword"], + "Progressive Glove": [None, "Power Glove", "Titan Mitts"], + "Progressive Bow": [None, "Bow", "Silver Bow"], + "Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"], + "Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"] + } - locations, names, use_door_tracker, checks_in_area, player_location_to_area, \ - precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ - get_static_room_data(room) + # Determine which icon to use + display_data = {} + for item_name, item_id in progressive_items.items(): + level = min(inventory[item_id], len(progressive_names[item_name]) - 1) + display_name = progressive_names[item_name][level] + acquired = True + if not display_name: + acquired = False + display_name = progressive_names[item_name][level + 1] + base_name = item_name.split(maxsplit=1)[1].lower() + display_data[base_name + "_acquired"] = acquired + display_data[base_name + "_icon"] = display_name + + # The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should? + sp_areas = ordered_areas[2:15] + + return render_template( + template_name_or_list="tracker__ALinkToThePast.html", + room=tracker_data.room, + team=team, + player=player, + inventory=inventory, + player_name=tracker_data.get_player_name(team, player), + checks_done=checks_done, + checks_in_area=checks_in_area, + acquired_items={tracker_data.item_id_to_name["A Link to the Past"][id] for id in inventory}, + sp_areas=sp_areas, + small_key_ids=small_key_ids, + key_locations=player_small_key_locations, + big_key_ids=big_key_ids, + big_key_locations=player_big_key_locations, + **display_data, + ) - checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations} - for playernumber in range(1, len(team) + 1) if playernumber not in groups} - for teamnumber, team in enumerate(names)} + _multiworld_trackers["A Link to the Past"] = render_ALinkToThePast_multiworld_tracker + _player_trackers["A Link to the Past"] = render_ALinkToThePast_tracker + +if "Minecraft" in network_data_package["games"]: + def render_Minecraft_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + icons = { + "Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png", + "Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png", + "Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png", + "Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png", + "Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png", + "Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png", + "Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png", + "Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png", + "Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png", + "Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png", + "Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png", + "Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png", + "Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png", + "Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png", + "Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png", + "Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png", + "Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png", + "Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png", + "Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png", + "Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png", + "Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png", + "Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif", + "Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png", + "Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif", + "Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png", + "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png", + "Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png", + "Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png", + "Saddle": "https://i.imgur.com/2QtDyR0.png", + "Channeling Book": "https://i.imgur.com/J3WsYZw.png", + "Silk Touch Book": "https://i.imgur.com/iqERxHQ.png", + "Piercing IV Book": "https://i.imgur.com/OzJptGz.png", + } - percent_total_checks_done = {teamnumber: {playernumber: 0 - for playernumber in range(1, len(team) + 1) if playernumber not in groups} - for teamnumber, team in enumerate(names)} + minecraft_location_ids = { + "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070, + 42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077], + "Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021, + 42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014], + "The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046], + "Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42109, 42035, 42016, 42020, + 42048, 42054, 42068, 42043, 42106, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42105, + 42099, 42103, 42110, 42100], + "Husbandry": [42065, 42067, 42078, 42022, 42113, 42107, 42007, 42079, 42013, 42028, 42036, 42108, 42111, + 42112, + 42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095], + "Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091], + } - total_locations = {teamnumber: sum(len(locations[playernumber]) - for playernumber in range(1, len(team) + 1) if playernumber not in groups) - for teamnumber, team in enumerate(names)} + display_data = {} - hints = {team: set() for team in range(len(names))} - if room.multisave: - multisave = restricted_loads(room.multisave) - else: - multisave = {} - if "hints" in multisave: - for (team, slot), slot_hints in multisave["hints"].items(): - hints[team] |= set(slot_hints) - - for (team, player), locations_checked in multisave.get("location_checks", {}).items(): - if player in groups: - continue - player_locations = locations[player] - checks_done[team][player]["Total"] = len(locations_checked) - percent_total_checks_done[team][player] = ( - checks_done[team][player]["Total"] / len(player_locations) * 100 - if player_locations - else 100 + # Determine display for progressive items + progressive_items = { + "Progressive Tools": 45013, + "Progressive Weapons": 45012, + "Progressive Armor": 45014, + "Progressive Resource Crafting": 45001 + } + progressive_names = { + "Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"], + "Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"], + "Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"], + "Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"] + } + + inventory = tracker_data.get_player_inventory_counts(team, player) + for item_name, item_id in progressive_items.items(): + level = min(inventory[item_id], len(progressive_names[item_name]) - 1) + display_name = progressive_names[item_name][level] + base_name = item_name.split(maxsplit=1)[1].lower().replace(" ", "_") + display_data[base_name + "_url"] = icons[display_name] + + # Multi-items + multi_items = { + "3 Ender Pearls": 45029, + "8 Netherite Scrap": 45015, + "Dragon Egg Shard": 45043 + } + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[-1].lower() + count = inventory[item_id] + if count >= 0: + display_data[base_name + "_count"] = count + + # Victory condition + game_state = tracker_data.get_player_client_status(team, player) + display_data["game_finished"] = game_state == 30 + + # Turn location IDs into advancement tab counts + checked_locations = tracker_data.get_player_checked_locations(team, player) + lookup_name = lambda id: tracker_data.location_id_to_name["Minecraft"][id] + location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} + for tab_name, tab_locations in minecraft_location_ids.items()} + checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) + for tab_name, tab_locations in minecraft_location_ids.items()} + checks_done["Total"] = len(checked_locations) + checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()} + checks_in_area["Total"] = sum(checks_in_area.values()) + + lookup_any_item_id_to_name = tracker_data.item_id_to_name["Minecraft"] + return render_template( + "tracker__Minecraft.html", + inventory=inventory, + icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0}, + player=player, + team=team, + room=tracker_data.room, + player_name=tracker_data.get_player_name(team, player), + saving_second=tracker_data.get_room_saving_second(), + checks_done=checks_done, + checks_in_area=checks_in_area, + location_info=location_info, + **display_data, ) - activity_timers = {} - now = datetime.datetime.utcnow() - for (team, player), timestamp in multisave.get("client_activity_timers", []): - activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) - - player_names = {} - completed_worlds = 0 - states: typing.Dict[typing.Tuple[int, int], int] = {} - for team, names in enumerate(names): - for player, name in enumerate(names, 1): - player_names[team, player] = name - states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0) - if states[team, player] == ClientStatus.CLIENT_GOAL and player not in groups: - completed_worlds += 1 - long_player_names = player_names.copy() - for (team, player), alias in multisave.get("name_aliases", {}).items(): - player_names[team, player] = alias - long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})" - - video = {} - for (team, player), data in multisave.get("video", []): - video[team, player] = data - - return dict( - player_names=player_names, room=room, checks_done=checks_done, - percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area, - activity_timers=activity_timers, video=video, hints=hints, - long_player_names=long_player_names, - multisave=multisave, precollected_items=precollected_items, groups=groups, - locations=locations, total_locations=total_locations, games=games, states=states, - completed_worlds=completed_worlds, - custom_locations=custom_locations, custom_items=custom_items, - ) + _player_trackers["Minecraft"] = render_Minecraft_tracker + +if "Ocarina of Time" in network_data_package["games"]: + def render_OcarinaOfTime_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + icons = { + "Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png", + "Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png", + "Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png", + "Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png", + "Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png", + "Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png", + "Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png", + "Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png", + "Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png", + "Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png", + "Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png", + "Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png", + "Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png", + "Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png", + "Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png", + "Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png", + "Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png", + "Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png", + "Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png", + "Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png", + "Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png", + "Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png", + "Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png", + "Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png", + "Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png", + "Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png", + "Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png", + "Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png", + "Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png", + "Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png", + "Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png", + "Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png", + "Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png", + "Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png", + "Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png", + "Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png", + "Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png", + "Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png", + "Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png", + "Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png", + "Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png", + "Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png", + "Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png", + "Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png", + "Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png", + "Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png", + "Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png", + } + display_data = {} -def _get_inventory_data(data: typing.Dict[str, typing.Any]) \ - -> typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]]: - inventory: typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]] = { - teamnumber: {playernumber: collections.Counter() for playernumber in team_data} - for teamnumber, team_data in data["checks_done"].items() - } + # Determine display for progressive items + progressive_items = { + "Progressive Hookshot": 66128, + "Progressive Strength Upgrade": 66129, + "Progressive Wallet": 66133, + "Progressive Scale": 66134, + "Magic Meter": 66138, + "Ocarina": 66139, + } - groups = data["groups"] - - for (team, player), locations_checked in data["multisave"].get("location_checks", {}).items(): - if player in data["groups"]: - continue - player_locations = data["locations"][player] - precollected = data["precollected_items"][player] - for item_id in precollected: - inventory[team][player][item_id] += 1 - for location in locations_checked: - item_id, recipient, flags = player_locations[location] - recipients = groups.get(recipient, [recipient]) - for recipient in recipients: - inventory[team][recipient][item_id] += 1 - return inventory - - -def _get_named_inventory(inventory: typing.Dict[int, int], custom_items: typing.Dict[int, str] = None) \ - -> typing.Dict[str, int]: - """slow""" - if custom_items: - mapping = collections.ChainMap(custom_items, lookup_any_item_id_to_name) - else: - mapping = lookup_any_item_id_to_name + progressive_names = { + "Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"], + "Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets", + "Golden Gauntlets"], + "Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"], + "Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"], + "Magic Meter": ["Small Magic", "Small Magic", "Large Magic"], + "Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"] + } - return collections.Counter({mapping.get(item_id, None): count for item_id, count in inventory.items()}) + inventory = tracker_data.get_player_inventory_counts(team, player) + for item_name, item_id in progressive_items.items(): + level = min(inventory[item_id], len(progressive_names[item_name]) - 1) + display_name = progressive_names[item_name][level] + if item_name.startswith("Progressive"): + base_name = item_name.split(maxsplit=1)[1].lower().replace(" ", "_") + else: + base_name = item_name.lower().replace(" ", "_") + display_data[base_name + "_url"] = icons[display_name] + + if base_name == "hookshot": + display_data["hookshot_length"] = {0: "", 1: "H", 2: "L"}.get(level) + if base_name == "wallet": + display_data["wallet_size"] = {0: "99", 1: "200", 2: "500", 3: "999"}.get(level) + + # Determine display for bottles. Show letter if it's obtained, determine bottle count + bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148] + display_data["bottle_count"] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4) + display_data["bottle_url"] = icons["Rutos Letter"] if inventory[66021] > 0 else icons["Bottle"] + + # Determine bombchu display + display_data["has_bombchus"] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137])) + + # Multi-items + multi_items = { + "Gold Skulltula Token": 66091, + "Triforce Piece": 66202, + } + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[-1].lower() + display_data[base_name + "_count"] = inventory[item_id] + + # Gather dungeon locations + area_id_ranges = { + "Overworld": ((67000, 67263), (67269, 67280), (67747, 68024), (68054, 68062)), + "Deku Tree": ((67281, 67303), (68063, 68077)), + "Dodongo's Cavern": ((67304, 67334), (68078, 68160)), + "Jabu Jabu's Belly": ((67335, 67359), (68161, 68188)), + "Bottom of the Well": ((67360, 67384), (68189, 68230)), + "Forest Temple": ((67385, 67420), (68231, 68281)), + "Fire Temple": ((67421, 67457), (68282, 68350)), + "Water Temple": ((67458, 67484), (68351, 68483)), + "Shadow Temple": ((67485, 67532), (68484, 68565)), + "Spirit Temple": ((67533, 67582), (68566, 68625)), + "Ice Cavern": ((67583, 67596), (68626, 68649)), + "Gerudo Training Ground": ((67597, 67635), (68650, 68656)), + "Thieves' Hideout": ((67264, 67268), (68025, 68053)), + "Ganon's Castle": ((67636, 67673), (68657, 68705)), + } + def lookup_and_trim(id, area): + full_name = tracker_data.location_id_to_name["Ocarina of Time"][id] + if "Ganons Tower" in full_name: + return full_name + if area not in ["Overworld", "Thieves' Hideout"]: + # trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC + return full_name[len(area):] + return full_name -@app.route('/tracker/') -@cache.memoize(timeout=60) # multisave is currently created at most every minute -def get_multiworld_tracker(tracker: UUID): - data = _get_multiworld_tracker_data(tracker) - if not data: - abort(404) + locations = tracker_data.get_player_locations(team, player) + checked_locations = tracker_data.get_player_checked_locations(team, player).intersection(set(locations)) + location_info = {} + checks_done = {} + checks_in_area = {} + for area, ranges in area_id_ranges.items(): + location_info[area] = {} + checks_done[area] = 0 + checks_in_area[area] = 0 + for r in ranges: + min_id, max_id = r + for id in range(min_id, max_id + 1): + if id in locations: + checked = id in checked_locations + location_info[area][lookup_and_trim(id, area)] = checked + checks_in_area[area] += 1 + checks_done[area] += checked + + checks_done["Total"] = sum(checks_done.values()) + checks_in_area["Total"] = sum(checks_in_area.values()) + + # Give skulltulas on non-tracked locations + non_tracked_locations = tracker_data.get_player_checked_locations(team, player).difference(set(locations)) + for id in non_tracked_locations: + if "GS" in lookup_and_trim(id, ""): + display_data["token_count"] += 1 + + oot_y = "✔" + oot_x = "✕" + + # Gather small and boss key info + small_key_counts = { + "Forest Temple": oot_y if inventory[66203] else inventory[66175], + "Fire Temple": oot_y if inventory[66204] else inventory[66176], + "Water Temple": oot_y if inventory[66205] else inventory[66177], + "Spirit Temple": oot_y if inventory[66206] else inventory[66178], + "Shadow Temple": oot_y if inventory[66207] else inventory[66179], + "Bottom of the Well": oot_y if inventory[66208] else inventory[66180], + "Gerudo Training Ground": oot_y if inventory[66209] else inventory[66181], + "Thieves' Hideout": oot_y if inventory[66210] else inventory[66182], + "Ganon's Castle": oot_y if inventory[66211] else inventory[66183], + } + boss_key_counts = { + "Forest Temple": oot_y if inventory[66149] else oot_x, + "Fire Temple": oot_y if inventory[66150] else oot_x, + "Water Temple": oot_y if inventory[66151] else oot_x, + "Spirit Temple": oot_y if inventory[66152] else oot_x, + "Shadow Temple": oot_y if inventory[66153] else oot_x, + "Ganon's Castle": oot_y if inventory[66154] else oot_x, + } - data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic") + # Victory condition + game_state = tracker_data.get_player_client_status(team, player) + display_data["game_finished"] = game_state == 30 + + lookup_any_item_id_to_name = tracker_data.item_id_to_name["Ocarina of Time"] + return render_template( + "tracker__OcarinaOfTime.html", + inventory=inventory, + player=player, + team=team, + room=tracker_data.room, + player_name=tracker_data.get_player_name(team, player), + icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0}, + checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, + small_key_counts=small_key_counts, + boss_key_counts=boss_key_counts, + **display_data, + ) - return render_template("multiTracker.html", **data) + _player_trackers["Ocarina of Time"] = render_OcarinaOfTime_tracker + +if "Timespinner" in network_data_package["games"]: + def render_Timespinner_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + icons = { + "Timespinner Wheel": "https://timespinnerwiki.com/mediawiki/images/7/76/Timespinner_Wheel.png", + "Timespinner Spindle": "https://timespinnerwiki.com/mediawiki/images/1/1a/Timespinner_Spindle.png", + "Timespinner Gear 1": "https://timespinnerwiki.com/mediawiki/images/3/3c/Timespinner_Gear_1.png", + "Timespinner Gear 2": "https://timespinnerwiki.com/mediawiki/images/e/e9/Timespinner_Gear_2.png", + "Timespinner Gear 3": "https://timespinnerwiki.com/mediawiki/images/2/22/Timespinner_Gear_3.png", + "Talaria Attachment": "https://timespinnerwiki.com/mediawiki/images/6/61/Talaria_Attachment.png", + "Succubus Hairpin": "https://timespinnerwiki.com/mediawiki/images/4/49/Succubus_Hairpin.png", + "Lightwall": "https://timespinnerwiki.com/mediawiki/images/0/03/Lightwall.png", + "Celestial Sash": "https://timespinnerwiki.com/mediawiki/images/f/f1/Celestial_Sash.png", + "Twin Pyramid Key": "https://timespinnerwiki.com/mediawiki/images/4/49/Twin_Pyramid_Key.png", + "Security Keycard D": "https://timespinnerwiki.com/mediawiki/images/1/1b/Security_Keycard_D.png", + "Security Keycard C": "https://timespinnerwiki.com/mediawiki/images/e/e5/Security_Keycard_C.png", + "Security Keycard B": "https://timespinnerwiki.com/mediawiki/images/f/f6/Security_Keycard_B.png", + "Security Keycard A": "https://timespinnerwiki.com/mediawiki/images/b/b9/Security_Keycard_A.png", + "Library Keycard V": "https://timespinnerwiki.com/mediawiki/images/5/50/Library_Keycard_V.png", + "Tablet": "https://timespinnerwiki.com/mediawiki/images/a/a0/Tablet.png", + "Elevator Keycard": "https://timespinnerwiki.com/mediawiki/images/5/55/Elevator_Keycard.png", + "Oculus Ring": "https://timespinnerwiki.com/mediawiki/images/8/8d/Oculus_Ring.png", + "Water Mask": "https://timespinnerwiki.com/mediawiki/images/0/04/Water_Mask.png", + "Gas Mask": "https://timespinnerwiki.com/mediawiki/images/2/2e/Gas_Mask.png", + "Djinn Inferno": "https://timespinnerwiki.com/mediawiki/images/f/f6/Djinn_Inferno.png", + "Pyro Ring": "https://timespinnerwiki.com/mediawiki/images/2/2c/Pyro_Ring.png", + "Infernal Flames": "https://timespinnerwiki.com/mediawiki/images/1/1f/Infernal_Flames.png", + "Fire Orb": "https://timespinnerwiki.com/mediawiki/images/3/3e/Fire_Orb.png", + "Royal Ring": "https://timespinnerwiki.com/mediawiki/images/f/f3/Royal_Ring.png", + "Plasma Geyser": "https://timespinnerwiki.com/mediawiki/images/1/12/Plasma_Geyser.png", + "Plasma Orb": "https://timespinnerwiki.com/mediawiki/images/4/44/Plasma_Orb.png", + "Kobo": "https://timespinnerwiki.com/mediawiki/images/c/c6/Familiar_Kobo.png", + "Merchant Crow": "https://timespinnerwiki.com/mediawiki/images/4/4e/Familiar_Crow.png", + } -if "Factorio" in games: - @app.route('/tracker//Factorio') - @cache.memoize(timeout=60) # multisave is currently created at most every minute - def get_Factorio_multiworld_tracker(tracker: UUID): - data = _get_multiworld_tracker_data(tracker) - if not data: - abort(404) + timespinner_location_ids = { + "Present": [ + 1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009, + 1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019, + 1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029, + 1337030, 1337031, 1337032, 1337033, 1337034, 1337035, 1337036, 1337037, 1337038, 1337039, + 1337040, 1337041, 1337042, 1337043, 1337044, 1337045, 1337046, 1337047, 1337048, 1337049, + 1337050, 1337051, 1337052, 1337053, 1337054, 1337055, 1337056, 1337057, 1337058, 1337059, + 1337060, 1337061, 1337062, 1337063, 1337064, 1337065, 1337066, 1337067, 1337068, 1337069, + 1337070, 1337071, 1337072, 1337073, 1337074, 1337075, 1337076, 1337077, 1337078, 1337079, + 1337080, 1337081, 1337082, 1337083, 1337084, 1337085], + "Past": [ + 1337086, 1337087, 1337088, 1337089, + 1337090, 1337091, 1337092, 1337093, 1337094, 1337095, 1337096, 1337097, 1337098, 1337099, + 1337100, 1337101, 1337102, 1337103, 1337104, 1337105, 1337106, 1337107, 1337108, 1337109, + 1337110, 1337111, 1337112, 1337113, 1337114, 1337115, 1337116, 1337117, 1337118, 1337119, + 1337120, 1337121, 1337122, 1337123, 1337124, 1337125, 1337126, 1337127, 1337128, 1337129, + 1337130, 1337131, 1337132, 1337133, 1337134, 1337135, 1337136, 1337137, 1337138, 1337139, + 1337140, 1337141, 1337142, 1337143, 1337144, 1337145, 1337146, 1337147, 1337148, 1337149, + 1337150, 1337151, 1337152, 1337153, 1337154, 1337155, + 1337171, 1337172, 1337173, 1337174, 1337175], + "Ancient Pyramid": [ + 1337236, + 1337246, 1337247, 1337248, 1337249] + } - data["inventory"] = _get_inventory_data(data) - data["named_inventory"] = {team_id : { - player_id: _get_named_inventory(inventory, data["custom_items"]) - for player_id, inventory in team_inventory.items() - } for team_id, team_inventory in data["inventory"].items()} - data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio") + slot_data = tracker_data.get_slot_data(team, player) + if (slot_data["DownloadableItems"]): + timespinner_location_ids["Present"] += [ + 1337156, 1337157, 1337159, + 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169, + 1337170] + if (slot_data["Cantoran"]): + timespinner_location_ids["Past"].append(1337176) + if (slot_data["LoreChecks"]): + timespinner_location_ids["Present"] += [ + 1337177, 1337178, 1337179, + 1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187] + timespinner_location_ids["Past"] += [ + 1337188, 1337189, + 1337190, 1337191, 1337192, 1337193, 1337194, 1337195, 1337196, 1337197, 1337198] + if (slot_data["GyreArchives"]): + timespinner_location_ids["Ancient Pyramid"] += [ + 1337237, 1337238, 1337239, + 1337240, 1337241, 1337242, 1337243, 1337244, 1337245] + + display_data = {} + + # Victory condition + game_state = tracker_data.get_player_client_status(team, player) + display_data["game_finished"] = game_state == 30 + + inventory = tracker_data.get_player_inventory_counts(team, player) + + # Turn location IDs into advancement tab counts + checked_locations = tracker_data.get_player_checked_locations(team, player) + lookup_name = lambda id: tracker_data.location_id_to_name["Timespinner"][id] + location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} + for tab_name, tab_locations in timespinner_location_ids.items()} + checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) + for tab_name, tab_locations in timespinner_location_ids.items()} + checks_done["Total"] = len(checked_locations) + checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in timespinner_location_ids.items()} + checks_in_area["Total"] = sum(checks_in_area.values()) + options = {k for k, v in slot_data.items() if v} + + lookup_any_item_id_to_name = tracker_data.item_id_to_name["Timespinner"] + return render_template( + "tracker__Timespinner.html", + inventory=inventory, + icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0}, + player=player, + team=team, + room=tracker_data.room, + player_name=tracker_data.get_player_name(team, player), + checks_done=checks_done, + checks_in_area=checks_in_area, + location_info=location_info, + options=options, + **display_data, + ) - return render_template("multiFactorioTracker.html", **data) + _player_trackers["Timespinner"] = render_Timespinner_tracker + +if "Super Metroid" in network_data_package["games"]: + def render_SuperMetroid_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + icons = { + "Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ETank.png", + "Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Missile.png", + "Super Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Super.png", + "Power Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/PowerBomb.png", + "Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Bomb.png", + "Charge Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Charge.png", + "Ice Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Ice.png", + "Hi-Jump Boots": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/HiJump.png", + "Speed Booster": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpeedBooster.png", + "Wave Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Wave.png", + "Spazer": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Spazer.png", + "Spring Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpringBall.png", + "Varia Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Varia.png", + "Plasma Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Plasma.png", + "Grappling Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Grapple.png", + "Morph Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Morph.png", + "Reserve Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Reserve.png", + "Gravity Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Gravity.png", + "X-Ray Scope": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/XRayScope.png", + "Space Jump": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpaceJump.png", + "Screw Attack": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ScrewAttack.png", + "Nothing": "", + "No Energy": "", + "Kraid": "", + "Phantoon": "", + "Draygon": "", + "Ridley": "", + "Mother Brain": "", + } + multi_items = { + "Energy Tank": 83000, + "Missile": 83001, + "Super Missile": 83002, + "Power Bomb": 83003, + "Reserve Tank": 83020, + } -@app.route('/tracker//A Link to the Past') -@cache.memoize(timeout=60) # multisave is currently created at most every minute -def get_LttP_multiworld_tracker(tracker: UUID): - room: Room = Room.get(tracker=tracker) - if not room: - abort(404) - locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ - precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ - get_static_room_data(room) + supermetroid_location_ids = { + 'Crateria/Blue Brinstar': [82005, 82007, 82008, 82026, 82029, + 82000, 82004, 82006, 82009, 82010, + 82011, 82012, 82027, 82028, 82034, + 82036, 82037], + 'Green/Pink Brinstar': [82017, 82023, 82030, 82033, 82035, + 82013, 82014, 82015, 82016, 82018, + 82019, 82021, 82022, 82024, 82025, + 82031], + 'Red Brinstar': [82038, 82042, 82039, 82040, 82041], + 'Kraid': [82043, 82048, 82044], + 'Norfair': [82050, 82053, 82061, 82066, 82068, + 82049, 82051, 82054, 82055, 82056, + 82062, 82063, 82064, 82065, 82067], + 'Lower Norfair': [82078, 82079, 82080, 82070, 82071, + 82073, 82074, 82075, 82076, 82077], + 'Crocomire': [82052, 82060, 82057, 82058, 82059], + 'Wrecked Ship': [82129, 82132, 82134, 82135, 82001, + 82002, 82003, 82128, 82130, 82131, + 82133], + 'West Maridia': [82138, 82136, 82137, 82139, 82140, + 82141, 82142], + 'East Maridia': [82143, 82145, 82150, 82152, 82154, + 82144, 82146, 82147, 82148, 82149, + 82151], + } - inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if - playernumber not in groups} - for teamnumber, team in enumerate(names)} + display_data = {} + inventory = tracker_data.get_player_inventory_counts(team, player) + + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[0].lower() + display_data[base_name + "_count"] = inventory[item_id] + + # Victory condition + game_state = tracker_data.get_player_client_status(team, player) + display_data["game_finished"] = game_state == 30 + + # Turn location IDs into advancement tab counts + checked_locations = tracker_data.get_player_checked_locations(team, player) + lookup_name = lambda id: tracker_data.location_id_to_name["Super Metroid"][id] + location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} + for tab_name, tab_locations in supermetroid_location_ids.items()} + checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) + for tab_name, tab_locations in supermetroid_location_ids.items()} + checks_done['Total'] = len(checked_locations) + checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()} + checks_in_area['Total'] = sum(checks_in_area.values()) + + lookup_any_item_id_to_name = tracker_data.item_id_to_name["Super Metroid"] + return render_template( + "tracker__SuperMetroid.html", + inventory=inventory, + icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0}, + player=player, + team=team, + room=tracker_data.room, + player_name=tracker_data.get_player_name(team, player), + checks_done=checks_done, + checks_in_area=checks_in_area, + location_info=location_info, + **display_data, + ) - checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations} - for playernumber in range(1, len(team) + 1) if playernumber not in groups} - for teamnumber, team in enumerate(names)} + _player_trackers["Super Metroid"] = render_SuperMetroid_tracker - percent_total_checks_done = {teamnumber: {playernumber: 0 - for playernumber in range(1, len(team) + 1) if playernumber not in groups} - for teamnumber, team in enumerate(names)} +if "ChecksFinder" in network_data_package["games"]: + def render_ChecksFinder_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + icons = { + "Checks Available": "https://0rganics.org/archipelago/cf/spr_tiles_3.png", + "Map Width": "https://0rganics.org/archipelago/cf/spr_tiles_4.png", + "Map Height": "https://0rganics.org/archipelago/cf/spr_tiles_5.png", + "Map Bombs": "https://0rganics.org/archipelago/cf/spr_tiles_6.png", - hints = {team: set() for team in range(len(names))} - if room.multisave: - multisave = restricted_loads(room.multisave) - else: - multisave = {} - if "hints" in multisave: - for (team, slot), slot_hints in multisave["hints"].items(): - hints[team] |= set(slot_hints) - - def attribute_item(team: int, recipient: int, item: int): - nonlocal inventory - target_item = links.get(item, item) - if item in levels: # non-progressive - inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item]) - else: - inventory[team][recipient][target_item] += 1 - - for (team, player), locations_checked in multisave.get("location_checks", {}).items(): - if player in groups: - continue - player_locations = locations[player] - if precollected_items: - precollected = precollected_items[player] - for item_id in precollected: - attribute_item(team, player, item_id) - for location in locations_checked: - if location not in player_locations or location not in player_location_to_area.get(player, {}): - continue - item, recipient, flags = player_locations[location] - recipients = groups.get(recipient, [recipient]) - for recipient in recipients: - attribute_item(team, recipient, item) - checks_done[team][player][player_location_to_area[player][location]] += 1 - checks_done[team][player]["Total"] = len(locations_checked) - - percent_total_checks_done[team][player] = ( - checks_done[team][player]["Total"] / len(player_locations) * 100 - if player_locations - else 100 + "Nothing": "", + } + + checksfinder_location_ids = { + "Tile 1": 81000, + "Tile 2": 81001, + "Tile 3": 81002, + "Tile 4": 81003, + "Tile 5": 81004, + "Tile 6": 81005, + "Tile 7": 81006, + "Tile 8": 81007, + "Tile 9": 81008, + "Tile 10": 81009, + "Tile 11": 81010, + "Tile 12": 81011, + "Tile 13": 81012, + "Tile 14": 81013, + "Tile 15": 81014, + "Tile 16": 81015, + "Tile 17": 81016, + "Tile 18": 81017, + "Tile 19": 81018, + "Tile 20": 81019, + "Tile 21": 81020, + "Tile 22": 81021, + "Tile 23": 81022, + "Tile 24": 81023, + "Tile 25": 81024, + } + + display_data = {} + inventory = tracker_data.get_player_inventory_counts(team, player) + locations = tracker_data.get_player_locations(team, player) + + # Multi-items + multi_items = { + "Map Width": 80000, + "Map Height": 80001, + "Map Bombs": 80002 + } + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[-1].lower() + count = inventory[item_id] + display_data[base_name + "_count"] = count + display_data[base_name + "_display"] = count + 5 + + # Get location info + checked_locations = tracker_data.get_player_checked_locations(team, player) + lookup_name = lambda id: tracker_data.location_id_to_name["ChecksFinder"][id] + location_info = {tile_name: {lookup_name(tile_location): (tile_location in checked_locations)} for + tile_name, tile_location in checksfinder_location_ids.items() if + tile_location in set(locations)} + checks_done = {tile_name: len([tile_location]) for tile_name, tile_location in checksfinder_location_ids.items() + if tile_location in checked_locations and tile_location in set(locations)} + checks_done['Total'] = len(checked_locations) + checks_in_area = checks_done + + # Calculate checks available + display_data["checks_unlocked"] = min( + display_data["width_count"] + display_data["height_count"] + display_data["bombs_count"] + 5, 25) + display_data["checks_available"] = max(display_data["checks_unlocked"] - len(checked_locations), 0) + + # Victory condition + game_state = tracker_data.get_player_client_status(team, player) + display_data["game_finished"] = game_state == 30 + + lookup_any_item_id_to_name = tracker_data.item_id_to_name["ChecksFinder"] + return render_template( + "tracker__ChecksFinder.html", + inventory=inventory, icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0}, + player=player, + team=team, + room=tracker_data.room, + player_name=tracker_data.get_player_name(team, player), + checks_done=checks_done, + checks_in_area=checks_in_area, + location_info=location_info, + **display_data, ) - for (team, player), game_state in multisave.get("client_game_state", {}).items(): - if player in groups: - continue - if game_state == 30: - inventory[team][player][106] = 1 # Triforce + _player_trackers["ChecksFinder"] = render_ChecksFinder_tracker + +if "Starcraft 2 Wings of Liberty" in network_data_package["games"]: + def render_Starcraft2WingsOfLiberty_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + SC2WOL_LOC_ID_OFFSET = 1000 + SC2WOL_ITEM_ID_OFFSET = 1000 + + icons = { + "Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png", + "Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png", + "Starting Supply": "https://static.wikia.nocookie.net/starcraft/images/d/d3/TerranSupply_SC2_Icon1.gif", + + "Infantry Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel1.png", + "Infantry Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel2.png", + "Infantry Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel3.png", + "Infantry Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel1.png", + "Infantry Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel2.png", + "Infantry Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel3.png", + "Vehicle Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel1.png", + "Vehicle Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel2.png", + "Vehicle Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel3.png", + "Vehicle Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel1.png", + "Vehicle Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel2.png", + "Vehicle Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel3.png", + "Ship Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel1.png", + "Ship Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel2.png", + "Ship Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel3.png", + "Ship Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel1.png", + "Ship Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel2.png", + "Ship Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel3.png", + + "Bunker": "https://static.wikia.nocookie.net/starcraft/images/c/c5/Bunker_SC2_Icon1.jpg", + "Missile Turret": "https://static.wikia.nocookie.net/starcraft/images/5/5f/MissileTurret_SC2_Icon1.jpg", + "Sensor Tower": "https://static.wikia.nocookie.net/starcraft/images/d/d2/SensorTower_SC2_Icon1.jpg", + + "Projectile Accelerator (Bunker)": "https://0rganics.org/archipelago/sc2wol/ProjectileAccelerator.png", + "Neosteel Bunker (Bunker)": "https://0rganics.org/archipelago/sc2wol/NeosteelBunker.png", + "Titanium Housing (Missile Turret)": "https://0rganics.org/archipelago/sc2wol/TitaniumHousing.png", + "Hellstorm Batteries (Missile Turret)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png", + "Advanced Construction (SCV)": "https://0rganics.org/archipelago/sc2wol/AdvancedConstruction.png", + "Dual-Fusion Welders (SCV)": "https://0rganics.org/archipelago/sc2wol/Dual-FusionWelders.png", + "Fire-Suppression System (Building)": "https://0rganics.org/archipelago/sc2wol/Fire-SuppressionSystem.png", + "Orbital Command (Building)": "https://0rganics.org/archipelago/sc2wol/OrbitalCommandCampaign.png", + + "Marine": "https://static.wikia.nocookie.net/starcraft/images/4/47/Marine_SC2_Icon1.jpg", + "Medic": "https://static.wikia.nocookie.net/starcraft/images/7/74/Medic_SC2_Rend1.jpg", + "Firebat": "https://static.wikia.nocookie.net/starcraft/images/3/3c/Firebat_SC2_Rend1.jpg", + "Marauder": "https://static.wikia.nocookie.net/starcraft/images/b/ba/Marauder_SC2_Icon1.jpg", + "Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg", + + "Stimpack (Marine)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", + "Super Stimpack (Marine)": "/static/static/icons/sc2/superstimpack.png", + "Combat Shield (Marine)": "https://0rganics.org/archipelago/sc2wol/CombatShieldCampaign.png", + "Laser Targeting System (Marine)": "/static/static/icons/sc2/lasertargetingsystem.png", + "Magrail Munitions (Marine)": "/static/static/icons/sc2/magrailmunitions.png", + "Optimized Logistics (Marine)": "/static/static/icons/sc2/optimizedlogistics.png", + "Advanced Medic Facilities (Medic)": "https://0rganics.org/archipelago/sc2wol/AdvancedMedicFacilities.png", + "Stabilizer Medpacks (Medic)": "https://0rganics.org/archipelago/sc2wol/StabilizerMedpacks.png", + "Restoration (Medic)": "/static/static/icons/sc2/restoration.png", + "Optical Flare (Medic)": "/static/static/icons/sc2/opticalflare.png", + "Optimized Logistics (Medic)": "/static/static/icons/sc2/optimizedlogistics.png", + "Incinerator Gauntlets (Firebat)": "https://0rganics.org/archipelago/sc2wol/IncineratorGauntlets.png", + "Juggernaut Plating (Firebat)": "https://0rganics.org/archipelago/sc2wol/JuggernautPlating.png", + "Stimpack (Firebat)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", + "Super Stimpack (Firebat)": "/static/static/icons/sc2/superstimpack.png", + "Optimized Logistics (Firebat)": "/static/static/icons/sc2/optimizedlogistics.png", + "Concussive Shells (Marauder)": "https://0rganics.org/archipelago/sc2wol/ConcussiveShellsCampaign.png", + "Kinetic Foam (Marauder)": "https://0rganics.org/archipelago/sc2wol/KineticFoam.png", + "Stimpack (Marauder)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", + "Super Stimpack (Marauder)": "/static/static/icons/sc2/superstimpack.png", + "Laser Targeting System (Marauder)": "/static/static/icons/sc2/lasertargetingsystem.png", + "Magrail Munitions (Marauder)": "/static/static/icons/sc2/magrailmunitions.png", + "Internal Tech Module (Marauder)": "/static/static/icons/sc2/internalizedtechmodule.png", + "U-238 Rounds (Reaper)": "https://0rganics.org/archipelago/sc2wol/U-238Rounds.png", + "G-4 Clusterbomb (Reaper)": "https://0rganics.org/archipelago/sc2wol/G-4Clusterbomb.png", + "Stimpack (Reaper)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", + "Super Stimpack (Reaper)": "/static/static/icons/sc2/superstimpack.png", + "Laser Targeting System (Reaper)": "/static/static/icons/sc2/lasertargetingsystem.png", + "Advanced Cloaking Field (Reaper)": "/static/static/icons/sc2/terran-cloak-color.png", + "Spider Mines (Reaper)": "/static/static/icons/sc2/spidermine.png", + "Combat Drugs (Reaper)": "/static/static/icons/sc2/reapercombatdrugs.png", + + "Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg", + "Vulture": "https://static.wikia.nocookie.net/starcraft/images/d/da/Vulture_WoL.jpg", + "Goliath": "https://static.wikia.nocookie.net/starcraft/images/e/eb/Goliath_WoL.jpg", + "Diamondback": "https://static.wikia.nocookie.net/starcraft/images/a/a6/Diamondback_WoL.jpg", + "Siege Tank": "https://static.wikia.nocookie.net/starcraft/images/5/57/SiegeTank_SC2_Icon1.jpg", + + "Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png", + "Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png", + "Hellbat Aspect (Hellion)": "/static/static/icons/sc2/hellionbattlemode.png", + "Smart Servos (Hellion)": "/static/static/icons/sc2/transformationservos.png", + "Optimized Logistics (Hellion)": "/static/static/icons/sc2/optimizedlogistics.png", + "Jump Jets (Hellion)": "/static/static/icons/sc2/jumpjets.png", + "Stimpack (Hellion)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", + "Super Stimpack (Hellion)": "/static/static/icons/sc2/superstimpack.png", + "Cerberus Mine (Spider Mine)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png", + "High Explosive Munition (Spider Mine)": "/static/static/icons/sc2/high-explosive-spidermine.png", + "Replenishable Magazine (Vulture)": "https://0rganics.org/archipelago/sc2wol/ReplenishableMagazine.png", + "Ion Thrusters (Vulture)": "/static/static/icons/sc2/emergencythrusters.png", + "Auto Launchers (Vulture)": "/static/static/icons/sc2/jotunboosters.png", + "Multi-Lock Weapons System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Multi-LockWeaponsSystem.png", + "Ares-Class Targeting System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Ares-ClassTargetingSystem.png", + "Jump Jets (Goliath)": "/static/static/icons/sc2/jumpjets.png", + "Optimized Logistics (Goliath)": "/static/static/icons/sc2/optimizedlogistics.png", + "Tri-Lithium Power Cell (Diamondback)": "https://0rganics.org/archipelago/sc2wol/Tri-LithiumPowerCell.png", + "Shaped Hull (Diamondback)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png", + "Hyperfluxor (Diamondback)": "/static/static/icons/sc2/hyperfluxor.png", + "Burst Capacitors (Diamondback)": "/static/static/icons/sc2/burstcapacitors.png", + "Optimized Logistics (Diamondback)": "/static/static/icons/sc2/optimizedlogistics.png", + "Maelstrom Rounds (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/MaelstromRounds.png", + "Shaped Blast (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/ShapedBlast.png", + "Jump Jets (Siege Tank)": "/static/static/icons/sc2/jumpjets.png", + "Spider Mines (Siege Tank)": "/static/static/icons/sc2/siegetank-spidermines.png", + "Smart Servos (Siege Tank)": "/static/static/icons/sc2/transformationservos.png", + "Graduating Range (Siege Tank)": "/static/static/icons/sc2/siegetankrange.png", + "Laser Targeting System (Siege Tank)": "/static/static/icons/sc2/lasertargetingsystem.png", + "Advanced Siege Tech (Siege Tank)": "/static/static/icons/sc2/improvedsiegemode.png", + "Internal Tech Module (Siege Tank)": "/static/static/icons/sc2/internalizedtechmodule.png", + + "Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg", + "Wraith": "https://static.wikia.nocookie.net/starcraft/images/7/75/Wraith_WoL.jpg", + "Viking": "https://static.wikia.nocookie.net/starcraft/images/2/2a/Viking_SC2_Icon1.jpg", + "Banshee": "https://static.wikia.nocookie.net/starcraft/images/3/32/Banshee_SC2_Icon1.jpg", + "Battlecruiser": "https://static.wikia.nocookie.net/starcraft/images/f/f5/Battlecruiser_SC2_Icon1.jpg", + + "Rapid Deployment Tube (Medivac)": "https://0rganics.org/archipelago/sc2wol/RapidDeploymentTube.png", + "Advanced Healing AI (Medivac)": "https://0rganics.org/archipelago/sc2wol/AdvancedHealingAI.png", + "Expanded Hull (Medivac)": "/static/static/icons/sc2/neosteelfortifiedarmor.png", + "Afterburners (Medivac)": "/static/static/icons/sc2/medivacemergencythrusters.png", + "Tomahawk Power Cells (Wraith)": "https://0rganics.org/archipelago/sc2wol/TomahawkPowerCells.png", + "Displacement Field (Wraith)": "https://0rganics.org/archipelago/sc2wol/DisplacementField.png", + "Advanced Laser Technology (Wraith)": "/static/static/icons/sc2/improvedburstlaser.png", + "Ripwave Missiles (Viking)": "https://0rganics.org/archipelago/sc2wol/RipwaveMissiles.png", + "Phobos-Class Weapons System (Viking)": "https://0rganics.org/archipelago/sc2wol/Phobos-ClassWeaponsSystem.png", + "Smart Servos (Viking)": "/static/static/icons/sc2/transformationservos.png", + "Magrail Munitions (Viking)": "/static/static/icons/sc2/magrailmunitions.png", + "Cross-Spectrum Dampeners (Banshee)": "/static/static/icons/sc2/crossspectrumdampeners.png", + "Advanced Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png", + "Shockwave Missile Battery (Banshee)": "https://0rganics.org/archipelago/sc2wol/ShockwaveMissileBattery.png", + "Hyperflight Rotors (Banshee)": "/static/static/icons/sc2/hyperflightrotors.png", + "Laser Targeting System (Banshee)": "/static/static/icons/sc2/lasertargetingsystem.png", + "Internal Tech Module (Banshee)": "/static/static/icons/sc2/internalizedtechmodule.png", + "Missile Pods (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/MissilePods.png", + "Defensive Matrix (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png", + "Tactical Jump (Battlecruiser)": "/static/static/icons/sc2/warpjump.png", + "Cloak (Battlecruiser)": "/static/static/icons/sc2/terran-cloak-color.png", + "ATX Laser Battery (Battlecruiser)": "/static/static/icons/sc2/specialordance.png", + "Optimized Logistics (Battlecruiser)": "/static/static/icons/sc2/optimizedlogistics.png", + "Internal Tech Module (Battlecruiser)": "/static/static/icons/sc2/internalizedtechmodule.png", + + "Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg", + "Spectre": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Spectre_WoL.jpg", + "Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg", + + "Widow Mine": "/static/static/icons/sc2/widowmine.png", + "Cyclone": "/static/static/icons/sc2/cyclone.png", + "Liberator": "/static/static/icons/sc2/liberator.png", + "Valkyrie": "/static/static/icons/sc2/valkyrie.png", + + "Ocular Implants (Ghost)": "https://0rganics.org/archipelago/sc2wol/OcularImplants.png", + "Crius Suit (Ghost)": "https://0rganics.org/archipelago/sc2wol/CriusSuit.png", + "EMP Rounds (Ghost)": "/static/static/icons/sc2/terran-emp-color.png", + "Lockdown (Ghost)": "/static/static/icons/sc2/lockdown.png", + "Psionic Lash (Spectre)": "https://0rganics.org/archipelago/sc2wol/PsionicLash.png", + "Nyx-Class Cloaking Module (Spectre)": "https://0rganics.org/archipelago/sc2wol/Nyx-ClassCloakingModule.png", + "Impaler Rounds (Spectre)": "/static/static/icons/sc2/impalerrounds.png", + "330mm Barrage Cannon (Thor)": "https://0rganics.org/archipelago/sc2wol/330mmBarrageCannon.png", + "Immortality Protocol (Thor)": "https://0rganics.org/archipelago/sc2wol/ImmortalityProtocol.png", + "High Impact Payload (Thor)": "/static/static/icons/sc2/thorsiegemode.png", + "Smart Servos (Thor)": "/static/static/icons/sc2/transformationservos.png", + + "Optimized Logistics (Predator)": "/static/static/icons/sc2/optimizedlogistics.png", + "Drilling Claws (Widow Mine)": "/static/static/icons/sc2/drillingclaws.png", + "Concealment (Widow Mine)": "/static/static/icons/sc2/widowminehidden.png", + "Black Market Launchers (Widow Mine)": "/static/static/icons/sc2/widowmine-attackrange.png", + "Executioner Missiles (Widow Mine)": "/static/static/icons/sc2/widowmine-deathblossom.png", + "Mag-Field Accelerators (Cyclone)": "/static/static/icons/sc2/magfieldaccelerator.png", + "Mag-Field Launchers (Cyclone)": "/static/static/icons/sc2/cyclonerangeupgrade.png", + "Targeting Optics (Cyclone)": "/static/static/icons/sc2/targetingoptics.png", + "Rapid Fire Launchers (Cyclone)": "/static/static/icons/sc2/ripwavemissiles.png", + "Bio Mechanical Repair Drone (Raven)": "/static/static/icons/sc2/biomechanicaldrone.png", + "Spider Mines (Raven)": "/static/static/icons/sc2/siegetank-spidermines.png", + "Railgun Turret (Raven)": "/static/static/icons/sc2/autoturretblackops.png", + "Hunter-Seeker Weapon (Raven)": "/static/static/icons/sc2/specialordance.png", + "Interference Matrix (Raven)": "/static/static/icons/sc2/interferencematrix.png", + "Anti-Armor Missile (Raven)": "/static/static/icons/sc2/shreddermissile.png", + "Internal Tech Module (Raven)": "/static/static/icons/sc2/internalizedtechmodule.png", + "EMP Shockwave (Science Vessel)": "/static/static/icons/sc2/staticempblast.png", + "Defensive Matrix (Science Vessel)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png", + "Advanced Ballistics (Liberator)": "/static/static/icons/sc2/advanceballistics.png", + "Raid Artillery (Liberator)": "/static/static/icons/sc2/terrandefendermodestructureattack.png", + "Cloak (Liberator)": "/static/static/icons/sc2/terran-cloak-color.png", + "Laser Targeting System (Liberator)": "/static/static/icons/sc2/lasertargetingsystem.png", + "Optimized Logistics (Liberator)": "/static/static/icons/sc2/optimizedlogistics.png", + "Enhanced Cluster Launchers (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png", + "Shaped Hull (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png", + "Burst Lasers (Valkyrie)": "/static/static/icons/sc2/improvedburstlaser.png", + "Afterburners (Valkyrie)": "/static/static/icons/sc2/medivacemergencythrusters.png", + + "War Pigs": "https://static.wikia.nocookie.net/starcraft/images/e/ed/WarPigs_SC2_Icon1.jpg", + "Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg", + "Hammer Securities": "https://static.wikia.nocookie.net/starcraft/images/3/3b/HammerSecurity_SC2_Icon1.jpg", + "Spartan Company": "https://static.wikia.nocookie.net/starcraft/images/b/be/SpartanCompany_SC2_Icon1.jpg", + "Siege Breakers": "https://static.wikia.nocookie.net/starcraft/images/3/31/SiegeBreakers_SC2_Icon1.jpg", + "Hel's Angel": "https://static.wikia.nocookie.net/starcraft/images/6/63/HelsAngels_SC2_Icon1.jpg", + "Dusk Wings": "https://static.wikia.nocookie.net/starcraft/images/5/52/DuskWings_SC2_Icon1.jpg", + "Jackson's Revenge": "https://static.wikia.nocookie.net/starcraft/images/9/95/JacksonsRevenge_SC2_Icon1.jpg", + + "Ultra-Capacitors": "https://static.wikia.nocookie.net/starcraft/images/2/23/SC2_Lab_Ultra_Capacitors_Icon.png", + "Vanadium Plating": "https://static.wikia.nocookie.net/starcraft/images/6/67/SC2_Lab_VanPlating_Icon.png", + "Orbital Depots": "https://static.wikia.nocookie.net/starcraft/images/0/01/SC2_Lab_Orbital_Depot_Icon.png", + "Micro-Filtering": "https://static.wikia.nocookie.net/starcraft/images/2/20/SC2_Lab_MicroFilter_Icon.png", + "Automated Refinery": "https://static.wikia.nocookie.net/starcraft/images/7/71/SC2_Lab_Auto_Refinery_Icon.png", + "Command Center Reactor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/SC2_Lab_CC_Reactor_Icon.png", + "Raven": "https://static.wikia.nocookie.net/starcraft/images/1/19/SC2_Lab_Raven_Icon.png", + "Science Vessel": "https://static.wikia.nocookie.net/starcraft/images/c/c3/SC2_Lab_SciVes_Icon.png", + "Tech Reactor": "https://static.wikia.nocookie.net/starcraft/images/c/c5/SC2_Lab_Tech_Reactor_Icon.png", + "Orbital Strike": "https://static.wikia.nocookie.net/starcraft/images/d/df/SC2_Lab_Orb_Strike_Icon.png", + + "Shrike Turret (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png", + "Fortified Bunker (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png", + "Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png", + "Perdition Turret": "https://static.wikia.nocookie.net/starcraft/images/a/af/SC2_Lab_PerdTurret_Icon.png", + "Predator": "https://static.wikia.nocookie.net/starcraft/images/8/83/SC2_Lab_Predator_Icon.png", + "Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png", + "Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png", + "Regenerative Bio-Steel Level 1": "/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png", + "Regenerative Bio-Steel Level 2": "/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png", + "Hive Mind Emulator": "https://static.wikia.nocookie.net/starcraft/images/b/bc/SC2_Lab_Hive_Emulator_Icon.png", + "Psi Disrupter": "https://static.wikia.nocookie.net/starcraft/images/c/cf/SC2_Lab_Psi_Disruptor_Icon.png", + + "Zealot": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Icon_Protoss_Zealot.jpg", + "Stalker": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Icon_Protoss_Stalker.jpg", + "High Templar": "https://static.wikia.nocookie.net/starcraft/images/a/a0/Icon_Protoss_High_Templar.jpg", + "Dark Templar": "https://static.wikia.nocookie.net/starcraft/images/9/90/Icon_Protoss_Dark_Templar.jpg", + "Immortal": "https://static.wikia.nocookie.net/starcraft/images/c/c1/Icon_Protoss_Immortal.jpg", + "Colossus": "https://static.wikia.nocookie.net/starcraft/images/4/40/Icon_Protoss_Colossus.jpg", + "Phoenix": "https://static.wikia.nocookie.net/starcraft/images/b/b1/Icon_Protoss_Phoenix.jpg", + "Void Ray": "https://static.wikia.nocookie.net/starcraft/images/1/1d/VoidRay_SC2_Rend1.jpg", + "Carrier": "https://static.wikia.nocookie.net/starcraft/images/2/2c/Icon_Protoss_Carrier.jpg", + + "Nothing": "", + } + sc2wol_location_ids = { + "Liberation Day": range(SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 200), + "The Outlaws": range(SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 300), + "Zero Hour": range(SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 400), + "Evacuation": range(SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 500), + "Outbreak": range(SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 600), + "Safe Haven": range(SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 700), + "Haven's Fall": range(SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 800), + "Smash and Grab": range(SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 900), + "The Dig": range(SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 1000), + "The Moebius Factor": range(SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1100), + "Supernova": range(SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1200), + "Maw of the Void": range(SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1300), + "Devil's Playground": range(SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1400), + "Welcome to the Jungle": range(SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1500), + "Breakout": range(SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1600), + "Ghost of a Chance": range(SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1700), + "The Great Train Robbery": range(SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1800), + "Cutthroat": range(SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1900), + "Engine of Destruction": range(SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 2000), + "Media Blitz": range(SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2100), + "Piercing the Shroud": range(SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2200), + "Whispers of Doom": range(SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2300), + "A Sinister Turn": range(SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2400), + "Echoes of the Future": range(SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2500), + "In Utter Darkness": range(SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2600), + "Gates of Hell": range(SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2700), + "Belly of the Beast": range(SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2800), + "Shatter the Sky": range(SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2900), + } - player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)} - player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)} - for loc_data in locations.values(): - for values in loc_data.values(): - item_id, item_player, flags = values + display_data = {} - if item_id in ids_big_key: - player_big_key_locations[item_player].add(ids_big_key[item_id]) - elif item_id in ids_small_key: - player_small_key_locations[item_player].add(ids_small_key[item_id]) - group_big_key_locations = set() - group_key_locations = set() - for player in [player for player in range(1, len(names[0]) + 1) if player not in groups]: - group_key_locations |= player_small_key_locations[player] - group_big_key_locations |= player_big_key_locations[player] - - activity_timers = {} - now = datetime.datetime.utcnow() - for (team, player), timestamp in multisave.get("client_activity_timers", []): - activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) - - player_names = {} - for team, names in enumerate(names): - for player, name in enumerate(names, 1): - player_names[(team, player)] = name - long_player_names = player_names.copy() - for (team, player), alias in multisave.get("name_aliases", {}).items(): - player_names[(team, player)] = alias - long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})" - - video = {} - for (team, player), data in multisave.get("video", []): - video[(team, player)] = data - - enabled_multiworld_trackers = get_enabled_multiworld_trackers(room, "A Link to the Past") - - return render_template("lttpMultiTracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name, - lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names, - tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons, - multi_items=multi_items, checks_done=checks_done, - percent_total_checks_done=percent_total_checks_done, - ordered_areas=ordered_areas, checks_in_area=seed_checks_in_area, - activity_timers=activity_timers, - key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids, - video=video, big_key_locations=group_big_key_locations, - hints=hints, long_player_names=long_player_names, - enabled_multiworld_trackers=enabled_multiworld_trackers) - - -game_specific_trackers: typing.Dict[str, typing.Callable] = { - "Minecraft": __renderMinecraftTracker, - "Ocarina of Time": __renderOoTTracker, - "Timespinner": __renderTimespinnerTracker, - "A Link to the Past": __renderAlttpTracker, - "ChecksFinder": __renderChecksfinder, - "Super Metroid": __renderSuperMetroidTracker, - "Starcraft 2 Wings of Liberty": __renderSC2WoLTracker -} - -multi_trackers: typing.Dict[str, typing.Callable] = { - "A Link to the Past": get_LttP_multiworld_tracker, -} - -if "Factorio" in games: - multi_trackers["Factorio"] = get_Factorio_multiworld_tracker + # Grouped Items + grouped_item_ids = { + "Progressive Weapon Upgrade": 107 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Armor Upgrade": 108 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Infantry Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Vehicle Upgrade": 110 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Ship Upgrade": 111 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Weapon/Armor Upgrade": 112 + SC2WOL_ITEM_ID_OFFSET + } + grouped_item_replacements = { + "Progressive Weapon Upgrade": ["Progressive Infantry Weapon", "Progressive Vehicle Weapon", + "Progressive Ship Weapon"], + "Progressive Armor Upgrade": ["Progressive Infantry Armor", "Progressive Vehicle Armor", + "Progressive Ship Armor"], + "Progressive Infantry Upgrade": ["Progressive Infantry Weapon", "Progressive Infantry Armor"], + "Progressive Vehicle Upgrade": ["Progressive Vehicle Weapon", "Progressive Vehicle Armor"], + "Progressive Ship Upgrade": ["Progressive Ship Weapon", "Progressive Ship Armor"] + } + grouped_item_replacements["Progressive Weapon/Armor Upgrade"] = grouped_item_replacements[ + "Progressive Weapon Upgrade"] + \ + grouped_item_replacements[ + "Progressive Armor Upgrade"] + replacement_item_ids = { + "Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET, + } + + inventory = tracker_data.get_player_inventory_counts(team, player) + for grouped_item_name, grouped_item_id in grouped_item_ids.items(): + count: int = inventory[grouped_item_id] + if count > 0: + for replacement_item in grouped_item_replacements[grouped_item_name]: + replacement_id: int = replacement_item_ids[replacement_item] + inventory[replacement_id] = count + + # Determine display for progressive items + progressive_items = { + "Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Stimpack (Marine)": 208 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Stimpack (Firebat)": 226 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Stimpack (Marauder)": 228 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Stimpack (Reaper)": 250 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Stimpack (Hellion)": 259 + SC2WOL_ITEM_ID_OFFSET, + "Progressive High Impact Payload (Thor)": 361 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Cross-Spectrum Dampeners (Banshee)": 316 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Regenerative Bio-Steel": 617 + SC2WOL_ITEM_ID_OFFSET + } + progressive_names = { + "Progressive Infantry Weapon": ["Infantry Weapons Level 1", "Infantry Weapons Level 1", + "Infantry Weapons Level 2", "Infantry Weapons Level 3"], + "Progressive Infantry Armor": ["Infantry Armor Level 1", "Infantry Armor Level 1", + "Infantry Armor Level 2", "Infantry Armor Level 3"], + "Progressive Vehicle Weapon": ["Vehicle Weapons Level 1", "Vehicle Weapons Level 1", + "Vehicle Weapons Level 2", "Vehicle Weapons Level 3"], + "Progressive Vehicle Armor": ["Vehicle Armor Level 1", "Vehicle Armor Level 1", + "Vehicle Armor Level 2", "Vehicle Armor Level 3"], + "Progressive Ship Weapon": ["Ship Weapons Level 1", "Ship Weapons Level 1", + "Ship Weapons Level 2", "Ship Weapons Level 3"], + "Progressive Ship Armor": ["Ship Armor Level 1", "Ship Armor Level 1", + "Ship Armor Level 2", "Ship Armor Level 3"], + "Progressive Stimpack (Marine)": ["Stimpack (Marine)", "Stimpack (Marine)", + "Super Stimpack (Marine)"], + "Progressive Stimpack (Firebat)": ["Stimpack (Firebat)", "Stimpack (Firebat)", + "Super Stimpack (Firebat)"], + "Progressive Stimpack (Marauder)": ["Stimpack (Marauder)", "Stimpack (Marauder)", + "Super Stimpack (Marauder)"], + "Progressive Stimpack (Reaper)": ["Stimpack (Reaper)", "Stimpack (Reaper)", + "Super Stimpack (Reaper)"], + "Progressive Stimpack (Hellion)": ["Stimpack (Hellion)", "Stimpack (Hellion)", + "Super Stimpack (Hellion)"], + "Progressive High Impact Payload (Thor)": ["High Impact Payload (Thor)", + "High Impact Payload (Thor)", "Smart Servos (Thor)"], + "Progressive Cross-Spectrum Dampeners (Banshee)": ["Cross-Spectrum Dampeners (Banshee)", + "Cross-Spectrum Dampeners (Banshee)", + "Advanced Cross-Spectrum Dampeners (Banshee)"], + "Progressive Regenerative Bio-Steel": ["Regenerative Bio-Steel Level 1", + "Regenerative Bio-Steel Level 1", + "Regenerative Bio-Steel Level 2"] + } + for item_name, item_id in progressive_items.items(): + level = min(inventory[item_id], len(progressive_names[item_name]) - 1) + display_name = progressive_names[item_name][level] + base_name = (item_name.split(maxsplit=1)[1].lower() + .replace(' ', '_') + .replace("-", "") + .replace("(", "") + .replace(")", "")) + display_data[base_name + "_level"] = level + display_data[base_name + "_url"] = icons[display_name] + display_data[base_name + "_name"] = display_name + + # Multi-items + multi_items = { + "+15 Starting Minerals": 800 + SC2WOL_ITEM_ID_OFFSET, + "+15 Starting Vespene": 801 + SC2WOL_ITEM_ID_OFFSET, + "+2 Starting Supply": 802 + SC2WOL_ITEM_ID_OFFSET + } + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[-1].lower() + count = inventory[item_id] + if base_name == "supply": + count = count * 2 + display_data[base_name + "_count"] = count + else: + count = count * 15 + display_data[base_name + "_count"] = count + + # Victory condition + game_state = tracker_data.get_player_client_status(team, player) + display_data["game_finished"] = game_state == 30 + + # Turn location IDs into mission objective counts + locations = tracker_data.get_player_locations(team, player) + checked_locations = tracker_data.get_player_checked_locations(team, player) + lookup_name = lambda id: tracker_data.location_id_to_name["Starcraft 2 Wings of Liberty"][id] + location_info = {mission_name: {lookup_name(id): (id in checked_locations) for id in mission_locations if + id in set(locations)} for mission_name, mission_locations in + sc2wol_location_ids.items()} + checks_done = {mission_name: len( + [id for id in mission_locations if id in checked_locations and id in set(locations)]) for + mission_name, mission_locations in sc2wol_location_ids.items()} + checks_done['Total'] = len(checked_locations) + checks_in_area = {mission_name: len([id for id in mission_locations if id in set(locations)]) for + mission_name, mission_locations in sc2wol_location_ids.items()} + checks_in_area['Total'] = sum(checks_in_area.values()) + + lookup_any_item_id_to_name = tracker_data.item_id_to_name["Starcraft 2 Wings of Liberty"] + return render_template( + "tracker__Starcraft2WingsOfLiberty.html", + inventory=inventory, + icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0}, + player=player, + team=team, + room=tracker_data.room, + player_name=tracker_data.get_player_name(team, player), + checks_done=checks_done, + checks_in_area=checks_in_area, + location_info=location_info, + **display_data, + ) + + _player_trackers["Starcraft 2 Wings of Liberty"] = render_Starcraft2WingsOfLiberty_tracker diff --git a/worlds/__init__.py b/worlds/__init__.py index 40e0b20f1974..66c91639b9f3 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -1,43 +1,40 @@ import importlib import os import sys -import typing import warnings import zipimport +from typing import Dict, List, NamedTuple, TypedDict -from Utils import user_path, local_path +from Utils import local_path, user_path local_folder = os.path.dirname(__file__) user_folder = user_path("worlds") if user_path() != local_path() else None -__all__ = ( - "lookup_any_item_id_to_name", - "lookup_any_location_id_to_name", +__all__ = { "network_data_package", "AutoWorldRegister", "world_sources", "local_folder", "user_folder", -) - - -class GamesData(typing.TypedDict): - item_name_groups: typing.Dict[str, typing.List[str]] - item_name_to_id: typing.Dict[str, int] - location_name_groups: typing.Dict[str, typing.List[str]] - location_name_to_id: typing.Dict[str, int] - version: int + "GamesPackage", + "DataPackage", +} -class GamesPackage(GamesData, total=False): +class GamesPackage(TypedDict, total=False): + item_name_groups: Dict[str, List[str]] + item_name_to_id: Dict[str, int] + location_name_groups: Dict[str, List[str]] + location_name_to_id: Dict[str, int] checksum: str + version: int # TODO: Remove support after per game data packages API change. -class DataPackage(typing.TypedDict): - games: typing.Dict[str, GamesPackage] +class DataPackage(TypedDict): + games: Dict[str, GamesPackage] -class WorldSource(typing.NamedTuple): +class WorldSource(NamedTuple): path: str # typically relative path from this module is_zip: bool = False relative: bool = True # relative to regular world import folder @@ -88,7 +85,7 @@ def load(self) -> bool: # find potential world containers, currently folders and zip-importable .apworld's -world_sources: typing.List[WorldSource] = [] +world_sources: List[WorldSource] = [] for folder in (folder for folder in (user_folder, local_folder) if folder): relative = folder == local_folder for entry in os.scandir(folder): @@ -105,25 +102,9 @@ def load(self) -> bool: for world_source in world_sources: world_source.load() -lookup_any_item_id_to_name = {} -lookup_any_location_id_to_name = {} -games: typing.Dict[str, GamesPackage] = {} - -from .AutoWorld import AutoWorldRegister # noqa: E402 - # Build the data package for each game. -for world_name, world in AutoWorldRegister.world_types.items(): - games[world_name] = world.get_data_package_data() - lookup_any_item_id_to_name.update(world.item_id_to_name) - lookup_any_location_id_to_name.update(world.location_id_to_name) +from .AutoWorld import AutoWorldRegister network_data_package: DataPackage = { - "games": games, + "games": {world_name: world.get_data_package_data() for world_name, world in AutoWorldRegister.world_types.items()}, } - -# Set entire datapackage to version 0 if any of them are set to 0 -if any(not world.data_version for world in AutoWorldRegister.world_types.values()): - import logging - - logging.warning(f"Datapackage is in custom mode. Custom Worlds: " - f"{[world for world in AutoWorldRegister.world_types.values() if not world.data_version]}") From e916b0d6b0447f6637d7b143ccf20766f9d6e6db Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sat, 18 Nov 2023 13:35:57 -0500 Subject: [PATCH 043/104] Stardew Valley: Add Options presets (#2470) --- worlds/stardew_valley/__init__.py | 2 + worlds/stardew_valley/data/items.csv | 4 +- worlds/stardew_valley/items.py | 8 +- worlds/stardew_valley/logic.py | 7 +- worlds/stardew_valley/options.py | 131 +++---- worlds/stardew_valley/presets.py | 323 ++++++++++++++++++ worlds/stardew_valley/test/TestRules.py | 2 +- worlds/stardew_valley/test/__init__.py | 4 +- .../test/checks/option_checks.py | 2 +- 9 files changed, 402 insertions(+), 81 deletions(-) create mode 100644 worlds/stardew_valley/presets.py diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 177b6436ae56..24ffa8c1add8 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -11,6 +11,7 @@ from .logic import StardewLogic, StardewRule, True_, MAX_MONTHS from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, NumberOfLuckBuffs, NumberOfMovementBuffs, \ BackpackProgression, BuildingProgression, ExcludeGingerIsland +from .presets import sv_options_presets from .regions import create_regions from .rules import set_rules from worlds.generic.Rules import set_rule @@ -34,6 +35,7 @@ class StardewItem(Item): class StardewWebWorld(WebWorld): theme = "dirt" bug_report_page = "https://github.com/agilbert1412/StardewArchipelago/issues/new?labels=bug&title=%5BBug%5D%3A+Brief+Description+of+bug+here" + options_presets = sv_options_presets tutorials = [ Tutorial( diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index a3d61e8b58e0..3c4ddb84156b 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -1,7 +1,7 @@ id,name,classification,groups,mod_name 0,Joja Cola,filler,TRASH, -15,Rusty Key,progression,MUSEUM, -16,Dwarvish Translation Guide,progression,MUSEUM, +15,Rusty Key,progression,, +16,Dwarvish Translation Guide,progression,, 17,Bridge Repair,progression,COMMUNITY_REWARD, 18,Greenhouse,progression,COMMUNITY_REWARD, 19,Glittering Boulder Removed,progression,COMMUNITY_REWARD, diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 2d28b4de43c1..a5a370aa08cd 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -300,15 +300,15 @@ def create_stardrops(item_factory: StardewItemFactory, options: StardewValleyOpt def create_museum_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + items.append(item_factory("Rusty Key")) + items.append(item_factory("Dwarvish Translation Guide")) + items.append(item_factory("Ancient Seeds Recipe")) if options.museumsanity == Museumsanity.option_none: return - items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 5) + items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 10) items.extend(item_factory(item) for item in ["Ancient Seeds"] * 5) items.extend(item_factory(item) for item in ["Traveling Merchant Metal Detector"] * 4) - items.append(item_factory("Ancient Seeds Recipe")) items.append(item_factory("Stardrop")) - items.append(item_factory("Rusty Key")) - items.append(item_factory("Dwarvish Translation Guide")) def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py index 0746bd775242..5a6244cf37ae 100644 --- a/worlds/stardew_valley/logic.py +++ b/worlds/stardew_valley/logic.py @@ -8,7 +8,7 @@ from .data.bundle_data import BundleItem from .data.crops_data import crops_by_name from .data.fish_data import island_fish -from .data.museum_data import all_museum_items, MuseumItem, all_museum_artifacts, dwarf_scrolls, all_museum_minerals +from .data.museum_data import all_museum_items, MuseumItem, all_museum_artifacts, all_museum_minerals from .data.recipe_data import all_cooking_recipes, CookingRecipe, RecipeSource, FriendshipSource, QueenOfSauceSource, \ StarterSource, ShopSource, SkillSource from .data.villagers_data import all_villagers_by_name, Villager @@ -1283,8 +1283,6 @@ def has_year_three(self) -> StardewRule: return self.has_lived_months(8) def can_speak_dwarf(self) -> StardewRule: - if self.options.museumsanity == Museumsanity.option_none: - return And([self.can_donate_museum_item(item) for item in dwarf_scrolls]) return self.received("Dwarvish Translation Guide") def can_donate_museum_item(self, item: MuseumItem) -> StardewRule: @@ -1370,9 +1368,6 @@ def has_lived_months(self, number: int) -> StardewRule: return self.received("Month End", number) def has_rusty_key(self) -> StardewRule: - if self.options.museumsanity == Museumsanity.option_none: - required_donations = 80 # It's 60, but without a metal detector I'd rather overshoot so players don't get screwed by RNG - return self.has([item.name for item in all_museum_items], required_donations) & self.can_reach_region(Region.museum) return self.received(Wallet.rusty_key) def can_win_egg_hunt(self) -> StardewRule: diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index f462f507d4a3..d85bbf06f6ee 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -7,15 +7,15 @@ class Goal(Choice): """What's your goal with this play-through? - Community Center: The world will be completed once you complete the Community Center. - Grandpa's Evaluation: The world will be completed once 4 candles are lit at Grandpa's Shrine. - Bottom of the Mines: The world will be completed once you reach level 120 in the mineshaft. - Cryptic Note: The world will be completed once you complete the quest "Cryptic Note" where Mr Qi asks you to reach floor 100 in the Skull Cavern. - Master Angler: The world will be completed once you have caught every fish in the game. Pairs well with Fishsanity. - Complete Collection: The world will be completed once you have completed the museum by donating every possible item. Pairs well with Museumsanity. - Full House: The world will be completed once you get married and have two kids. Pairs well with Friendsanity. - Greatest Walnut Hunter: The world will be completed once you find all 130 Golden Walnuts - Perfection: The world will be completed once you attain Perfection, based on the vanilla definition. + Community Center: Complete the Community Center. + Grandpa's Evaluation: Succeed grandpa's evaluation with 4 lit candles. + Bottom of the Mines: Reach level 120 in the mineshaft. + Cryptic Note: Complete the quest "Cryptic Note" where Mr Qi asks you to reach floor 100 in the Skull Cavern. + Master Angler: Catch every fish in the game. Pairs well with Fishsanity. + Complete Collection: Complete the museum by donating every possible item. Pairs well with Museumsanity. + Full House: Get married and have two children. Pairs well with Friendsanity. + Greatest Walnut Hunter: Find all 130 Golden Walnuts + Perfection: Attain Perfection, based on the vanilla definition. """ internal_name = "goal" display_name = "Goal" @@ -50,7 +50,7 @@ def get_option_name(cls, value) -> str: class StartingMoney(SpecialRange): """Amount of gold when arriving at the farm. - Set to -1 or unlimited for infinite money in this playthrough""" + Set to -1 or unlimited for infinite money""" internal_name = "starting_money" display_name = "Starting Gold" range_start = -1 @@ -117,10 +117,10 @@ class BundlePrice(Choice): class EntranceRandomization(Choice): """Should area entrances be randomized? Disabled: No entrance randomization is done - Pelican Town: Only buildings in the main town area are randomized among each other - Non Progression: Only buildings that are always available are randomized with each other - Buildings: All Entrances that Allow you to enter a building using a door are randomized with each other - Chaos: Same as above, but the entrances get reshuffled every single day! + Pelican Town: Only doors in the main town area are randomized with each other + Non Progression: Only entrances that are always available are randomized with each other + Buildings: All Entrances that Allow you to enter a building are randomized with each other + Chaos: Same as "Buildings", but the entrances get reshuffled every single day! """ # Everything: All buildings and areas are randomized with each other # Chaos, same as everything: but the buildings are shuffled again every in-game day. You can't learn it! @@ -144,11 +144,10 @@ class EntranceRandomization(Choice): class SeasonRandomization(Choice): """Should seasons be randomized? - All settings allow you to choose which season you want to play next (from those unlocked) at the end of a season. - Disabled: You will start in Spring with all seasons unlocked. - Randomized: The seasons will be unlocked randomly as Archipelago items. - Randomized Not Winter: The seasons are randomized, but you're guaranteed not to start with winter. - Progressive: You will start in Spring and unlock the seasons in their original order. + Disabled: Start in Spring with all seasons unlocked. + Randomized: Start in a random season and the other 3 must be unlocked randomly. + Randomized Not Winter: Same as randomized, but the start season is guaranteed not to be winter. + Progressive: Start in Spring and unlock the seasons in their original order. """ internal_name = "season_randomization" display_name = "Season Randomization" @@ -163,20 +162,21 @@ class Cropsanity(Choice): """Formerly named "Seed Shuffle" Pierre now sells a random amount of seasonal seeds and Joja sells them without season requirements, but only in huge packs. Disabled: All the seeds are unlocked from the start, there are no location checks for growing and harvesting crops - Shuffled: Seeds are unlocked as archipelago item, for each seed there is a location check for growing and harvesting that crop + Shuffled: Seeds are unlocked as archipelago items, for each seed there is a location check for growing and harvesting that crop """ internal_name = "cropsanity" display_name = "Cropsanity" default = 1 option_disabled = 0 - option_shuffled = 1 + option_enabled = 1 + alias_shuffled = option_enabled class BackpackProgression(Choice): - """How is the backpack progression handled? - Vanilla: You can buy them at Pierre's General Store. + """Shuffle the backpack? + Vanilla: You can buy backpacks at Pierre's General Store. Progressive: You will randomly find Progressive Backpack upgrades. - Early Progressive: You can expect your first Backpack in sphere 1. + Early Progressive: Same as progressive, but one backpack will be placed early in the multiworld. """ internal_name = "backpack_progression" display_name = "Backpack Progression" @@ -187,8 +187,8 @@ class BackpackProgression(Choice): class ToolProgression(Choice): - """How is the tool progression handled? - Vanilla: Clint will upgrade your tools with ore. + """Shuffle the tool upgrades? + Vanilla: Clint will upgrade your tools with metal bars. Progressive: You will randomly find Progressive Tool upgrades.""" internal_name = "tool_progression" display_name = "Tool Progression" @@ -198,12 +198,11 @@ class ToolProgression(Choice): class ElevatorProgression(Choice): - """How is Elevator progression handled? - Vanilla: You will unlock new elevator floors for yourself. - Progressive: You will randomly find Progressive Mine Elevators to go deeper. Locations are sent for reaching - every elevator level. - Progressive from previous floor: Same as progressive, but you must reach elevator floors on your own, - you cannot use the elevator to check elevator locations""" + """Shuffle the elevator? + Vanilla: Reaching a mineshaft floor unlocks the elevator for it + Progressive: You will randomly find Progressive Mine Elevators to go deeper. + Progressive from previous floor: Same as progressive, but you cannot use the elevator to check elevator locations. + You must reach elevator floors on your own.""" internal_name = "elevator_progression" display_name = "Elevator Progression" default = 2 @@ -213,10 +212,9 @@ class ElevatorProgression(Choice): class SkillProgression(Choice): - """How is the skill progression handled? - Vanilla: You will level up and get the normal reward at each level. - Progressive: The xp will be earned internally, locations will be sent when you earn a level. Your real - levels will be scattered around the multiworld.""" + """Shuffle skill levels? + Vanilla: Leveling up skills is normal + Progressive: Skill levels are unlocked randomly, and earning xp sends checks""" internal_name = "skill_progression" display_name = "Skill Progression" default = 1 @@ -225,11 +223,11 @@ class SkillProgression(Choice): class BuildingProgression(Choice): - """How is the building progression handled? - Vanilla: You will buy each building normally. + """Shuffle Carpenter Buildings? + Vanilla: You can buy each building normally. Progressive: You will receive the buildings and will be able to build the first one of each type for free, once it is received. If you want more of the same building, it will cost the vanilla price. - Progressive early shipping bin: You can expect your shipping bin in sphere 1. + Progressive early shipping bin: Same as Progressive, but the shipping bin will be placed early in the multiworld. """ internal_name = "building_progression" display_name = "Building Progression" @@ -240,10 +238,10 @@ class BuildingProgression(Choice): class FestivalLocations(Choice): - """Locations for attending and participating in festivals - With Disabled, you do not need to attend festivals - With Easy, there are checks for participating in festivals - With Hard, the festival checks are only granted when the player performs well in the festival + """Shuffle Festival Activities? + Disabled: You do not need to attend festivals + Easy: Every festival has checks, but they are easy and usually only require attendance + Hard: Festivals have more checks, and many require performing well, not just attending """ internal_name = "festival_locations" display_name = "Festival Locations" @@ -254,11 +252,10 @@ class FestivalLocations(Choice): class ArcadeMachineLocations(Choice): - """How are the Arcade Machines handled? - Disabled: The arcade machines are not included in the Archipelago shuffling. + """Shuffle the arcade machines? + Disabled: The arcade machines are not included. Victories: Each Arcade Machine will contain one check on victory - Victories Easy: The arcade machines are both made considerably easier to be more accessible for the average - player. + Victories Easy: Same as Victories, but both games are made considerably easier. Full Shuffling: The arcade machines will contain multiple checks each, and different buffs that make the game easier are in the item pool. Junimo Kart has one check at the end of each level. Journey of the Prairie King has one check after each boss, plus one check for each vendor equipment. @@ -273,10 +270,10 @@ class ArcadeMachineLocations(Choice): class SpecialOrderLocations(Choice): - """How are the Special Orders handled? + """Shuffle Special Orders? Disabled: The special orders are not included in the Archipelago shuffling. Board Only: The Special Orders on the board in town are location checks - Board and Qi: The Special Orders from Qi's walnut room are checks, as well as the board in town + Board and Qi: The Special Orders from Mr Qi's walnut room are checks, in addition to the board in town """ internal_name = "special_order_locations" display_name = "Special Order Locations" @@ -287,7 +284,7 @@ class SpecialOrderLocations(Choice): class HelpWantedLocations(SpecialRange): - """How many "Help Wanted" quests need to be completed as Archipelago Locations + """Include location checks for Help Wanted quests Out of every 7 quests, 4 will be item deliveries, and then 1 of each for: Fishing, Gathering and Slaying Monsters. Choosing a multiple of 7 is recommended.""" internal_name = "help_wanted_locations" @@ -307,7 +304,7 @@ class HelpWantedLocations(SpecialRange): class Fishsanity(Choice): - """Locations for catching fish? + """Locations for catching a fish the first time? None: There are no locations for catching fish Legendaries: Each of the 5 legendary fish are checks Special: A curated selection of strong fish are checks @@ -336,7 +333,7 @@ class Museumsanity(Choice): None: There are no locations for donating artifacts and minerals to the museum Milestones: The donation milestones from the vanilla game are checks Randomized: A random selection of minerals and artifacts are checks - All: Every single donation will be a check + All: Every single donation is a check """ internal_name = "museumsanity" display_name = "Museumsanity" @@ -348,12 +345,12 @@ class Museumsanity(Choice): class Friendsanity(Choice): - """Locations for friendships? - None: There are no checks for befriending villagers - Bachelors: Each heart of a bachelor is a check - Starting NPCs: Each heart for npcs that are immediately available is a check - All: Every heart with every NPC is a check, including Leo, Kent, Sandy, etc - All With Marriage: Marriage candidates must also be dated, married, and befriended up to 14 hearts. + """Shuffle Friendships? + None: Friendship hearts are earned normally + Bachelors: Hearts with bachelors are shuffled + Starting NPCs: Hearts for NPCs available immediately are checks + All: Hearts for all npcs are checks, including Leo, Kent, Sandy, etc + All With Marriage: Hearts for all npcs are checks, including romance hearts up to 14 when applicable """ internal_name = "friendsanity" display_name = "Friendsanity" @@ -368,7 +365,7 @@ class Friendsanity(Choice): # Conditional Setting - Friendsanity not None class FriendsanityHeartSize(Range): - """If using friendsanity, how many hearts are received per item, and how many hearts must be earned to send a check + """If using friendsanity, how many hearts are received per heart item, and how many hearts must be earned to send a check A higher value will lead to fewer heart items in the item pool, reducing bloat""" internal_name = "friendsanity_heart_size" display_name = "Friendsanity Heart Size" @@ -411,6 +408,7 @@ class ExcludeGingerIsland(Toggle): class TrapItems(Choice): """When rolling filler items, including resource packs, the game can also roll trap items. + Trap items are negative items that cause problems or annoyances for the player This setting is for choosing if traps will be in the item pool, and if so, how punishing they will be. """ internal_name = "trap_items" @@ -441,14 +439,16 @@ class MultipleDaySleepCost(SpecialRange): special_range_names = { "free": 0, - "cheap": 25, - "medium": 50, - "expensive": 100, + "cheap": 10, + "medium": 25, + "expensive": 50, + "very expensive": 100, } class ExperienceMultiplier(SpecialRange): - """How fast you want to earn skill experience. A lower setting mean less experience. + """How fast you want to earn skill experience. + A lower setting mean less experience. A higher setting means more experience.""" internal_name = "experience_multiplier" display_name = "Experience Multiplier" @@ -513,14 +513,15 @@ class QuickStart(Toggle): class Gifting(Toggle): - """Do you want to enable gifting items to and from other Stardew Valley worlds?""" + """Do you want to enable gifting items to and from other Archipelago slots? + Items can only be sent to games that also support gifting""" internal_name = "gifting" display_name = "Gifting" default = 1 class Mods(OptionSet): - """List of mods that will be considered for shuffling.""" + """List of mods that will be included in the shuffling.""" internal_name = "mods" display_name = "Mods" valid_keys = { diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py new file mode 100644 index 000000000000..8823c52e5b20 --- /dev/null +++ b/worlds/stardew_valley/presets.py @@ -0,0 +1,323 @@ +from typing import Any, Dict + +from Options import Accessibility, ProgressionBalancing, DeathLink +from .options import Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, EntranceRandomization, SeasonRandomization, Cropsanity, \ + BackpackProgression, ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, FestivalLocations, ArcadeMachineLocations, \ + SpecialOrderLocations, HelpWantedLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, NumberOfMovementBuffs, NumberOfLuckBuffs, \ + ExcludeGingerIsland, TrapItems, MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, \ + Gifting + +all_random_settings = { + "progression_balancing": "random", + "accessibility": "random", + Goal.internal_name: "random", + StartingMoney.internal_name: "random", + ProfitMargin.internal_name: "random", + BundleRandomization.internal_name: "random", + BundlePrice.internal_name: "random", + EntranceRandomization.internal_name: "random", + SeasonRandomization.internal_name: "random", + Cropsanity.internal_name: "random", + BackpackProgression.internal_name: "random", + ToolProgression.internal_name: "random", + ElevatorProgression.internal_name: "random", + SkillProgression.internal_name: "random", + BuildingProgression.internal_name: "random", + FestivalLocations.internal_name: "random", + ArcadeMachineLocations.internal_name: "random", + SpecialOrderLocations.internal_name: "random", + HelpWantedLocations.internal_name: "random", + Fishsanity.internal_name: "random", + Museumsanity.internal_name: "random", + Friendsanity.internal_name: "random", + FriendsanityHeartSize.internal_name: "random", + NumberOfMovementBuffs.internal_name: "random", + NumberOfLuckBuffs.internal_name: "random", + ExcludeGingerIsland.internal_name: "random", + TrapItems.internal_name: "random", + MultipleDaySleepEnabled.internal_name: "random", + MultipleDaySleepCost.internal_name: "random", + ExperienceMultiplier.internal_name: "random", + FriendshipMultiplier.internal_name: "random", + DebrisMultiplier.internal_name: "random", + QuickStart.internal_name: "random", + Gifting.internal_name: "random", + "death_link": "random", +} + +easy_settings = { + "progression_balancing": ProgressionBalancing.default, + "accessibility": Accessibility.option_items, + Goal.internal_name: Goal.option_community_center, + StartingMoney.internal_name: "very rich", + ProfitMargin.internal_name: "double", + BundleRandomization.internal_name: BundleRandomization.option_thematic, + BundlePrice.internal_name: BundlePrice.option_cheap, + EntranceRandomization.internal_name: EntranceRandomization.option_disabled, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, + Cropsanity.internal_name: Cropsanity.option_enabled, + BackpackProgression.internal_name: BackpackProgression.option_early_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive_early_shipping_bin, + FestivalLocations.internal_name: FestivalLocations.option_easy, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + HelpWantedLocations.internal_name: "minimum", + Fishsanity.internal_name: Fishsanity.option_only_easy_fish, + Museumsanity.internal_name: Museumsanity.option_milestones, + Friendsanity.internal_name: Friendsanity.option_none, + FriendsanityHeartSize.internal_name: 4, + NumberOfMovementBuffs.internal_name: 8, + NumberOfLuckBuffs.internal_name: 8, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + TrapItems.internal_name: TrapItems.option_easy, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, + MultipleDaySleepCost.internal_name: "free", + ExperienceMultiplier.internal_name: "triple", + FriendshipMultiplier.internal_name: "quadruple", + DebrisMultiplier.internal_name: DebrisMultiplier.option_quarter, + QuickStart.internal_name: QuickStart.option_true, + Gifting.internal_name: Gifting.option_true, + "death_link": "false", +} + +medium_settings = { + "progression_balancing": 25, + "accessibility": Accessibility.option_locations, + Goal.internal_name: Goal.option_community_center, + StartingMoney.internal_name: "rich", + ProfitMargin.internal_name: 150, + BundleRandomization.internal_name: BundleRandomization.option_thematic, + BundlePrice.internal_name: BundlePrice.option_normal, + EntranceRandomization.internal_name: EntranceRandomization.option_non_progression, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_enabled, + BackpackProgression.internal_name: BackpackProgression.option_early_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive_early_shipping_bin, + FestivalLocations.internal_name: FestivalLocations.option_hard, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories_easy, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only, + HelpWantedLocations.internal_name: "normal", + Fishsanity.internal_name: Fishsanity.option_exclude_legendaries, + Museumsanity.internal_name: Museumsanity.option_milestones, + Friendsanity.internal_name: Friendsanity.option_starting_npcs, + FriendsanityHeartSize.internal_name: 4, + NumberOfMovementBuffs.internal_name: 6, + NumberOfLuckBuffs.internal_name: 6, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + TrapItems.internal_name: TrapItems.option_medium, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, + MultipleDaySleepCost.internal_name: "free", + ExperienceMultiplier.internal_name: "double", + FriendshipMultiplier.internal_name: "triple", + DebrisMultiplier.internal_name: DebrisMultiplier.option_half, + QuickStart.internal_name: QuickStart.option_true, + Gifting.internal_name: Gifting.option_true, + "death_link": "false", +} + +hard_settings = { + "progression_balancing": 0, + "accessibility": Accessibility.option_locations, + Goal.internal_name: Goal.option_grandpa_evaluation, + StartingMoney.internal_name: "extra", + ProfitMargin.internal_name: "normal", + BundleRandomization.internal_name: BundleRandomization.option_thematic, + BundlePrice.internal_name: BundlePrice.option_expensive, + EntranceRandomization.internal_name: EntranceRandomization.option_buildings, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_enabled, + BackpackProgression.internal_name: BackpackProgression.option_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive, + FestivalLocations.internal_name: FestivalLocations.option_hard, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + HelpWantedLocations.internal_name: "lots", + Fishsanity.internal_name: Fishsanity.option_all, + Museumsanity.internal_name: Museumsanity.option_all, + Friendsanity.internal_name: Friendsanity.option_all, + FriendsanityHeartSize.internal_name: 4, + NumberOfMovementBuffs.internal_name: 4, + NumberOfLuckBuffs.internal_name: 4, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + TrapItems.internal_name: TrapItems.option_hard, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, + MultipleDaySleepCost.internal_name: "cheap", + ExperienceMultiplier.internal_name: "vanilla", + FriendshipMultiplier.internal_name: "double", + DebrisMultiplier.internal_name: DebrisMultiplier.option_vanilla, + QuickStart.internal_name: QuickStart.option_true, + Gifting.internal_name: Gifting.option_true, + "death_link": "true", +} + +nightmare_settings = { + "progression_balancing": 0, + "accessibility": Accessibility.option_locations, + Goal.internal_name: Goal.option_community_center, + StartingMoney.internal_name: "vanilla", + ProfitMargin.internal_name: "half", + BundleRandomization.internal_name: BundleRandomization.option_shuffled, + BundlePrice.internal_name: BundlePrice.option_expensive, + EntranceRandomization.internal_name: EntranceRandomization.option_buildings, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_enabled, + BackpackProgression.internal_name: BackpackProgression.option_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive, + FestivalLocations.internal_name: FestivalLocations.option_hard, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + HelpWantedLocations.internal_name: "maximum", + Fishsanity.internal_name: Fishsanity.option_special, + Museumsanity.internal_name: Museumsanity.option_all, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize.internal_name: 4, + NumberOfMovementBuffs.internal_name: 2, + NumberOfLuckBuffs.internal_name: 2, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + TrapItems.internal_name: TrapItems.option_hell, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, + MultipleDaySleepCost.internal_name: "expensive", + ExperienceMultiplier.internal_name: "half", + FriendshipMultiplier.internal_name: "vanilla", + DebrisMultiplier.internal_name: DebrisMultiplier.option_vanilla, + QuickStart.internal_name: QuickStart.option_false, + Gifting.internal_name: Gifting.option_true, + "death_link": "true", +} + +short_settings = { + "progression_balancing": ProgressionBalancing.default, + "accessibility": Accessibility.option_items, + Goal.internal_name: Goal.option_bottom_of_the_mines, + StartingMoney.internal_name: "filthy rich", + ProfitMargin.internal_name: "quadruple", + BundleRandomization.internal_name: BundleRandomization.option_thematic, + BundlePrice.internal_name: BundlePrice.option_very_cheap, + EntranceRandomization.internal_name: EntranceRandomization.option_disabled, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, + Cropsanity.internal_name: Cropsanity.option_disabled, + BackpackProgression.internal_name: BackpackProgression.option_early_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive_early_shipping_bin, + FestivalLocations.internal_name: FestivalLocations.option_disabled, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + HelpWantedLocations.internal_name: "none", + Fishsanity.internal_name: Fishsanity.option_none, + Museumsanity.internal_name: Museumsanity.option_none, + Friendsanity.internal_name: Friendsanity.option_none, + FriendsanityHeartSize.internal_name: 4, + NumberOfMovementBuffs.internal_name: 10, + NumberOfLuckBuffs.internal_name: 10, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + TrapItems.internal_name: TrapItems.option_easy, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, + MultipleDaySleepCost.internal_name: "free", + ExperienceMultiplier.internal_name: "quadruple", + FriendshipMultiplier.internal_name: 800, + DebrisMultiplier.internal_name: DebrisMultiplier.option_none, + QuickStart.internal_name: QuickStart.option_true, + Gifting.internal_name: Gifting.option_true, + "death_link": "false", +} + +lowsanity_settings = { + "progression_balancing": ProgressionBalancing.default, + "accessibility": Accessibility.option_minimal, + Goal.internal_name: Goal.default, + StartingMoney.internal_name: StartingMoney.default, + ProfitMargin.internal_name: ProfitMargin.default, + BundleRandomization.internal_name: BundleRandomization.default, + BundlePrice.internal_name: BundlePrice.default, + EntranceRandomization.internal_name: EntranceRandomization.default, + SeasonRandomization.internal_name: SeasonRandomization.option_disabled, + Cropsanity.internal_name: Cropsanity.option_disabled, + BackpackProgression.internal_name: BackpackProgression.option_vanilla, + ToolProgression.internal_name: ToolProgression.option_vanilla, + ElevatorProgression.internal_name: ElevatorProgression.option_vanilla, + SkillProgression.internal_name: SkillProgression.option_vanilla, + BuildingProgression.internal_name: BuildingProgression.option_vanilla, + FestivalLocations.internal_name: FestivalLocations.option_disabled, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + HelpWantedLocations.internal_name: "none", + Fishsanity.internal_name: Fishsanity.option_none, + Museumsanity.internal_name: Museumsanity.option_none, + Friendsanity.internal_name: Friendsanity.option_none, + FriendsanityHeartSize.internal_name: FriendsanityHeartSize.default, + NumberOfMovementBuffs.internal_name: NumberOfMovementBuffs.default, + NumberOfLuckBuffs.internal_name: NumberOfLuckBuffs.default, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + TrapItems.internal_name: TrapItems.default, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default, + MultipleDaySleepCost.internal_name: MultipleDaySleepCost.default, + ExperienceMultiplier.internal_name: ExperienceMultiplier.default, + FriendshipMultiplier.internal_name: FriendshipMultiplier.default, + DebrisMultiplier.internal_name: DebrisMultiplier.default, + QuickStart.internal_name: QuickStart.default, + Gifting.internal_name: Gifting.default, + "death_link": DeathLink.default, +} + +allsanity_settings = { + "progression_balancing": ProgressionBalancing.default, + "accessibility": Accessibility.option_locations, + Goal.internal_name: Goal.default, + StartingMoney.internal_name: StartingMoney.default, + ProfitMargin.internal_name: ProfitMargin.default, + BundleRandomization.internal_name: BundleRandomization.default, + BundlePrice.internal_name: BundlePrice.default, + EntranceRandomization.internal_name: EntranceRandomization.option_buildings, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_enabled, + BackpackProgression.internal_name: BackpackProgression.option_early_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive_early_shipping_bin, + FestivalLocations.internal_name: FestivalLocations.option_hard, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + HelpWantedLocations.internal_name: "maximum", + Fishsanity.internal_name: Fishsanity.option_all, + Museumsanity.internal_name: Museumsanity.option_all, + Friendsanity.internal_name: Friendsanity.option_all, + FriendsanityHeartSize.internal_name: 1, + NumberOfMovementBuffs.internal_name: 12, + NumberOfLuckBuffs.internal_name: 12, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + TrapItems.internal_name: TrapItems.default, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default, + MultipleDaySleepCost.internal_name: MultipleDaySleepCost.default, + ExperienceMultiplier.internal_name: ExperienceMultiplier.default, + FriendshipMultiplier.internal_name: FriendshipMultiplier.default, + DebrisMultiplier.internal_name: DebrisMultiplier.default, + QuickStart.internal_name: QuickStart.default, + Gifting.internal_name: Gifting.default, + "death_link": DeathLink.default, +} + +sv_options_presets: Dict[str, Dict[str, Any]] = { + "All random": all_random_settings, + "Easy": easy_settings, + "Medium": medium_settings, + "Hard": hard_settings, + "Nightmare": nightmare_settings, + "Short": short_settings, + "Lowsanity": lowsanity_settings, + "Allsanity": allsanity_settings, +} diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py index 72337812cd80..0749b1a8f153 100644 --- a/worlds/stardew_valley/test/TestRules.py +++ b/worlds/stardew_valley/test/TestRules.py @@ -329,7 +329,7 @@ class TestRecipeLogic(SVTestBase): options = { options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_shuffled, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, } # I wanted to make a test for different ways to obtain a pizza, but I'm stuck not knowing how to block the immediate purchase from Gus diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index b0c4ba2c7bcb..ba037f7a65da 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -47,7 +47,7 @@ def run_default_tests(self) -> bool: def minimal_locations_maximal_items(): min_max_options = { SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_shuffled, + Cropsanity.internal_name: Cropsanity.option_enabled, BackpackProgression.internal_name: BackpackProgression.option_vanilla, ToolProgression.internal_name: ToolProgression.option_vanilla, SkillProgression.internal_name: SkillProgression.option_vanilla, @@ -72,7 +72,7 @@ def allsanity_options_without_mods(): BundleRandomization.internal_name: BundleRandomization.option_shuffled, BundlePrice.internal_name: BundlePrice.option_expensive, SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_shuffled, + Cropsanity.internal_name: Cropsanity.option_enabled, BackpackProgression.internal_name: BackpackProgression.option_progressive, ToolProgression.internal_name: ToolProgression.option_progressive, SkillProgression.internal_name: SkillProgression.option_progressive, diff --git a/worlds/stardew_valley/test/checks/option_checks.py b/worlds/stardew_valley/test/checks/option_checks.py index ce8e552461e3..c9d9860cf52b 100644 --- a/worlds/stardew_valley/test/checks/option_checks.py +++ b/worlds/stardew_valley/test/checks/option_checks.py @@ -40,7 +40,7 @@ def assert_can_reach_island_if_should(tester: SVTestBase, multiworld: MultiWorld def assert_cropsanity_same_number_items_and_locations(tester: SVTestBase, multiworld: MultiWorld): - is_cropsanity = get_stardew_options(multiworld).cropsanity.value == options.Cropsanity.option_shuffled + is_cropsanity = get_stardew_options(multiworld).cropsanity.value == options.Cropsanity.option_enabled if not is_cropsanity: return From f959819801fa153dc2461ed22881fe2f80f206bf Mon Sep 17 00:00:00 2001 From: BadMagic100 Date: Wed, 22 Nov 2023 06:15:09 -0800 Subject: [PATCH 044/104] Hollow Knight: Don't force mimics local (#2482) --- worlds/hk/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index c16a108cd169..f7e7e22e69dd 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -170,7 +170,6 @@ def generate_early(self): charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random) self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs) # world.exclude_locations[self.player].value.update(white_palace_locations) - world.local_items[self.player].value.add("Mimic_Grub") for term, data in cost_terms.items(): mini = getattr(world, f"Minimum{data.option}Price")[self.player] maxi = getattr(world, f"Maximum{data.option}Price")[self.player] From 3b357315ee8704f3727e50c28b67c712468531fb Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 22 Nov 2023 09:15:35 -0500 Subject: [PATCH 045/104] Git: Added file type .smc to gitignore (#2476) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index aaea45ce985a..022abe38fe40 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ *.z64 *.n64 *.nes +*.smc *.sms *.gb *.gbc From d1b22935b44f0de1f8eddb982e22d5522ef99394 Mon Sep 17 00:00:00 2001 From: Jarno Date: Wed, 22 Nov 2023 15:17:33 +0100 Subject: [PATCH 046/104] Timespinner: New options from TS Rando v1.25 + Logic fix (#2090) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/timespinner/Locations.py | 30 +++++------ worlds/timespinner/Options.py | 35 ++++++++++--- worlds/timespinner/PreCalculatedWeights.py | 32 +++++++----- worlds/timespinner/Regions.py | 59 ++++++++++------------ worlds/timespinner/__init__.py | 21 +++++--- 5 files changed, 103 insertions(+), 74 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 70c76b863842..7b378b4637fa 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Optional, Callable, NamedTuple +from typing import List, Optional, Callable, NamedTuple from BaseClasses import MultiWorld, CollectionState from .Options import is_option_enabled from .PreCalculatedWeights import PreCalculatedWeights @@ -11,11 +11,11 @@ class LocationData(NamedTuple): region: str name: str code: Optional[int] - rule: Callable[[CollectionState], bool] = lambda state: True + rule: Optional[Callable[[CollectionState], bool]] = None def get_location_datas(world: Optional[MultiWorld], player: Optional[int], - precalculated_weights: PreCalculatedWeights) -> Tuple[LocationData, ...]: + precalculated_weights: PreCalculatedWeights) -> List[LocationData]: flooded: PreCalculatedWeights = precalculated_weights logic = TimespinnerLogic(world, player, precalculated_weights) @@ -88,9 +88,9 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Military Fortress (hangar)', 'Military Fortress: Soldiers bridge', 1337060), LocationData('Military Fortress (hangar)', 'Military Fortress: Giantess room', 1337061), LocationData('Military Fortress (hangar)', 'Military Fortress: Giantess bridge', 1337062), - LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 2', 1337063, lambda state: logic.has_doublejump(state) and logic.has_keycard_B(state)), - LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 1', 1337064, lambda state: logic.has_doublejump(state) and logic.has_keycard_B(state)), - LocationData('Military Fortress (hangar)', 'Military Fortress: Pedestal', 1337065, lambda state: logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state)), + LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 2', 1337063, lambda state: logic.has_keycard_B(state) and (state.has('Water Mask', player) if flooded.flood_lab else logic.has_doublejump(state))), + LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 1', 1337064, lambda state: logic.has_keycard_B(state) and (state.has('Water Mask', player) if flooded.flood_lab else logic.has_doublejump(state))), + LocationData('Military Fortress (hangar)', 'Military Fortress: Pedestal', 1337065, lambda state: state.has('Water Mask', player) if flooded.flood_lab else (logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state))), LocationData('The lab', 'Lab: Coffee break', 1337066), LocationData('The lab', 'Lab: Lower trash right', 1337067, logic.has_doublejump), LocationData('The lab', 'Lab: Lower trash left', 1337068, logic.has_upwarddash), @@ -139,17 +139,17 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Lower Lake Serene', 'Lake Serene (Lower): Under the eels', 1337106), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Water spikes room', 1337107), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater secret', 1337108, logic.can_break_walls), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: not flooded.dry_lake_serene or logic.has_doublejump_of_npc(state)), + LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: flooded.flood_lake_serene or logic.has_doublejump_of_npc(state)), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Past the eels', 1337110), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: not flooded.dry_lake_serene or logic.has_doublejump(state)), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: not flooded.flood_maw or logic.has_doublejump(state)), + LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: flooded.flood_lake_serene or logic.has_doublejump(state)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: flooded.flood_maw or logic.has_doublejump(state)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Secret room', 1337113, lambda state: logic.can_break_walls(state) and (not flooded.flood_maw or state.has('Water Mask', player))), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Bottom left room', 1337114, lambda state: not flooded.flood_maw or state.has('Water Mask', player)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Single shroom room', 1337115), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 1', 1337116, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 2', 1337117, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 1', 1337116, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 2', 1337117, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Pedestal', 1337120, lambda state: not flooded.flood_maw or state.has('Water Mask', player)), LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Last chance before Maw', 1337121, lambda state: state.has('Water Mask', player) if flooded.flood_maw else logic.has_doublejump(state)), LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Plasma Crystal', 1337173, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (not flooded.flood_maw or state.has('Water Mask', player))), @@ -197,7 +197,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Ancient Pyramid (entrance)', 'Ancient Pyramid: Why not it\'s right there', 1337246), LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Conviction guarded room', 1337247), LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Pit secret room', 1337248, lambda state: logic.can_break_walls(state) and (not flooded.flood_pyramid_shaft or state.has('Water Mask', player))), - LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Regret chest', 1337249, lambda state: logic.can_break_walls(state) and (not flooded.flood_pyramid_shaft or state.has('Water Mask', player))), + LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Regret chest', 1337249, lambda state: logic.can_break_walls(state) and (state.has('Water Mask', player) if flooded.flood_pyramid_shaft else logic.has_doublejump(state))), LocationData('Ancient Pyramid (right)', 'Ancient Pyramid: Nightmare Door chest', 1337236, lambda state: not flooded.flood_pyramid_back or state.has('Water Mask', player)), LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId, lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player) and (not flooded.flood_pyramid_back or state.has('Water Mask', player))) ] @@ -271,4 +271,4 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Ifrit\'s Lair', 'Ifrit: Post fight (chest)', 1337245), ) - return tuple(location_table) + return location_table diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 8b111849442c..f7921fcb81e0 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -54,14 +54,23 @@ class LoreChecks(Toggle): display_name = "Lore Checks" -class BossRando(Toggle): - "Shuffles the positions of all bosses." +class BossRando(Choice): + "Wheter all boss locations are shuffled, and if their damage/hp should be scaled." display_name = "Boss Randomization" + option_off = 0 + option_scaled = 1 + option_unscaled = 2 + alias_true = 1 -class BossScaling(DefaultOnToggle): - "When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Recommended)" - display_name = "Scale Random Boss Stats" +class EnemyRando(Choice): + "Wheter enemies will be randomized, and if their damage/hp should be scaled." + display_name = "Enemy Randomization" + option_off = 0 + option_scaled = 1 + option_unscaled = 2 + option_ryshia = 3 + alias_true = 1 class DamageRando(Choice): @@ -336,6 +345,7 @@ def rising_tide_option(location: str, with_save_point_option: bool = False) -> D class RisingTidesOverrides(OptionDict): """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on. Areas that are not specified will roll with the default 33% chance of getting flooded or drained""" + display_name = "Rising Tides Overrides" schema = Schema({ **rising_tide_option("Xarion"), **rising_tide_option("Maw"), @@ -345,9 +355,10 @@ class RisingTidesOverrides(OptionDict): **rising_tide_option("CastleBasement", with_save_point_option=True), **rising_tide_option("CastleCourtyard"), **rising_tide_option("LakeDesolation"), - **rising_tide_option("LakeSerene") + **rising_tide_option("LakeSerene"), + **rising_tide_option("LakeSereneBridge"), + **rising_tide_option("Lab"), }) - display_name = "Rising Tides Overrides" default = { "Xarion": { "Dry": 67, "Flooded": 33 }, "Maw": { "Dry": 67, "Flooded": 33 }, @@ -358,6 +369,8 @@ class RisingTidesOverrides(OptionDict): "CastleCourtyard": { "Dry": 67, "Flooded": 33 }, "LakeDesolation": { "Dry": 67, "Flooded": 33 }, "LakeSerene": { "Dry": 33, "Flooded": 67 }, + "LakeSereneBridge": { "Dry": 67, "Flooded": 33 }, + "Lab": { "Dry": 67, "Flooded": 33 }, } @@ -383,6 +396,11 @@ class Traps(OptionList): default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" ] +class PresentAccessWithWheelAndSpindle(Toggle): + """When inverted, allows using the refugee camp warp when both the Timespinner Wheel and Spindle is acquired.""" + display_name = "Past Wheel & Spindle Warp" + + # Some options that are available in the timespinner randomizer arent currently implemented timespinner_options: Dict[str, Option] = { "StartWithJewelryBox": StartWithJewelryBox, @@ -396,7 +414,7 @@ class Traps(OptionList): "Cantoran": Cantoran, "LoreChecks": LoreChecks, "BossRando": BossRando, - "BossScaling": BossScaling, + "EnemyRando": EnemyRando, "DamageRando": DamageRando, "DamageRandoOverrides": DamageRandoOverrides, "HpCap": HpCap, @@ -419,6 +437,7 @@ class Traps(OptionList): "UnchainedKeys": UnchainedKeys, "TrapChance": TrapChance, "Traps": Traps, + "PresentAccessWithWheelAndSpindle": PresentAccessWithWheelAndSpindle, "DeathLink": DeathLink, } diff --git a/worlds/timespinner/PreCalculatedWeights.py b/worlds/timespinner/PreCalculatedWeights.py index 64243e25edcc..ff7f031d3b67 100644 --- a/worlds/timespinner/PreCalculatedWeights.py +++ b/worlds/timespinner/PreCalculatedWeights.py @@ -1,4 +1,4 @@ -from typing import Tuple, Dict, Union +from typing import Tuple, Dict, Union, List from BaseClasses import MultiWorld from .Options import timespinner_options, is_option_enabled, get_option_value @@ -17,7 +17,9 @@ class PreCalculatedWeights: flood_moat: bool flood_courtyard: bool flood_lake_desolation: bool - dry_lake_serene: bool + flood_lake_serene: bool + flood_lake_serene_bridge: bool + flood_lab: bool def __init__(self, world: MultiWorld, player: int): if world and is_option_enabled(world, player, "RisingTides"): @@ -32,8 +34,9 @@ def __init__(self, world: MultiWorld, player: int): self.flood_moat, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat") self.flood_courtyard, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard") self.flood_lake_desolation, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation") - flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") - self.dry_lake_serene = not flood_lake_serene + self.flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") + self.flood_lake_serene_bridge, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSereneBridge") + self.flood_lab, _ = self.roll_flood_setting(world, player, weights_overrrides, "Lab") else: self.flood_basement = False self.flood_basement_high = False @@ -44,30 +47,32 @@ def __init__(self, world: MultiWorld, player: int): self.flood_moat = False self.flood_courtyard = False self.flood_lake_desolation = False - self.dry_lake_serene = False + self.flood_lake_serene = True + self.flood_lake_serene_bridge = False + self.flood_lab = False self.pyramid_keys_unlock, self.present_key_unlock, self.past_key_unlock, self.time_key_unlock = \ - self.get_pyramid_keys_unlocks(world, player, self.flood_maw) + self.get_pyramid_keys_unlocks(world, player, self.flood_maw, self.flood_xarion) @staticmethod - def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: bool) -> Tuple[str, str, str, str]: - present_teleportation_gates: Tuple[str, ...] = ( + def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: bool, is_xarion_flooded: bool) -> Tuple[str, str, str, str]: + present_teleportation_gates: List[str] = [ "GateKittyBoss", "GateLeftLibrary", "GateMilitaryGate", "GateSealedCaves", "GateSealedSirensCave", "GateLakeDesolation" - ) + ] - past_teleportation_gates: Tuple[str, ...] = ( + past_teleportation_gates: List[str] = [ "GateLakeSereneRight", "GateAccessToPast", "GateCastleRamparts", "GateCastleKeep", "GateRoyalTowers", "GateCavesOfBanishment" - ) + ] ancient_pyramid_teleportation_gates: Tuple[str, ...] = ( "GateGyre", @@ -84,7 +89,10 @@ def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: boo ) if not is_maw_flooded: - past_teleportation_gates += ("GateMaw", ) + past_teleportation_gates.append("GateMaw") + + if not is_xarion_flooded: + present_teleportation_gates.append("GateXarion") if is_option_enabled(world, player, "Inverted"): all_gates: Tuple[str, ...] = present_teleportation_gates diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 905cae867ebe..fc7535642949 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -1,4 +1,4 @@ -from typing import List, Set, Dict, Tuple, Optional, Callable +from typing import List, Set, Dict, Optional, Callable from BaseClasses import CollectionState, MultiWorld, Region, Entrance, Location from .Options import is_option_enabled from .Locations import LocationData, get_location_datas @@ -7,9 +7,8 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights): - locationn_datas: Tuple[LocationData] = get_location_datas(world, player, precalculated_weights) - - locations_per_region: Dict[str, List[LocationData]] = split_location_datas_per_region(locationn_datas) + locations_per_region: Dict[str, List[LocationData]] = split_location_datas_per_region( + get_location_datas(world, player, precalculated_weights)) regions = [ create_region(world, player, locations_per_region, 'Menu'), @@ -32,7 +31,6 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w create_region(world, player, locations_per_region, 'The lab (upper)'), create_region(world, player, locations_per_region, 'Emperors tower'), create_region(world, player, locations_per_region, 'Skeleton Shaft'), - create_region(world, player, locations_per_region, 'Sealed Caves (upper)'), create_region(world, player, locations_per_region, 'Sealed Caves (Xarion)'), create_region(world, player, locations_per_region, 'Refugee Camp'), create_region(world, player, locations_per_region, 'Forest'), @@ -63,7 +61,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w if __debug__: throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) - + world.regions += regions connectStartingRegion(world, player) @@ -71,9 +69,9 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w flooded: PreCalculatedWeights = precalculated_weights logic = TimespinnerLogic(world, player, precalculated_weights) - connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: logic.has_timestop(state) or state.has('Talaria Attachment', player) or flooded.flood_lake_desolation) + connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: flooded.flood_lake_desolation or logic.has_timestop(state) or state.has('Talaria Attachment', player)) connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) - connect(world, player, 'Lake desolation', 'Skeleton Shaft', lambda state: logic.has_doublejump(state) or flooded.flood_lake_desolation) + connect(world, player, 'Lake desolation', 'Skeleton Shaft', lambda state: flooded.flood_lake_desolation or logic.has_doublejump(state)) connect(world, player, 'Lake desolation', 'Space time continuum', logic.has_teleport) connect(world, player, 'Upper lake desolation', 'Lake desolation') connect(world, player, 'Upper lake desolation', 'Eastern lake desolation') @@ -109,40 +107,38 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w connect(world, player, 'Military Fortress', 'Temporal Gyre', lambda state: state.has('Timespinner Wheel', player)) connect(world, player, 'Military Fortress', 'Military Fortress (hangar)', logic.has_doublejump) connect(world, player, 'Military Fortress (hangar)', 'Military Fortress') - connect(world, player, 'Military Fortress (hangar)', 'The lab', lambda state: logic.has_keycard_B(state) and logic.has_doublejump(state)) + connect(world, player, 'Military Fortress (hangar)', 'The lab', lambda state: logic.has_keycard_B(state) and (state.has('Water Mask', player) if flooded.flood_lab else logic.has_doublejump(state))) connect(world, player, 'Temporal Gyre', 'Military Fortress') connect(world, player, 'The lab', 'Military Fortress') connect(world, player, 'The lab', 'The lab (power off)', logic.has_doublejump_of_npc) - connect(world, player, 'The lab (power off)', 'The lab') + connect(world, player, 'The lab (power off)', 'The lab', lambda state: not flooded.flood_lab or state.has('Water Mask', player)) connect(world, player, 'The lab (power off)', 'The lab (upper)', logic.has_forwarddash_doublejump) connect(world, player, 'The lab (upper)', 'The lab (power off)') connect(world, player, 'The lab (upper)', 'Emperors tower', logic.has_forwarddash_doublejump) connect(world, player, 'The lab (upper)', 'Ancient Pyramid (entrance)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player)) connect(world, player, 'Emperors tower', 'The lab (upper)') connect(world, player, 'Skeleton Shaft', 'Lake desolation') - connect(world, player, 'Skeleton Shaft', 'Sealed Caves (upper)', logic.has_keycard_A) + connect(world, player, 'Skeleton Shaft', 'Sealed Caves (Xarion)', logic.has_keycard_A) connect(world, player, 'Skeleton Shaft', 'Space time continuum', logic.has_teleport) - connect(world, player, 'Sealed Caves (upper)', 'Skeleton Shaft') - connect(world, player, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: logic.has_teleport(state) or logic.has_doublejump(state)) - connect(world, player, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', logic.has_doublejump) + connect(world, player, 'Sealed Caves (Xarion)', 'Skeleton Shaft') connect(world, player, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport) connect(world, player, 'Refugee Camp', 'Forest') - #connect(world, player, 'Refugee Camp', 'Library', lambda state: not is_option_enabled(world, player, "Inverted")) + connect(world, player, 'Refugee Camp', 'Library', lambda state: is_option_enabled(world, player, "Inverted") and is_option_enabled(world, player, "PresentAccessWithWheelAndSpindle") and state.has_all({'Timespinner Wheel', 'Timespinner Spindle'}, player)) connect(world, player, 'Refugee Camp', 'Space time continuum', logic.has_teleport) connect(world, player, 'Forest', 'Refugee Camp') - connect(world, player, 'Forest', 'Left Side forest Caves', lambda state: state.has('Talaria Attachment', player) or logic.has_timestop(state)) + connect(world, player, 'Forest', 'Left Side forest Caves', lambda state: flooded.flood_lake_serene_bridge or state.has('Talaria Attachment', player) or logic.has_timestop(state)) connect(world, player, 'Forest', 'Caves of Banishment (Sirens)') connect(world, player, 'Forest', 'Castle Ramparts') connect(world, player, 'Left Side forest Caves', 'Forest') connect(world, player, 'Left Side forest Caves', 'Upper Lake Serene', logic.has_timestop) - connect(world, player, 'Left Side forest Caves', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) + connect(world, player, 'Left Side forest Caves', 'Lower Lake Serene', lambda state: not flooded.flood_lake_serene or state.has('Water Mask', player)) connect(world, player, 'Left Side forest Caves', 'Space time continuum', logic.has_teleport) connect(world, player, 'Upper Lake Serene', 'Left Side forest Caves') - connect(world, player, 'Upper Lake Serene', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) + connect(world, player, 'Upper Lake Serene', 'Lower Lake Serene', lambda state: not flooded.flood_lake_serene or state.has('Water Mask', player)) connect(world, player, 'Lower Lake Serene', 'Upper Lake Serene') connect(world, player, 'Lower Lake Serene', 'Left Side forest Caves') - connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)', lambda state: not flooded.dry_lake_serene or logic.has_doublejump(state)) - connect(world, player, 'Caves of Banishment (upper)', 'Upper Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) + connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)', lambda state: flooded.flood_lake_serene or logic.has_doublejump(state)) + connect(world, player, 'Caves of Banishment (upper)', 'Lower Lake Serene', lambda state: not flooded.flood_lake_serene or state.has('Water Mask', player)) connect(world, player, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: logic.has_doublejump(state) or state.has_any({'Gas Mask', 'Talaria Attachment'} or logic.has_teleport(state), player)) connect(world, player, 'Caves of Banishment (upper)', 'Space time continuum', logic.has_teleport) connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: logic.has_doublejump(state) if not flooded.flood_maw else state.has('Water Mask', player)) @@ -153,7 +149,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w connect(world, player, 'Castle Ramparts', 'Castle Keep') connect(world, player, 'Castle Ramparts', 'Space time continuum', logic.has_teleport) connect(world, player, 'Castle Keep', 'Castle Ramparts') - connect(world, player, 'Castle Keep', 'Castle Basement', lambda state: state.has('Water Mask', player) or not flooded.flood_basement) + connect(world, player, 'Castle Keep', 'Castle Basement', lambda state: not flooded.flood_basement or state.has('Water Mask', player)) connect(world, player, 'Castle Keep', 'Royal towers (lower)', logic.has_doublejump) connect(world, player, 'Castle Keep', 'Space time continuum', logic.has_teleport) connect(world, player, 'Royal towers (lower)', 'Castle Keep') @@ -165,14 +161,15 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w #connect(world, player, 'Ancient Pyramid (entrance)', 'The lab (upper)', lambda state: not is_option_enabled(world, player, "EnterSandman")) connect(world, player, 'Ancient Pyramid (entrance)', 'Ancient Pyramid (left)', logic.has_doublejump) connect(world, player, 'Ancient Pyramid (left)', 'Ancient Pyramid (entrance)') - connect(world, player, 'Ancient Pyramid (left)', 'Ancient Pyramid (right)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) - connect(world, player, 'Ancient Pyramid (right)', 'Ancient Pyramid (left)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) + connect(world, player, 'Ancient Pyramid (left)', 'Ancient Pyramid (right)', lambda state: flooded.flood_pyramid_shaft or logic.has_upwarddash(state)) + connect(world, player, 'Ancient Pyramid (right)', 'Ancient Pyramid (left)', lambda state: flooded.flood_pyramid_shaft or logic.has_upwarddash(state)) connect(world, player, 'Space time continuum', 'Lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateLakeDesolation")) connect(world, player, 'Space time continuum', 'Lower lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateKittyBoss")) connect(world, player, 'Space time continuum', 'Library', lambda state: logic.can_teleport_to(state, "Present", "GateLeftLibrary")) connect(world, player, 'Space time continuum', 'Varndagroth tower right (lower)', lambda state: logic.can_teleport_to(state, "Present", "GateMilitaryGate")) connect(world, player, 'Space time continuum', 'Skeleton Shaft', lambda state: logic.can_teleport_to(state, "Present", "GateSealedCaves")) connect(world, player, 'Space time continuum', 'Sealed Caves (Sirens)', lambda state: logic.can_teleport_to(state, "Present", "GateSealedSirensCave")) + connect(world, player, 'Space time continuum', 'Sealed Caves (Xarion)', lambda state: logic.can_teleport_to(state, "Present", "GateXarion")) connect(world, player, 'Space time continuum', 'Upper Lake Serene', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneLeft")) connect(world, player, 'Space time continuum', 'Left Side forest Caves', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneRight")) connect(world, player, 'Space time continuum', 'Refugee Camp', lambda state: logic.can_teleport_to(state, "Past", "GateAccessToPast")) @@ -204,12 +201,13 @@ def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: def create_location(player: int, location_data: LocationData, region: Region) -> Location: location = Location(player, location_data.name, location_data.code, region) - location.access_rule = location_data.rule + + if location_data.rule: + location.access_rule = location_data.rule if id is None: location.event = True location.locked = True - return location @@ -220,7 +218,6 @@ def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str for location_data in locations_per_region[name]: location = create_location(player, location_data, region) region.locations.append(location) - return region @@ -237,11 +234,9 @@ def connectStartingRegion(world: MultiWorld, player: int): menu_to_tutorial = Entrance(player, 'Tutorial', menu) menu_to_tutorial.connect(tutorial) menu.exits.append(menu_to_tutorial) - tutorial_to_start = Entrance(player, 'Start Game', tutorial) tutorial_to_start.connect(starting_region) tutorial.exits.append(tutorial_to_start) - teleport_back_to_start = Entrance(player, 'Teleport back to start', space_time_continuum) teleport_back_to_start.connect(starting_region) space_time_continuum.exits.append(teleport_back_to_start) @@ -249,7 +244,7 @@ def connectStartingRegion(world: MultiWorld, player: int): def connect(world: MultiWorld, player: int, source: str, target: str, rule: Optional[Callable[[CollectionState], bool]] = None): - + sourceRegion = world.get_region(source, player) targetRegion = world.get_region(target, player) @@ -257,15 +252,13 @@ def connect(world: MultiWorld, player: int, source: str, target: str, if rule: connection.access_rule = rule - sourceRegion.exits.append(connection) connection.connect(targetRegion) -def split_location_datas_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]: +def split_location_datas_per_region(locations: List[LocationData]) -> Dict[str, List[LocationData]]: per_region: Dict[str, List[LocationData]] = {} for location in locations: per_region.setdefault(location.region, []).append(location) - - return per_region + return per_region \ No newline at end of file diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 24230862bdf6..ff7b3515e605 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -39,9 +39,9 @@ class TimespinnerWorld(World): option_definitions = timespinner_options game = "Timespinner" topology_present = True - data_version = 11 + data_version = 12 web = TimespinnerWebWorld() - required_client_version = (0, 3, 7) + required_client_version = (0, 4, 2) item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {location.name: location.code for location in get_location_datas(None, None, None)} @@ -108,7 +108,9 @@ def fill_slot_data(self) -> Dict[str, object]: slot_data["CastleMoat"] = self.precalculated_weights.flood_moat slot_data["CastleCourtyard"] = self.precalculated_weights.flood_courtyard slot_data["LakeDesolation"] = self.precalculated_weights.flood_lake_desolation - slot_data["DryLakeSerene"] = self.precalculated_weights.dry_lake_serene + slot_data["DryLakeSerene"] = not self.precalculated_weights.flood_lake_serene + slot_data["LakeSereneBridge"] = self.precalculated_weights.flood_lake_serene_bridge + slot_data["Lab"] = self.precalculated_weights.flood_lab return slot_data @@ -144,8 +146,12 @@ def write_spoiler_header(self, spoiler_handle: TextIO) -> None: flooded_areas.append("Castle Courtyard") if self.precalculated_weights.flood_lake_desolation: flooded_areas.append("Lake Desolation") - if not self.precalculated_weights.dry_lake_serene: + if self.precalculated_weights.flood_lake_serene: flooded_areas.append("Lake Serene") + if self.precalculated_weights.flood_lake_serene_bridge: + flooded_areas.append("Lake Serene Bridge") + if self.precalculated_weights.flood_lab: + flooded_areas.append("Lab") if len(flooded_areas) == 0: flooded_areas_string: str = "None" @@ -220,15 +226,18 @@ def get_excluded_items(self) -> Set[str]: def assign_starter_items(self, excluded_items: Set[str]) -> None: non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value + local_items: Set[str] = self.multiworld.local_items[self.player].value - local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item not in non_local_items) + local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if + item in local_items or not item in non_local_items) if not local_starter_melee_weapons: if 'Plasma Orb' in non_local_items: raise Exception("Atleast one melee orb must be local") else: local_starter_melee_weapons = ('Plasma Orb',) - local_starter_spells = tuple(item for item in starter_spells if item not in non_local_items) + local_starter_spells = tuple(item for item in starter_spells if + item in local_items or not item in non_local_items) if not local_starter_spells: if 'Lightwall' in non_local_items: raise Exception("Atleast one spell must be local") From 01b566b798ac568c35a35c78576a5cd0c771477d Mon Sep 17 00:00:00 2001 From: zig-for Date: Wed, 22 Nov 2023 06:29:33 -0800 Subject: [PATCH 047/104] LADX: Text shuffle (#2051) --- worlds/ladx/LADXR/generator.py | 17 +++++++++ worlds/ladx/LADXR/patches/owl.py | 8 +++-- worlds/ladx/LADXR/patches/phone.py | 57 +++++++++++++++--------------- worlds/ladx/LADXR/pointerTable.py | 5 ++- worlds/ladx/Options.py | 8 +++++ 5 files changed, 63 insertions(+), 32 deletions(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index 72d631da86a0..0406ad51f890 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -3,6 +3,7 @@ import importlib.machinery import os import pkgutil +from collections import defaultdict from .romTables import ROMWithTables from . import assembler @@ -322,6 +323,22 @@ def gen_hint(): if args.doubletrouble: patches.enemies.doubleTrouble(rom) + if ap_settings["text_shuffle"]: + buckets = defaultdict(list) + # For each ROM bank, shuffle text within the bank + for n, data in enumerate(rom.texts._PointerTable__data): + # Don't muck up which text boxes are questions and which are statements + if type(data) != int and data and data != b'\xFF': + buckets[(rom.texts._PointerTable__banks[n], data[len(data) - 1] == 0xfe)].append((n, data)) + for bucket in buckets.values(): + # For each bucket, make a copy and shuffle + shuffled = bucket.copy() + rnd.shuffle(shuffled) + # Then put new text in + for bucket_idx, (orig_idx, data) in enumerate(bucket): + rom.texts[shuffled[bucket_idx][0]] = data + + if ap_settings["trendy_game"] != TrendyGame.option_normal: # TODO: if 0 or 4, 5, remove inaccurate conveyor tiles diff --git a/worlds/ladx/LADXR/patches/owl.py b/worlds/ladx/LADXR/patches/owl.py index b22386a6cb8f..47e575191a31 100644 --- a/worlds/ladx/LADXR/patches/owl.py +++ b/worlds/ladx/LADXR/patches/owl.py @@ -11,15 +11,17 @@ def removeOwlEvents(rom): re.removeEntities(0x41) re.store(rom) # Clear texts used by the owl. Potentially reused somewhere o else. - rom.texts[0x0D9] = b'\xff' # used by boomerang # 1 Used by empty chest (master stalfos message) # 8 unused (0x0C0-0x0C7) # 1 used by bowwow in chest # 1 used by item for other player message # 2 used by arrow chest messages # 2 used by tunics - for idx in range(0x0BE, 0x0CE): - rom.texts[idx] = b'\xff' + + # Undoing this, we use it for text shuffle now + #rom.texts[0x0D9] = b'\xff' # used by boomerang + # for idx in range(0x0BE, 0x0CE): + # rom.texts[idx] = b'\xff' # Patch the owl entity into a ghost to allow refill of powder/bombs/arrows diff --git a/worlds/ladx/LADXR/patches/phone.py b/worlds/ladx/LADXR/patches/phone.py index f38745606c38..a2f3939a08a1 100644 --- a/worlds/ladx/LADXR/patches/phone.py +++ b/worlds/ladx/LADXR/patches/phone.py @@ -2,34 +2,35 @@ def patchPhone(rom): - rom.texts[0x141] = b"" - rom.texts[0x142] = b"" - rom.texts[0x143] = b"" - rom.texts[0x144] = b"" - rom.texts[0x145] = b"" - rom.texts[0x146] = b"" - rom.texts[0x147] = b"" - rom.texts[0x148] = b"" - rom.texts[0x149] = b"" - rom.texts[0x14A] = b"" - rom.texts[0x14B] = b"" - rom.texts[0x14C] = b"" - rom.texts[0x14D] = b"" - rom.texts[0x14E] = b"" - rom.texts[0x14F] = b"" - rom.texts[0x16E] = b"" - rom.texts[0x1FD] = b"" - rom.texts[0x228] = b"" - rom.texts[0x229] = b"" - rom.texts[0x22A] = b"" - rom.texts[0x240] = b"" - rom.texts[0x241] = b"" - rom.texts[0x242] = b"" - rom.texts[0x243] = b"" - rom.texts[0x244] = b"" - rom.texts[0x245] = b"" - rom.texts[0x247] = b"" - rom.texts[0x248] = b"" + # reenabled for text shuffle +# rom.texts[0x141] = b"" +# rom.texts[0x142] = b"" +# rom.texts[0x143] = b"" +# rom.texts[0x144] = b"" +# rom.texts[0x145] = b"" +# rom.texts[0x146] = b"" +# rom.texts[0x147] = b"" +# rom.texts[0x148] = b"" +# rom.texts[0x149] = b"" +# rom.texts[0x14A] = b"" +# rom.texts[0x14B] = b"" +# rom.texts[0x14C] = b"" +# rom.texts[0x14D] = b"" +# rom.texts[0x14E] = b"" +# rom.texts[0x14F] = b"" +# rom.texts[0x16E] = b"" +# rom.texts[0x1FD] = b"" +# rom.texts[0x228] = b"" +# rom.texts[0x229] = b"" +# rom.texts[0x22A] = b"" +# rom.texts[0x240] = b"" +# rom.texts[0x241] = b"" +# rom.texts[0x242] = b"" +# rom.texts[0x243] = b"" +# rom.texts[0x244] = b"" +# rom.texts[0x245] = b"" +# rom.texts[0x247] = b"" +# rom.texts[0x248] = b"" rom.patch(0x06, 0x2A8F, 0x2BBC, ASM(""" ; We use $DB6D to store which tunics we have. This is normally the Dungeon9 instrument, which does not exist. ld a, [$DC0F] diff --git a/worlds/ladx/LADXR/pointerTable.py b/worlds/ladx/LADXR/pointerTable.py index 9b8d49466c02..a1a92ba1780b 100644 --- a/worlds/ladx/LADXR/pointerTable.py +++ b/worlds/ladx/LADXR/pointerTable.py @@ -116,7 +116,10 @@ def store(self, rom): rom.banks[ptr_bank][ptr_addr] = pointer & 0xFF rom.banks[ptr_bank][ptr_addr + 1] = (pointer >> 8) | 0x40 - for n, s in enumerate(self.__data): + data = list(enumerate(self.__data)) + data.sort(key=lambda t: type(t[1]) == int or -len(t[1])) + + for n, s in data: if isinstance(s, int): pointer = s else: diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index f80ad1552001..f1d5c5130168 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -43,6 +43,12 @@ class TradeQuest(DefaultOffToggle, LADXROption): display_name = "Trade Quest" ladxr_name = "tradequest" +class TextShuffle(DefaultOffToggle): + """ + [On] Shuffles all the text in the game + [Off] (default) doesn't shuffle them. + """ + class Rooster(DefaultOnToggle, LADXROption): """ [On] Adds the rooster to the item pool. @@ -431,6 +437,7 @@ class AdditionalWarpPoints(DefaultOffToggle): 'trendy_game': TrendyGame, 'gfxmod': GfxMod, 'palette': Palette, + 'text_shuffle': TextShuffle, 'shuffle_nightmare_keys': ShuffleNightmareKeys, 'shuffle_small_keys': ShuffleSmallKeys, 'shuffle_maps': ShuffleMaps, @@ -439,4 +446,5 @@ class AdditionalWarpPoints(DefaultOffToggle): 'music_change_condition': MusicChangeCondition, 'nag_messages': NagMessages, 'ap_title_screen': APTitleScreen, + } From 79406faf27400d03d48259899aac2ab42014c688 Mon Sep 17 00:00:00 2001 From: Rjosephson Date: Wed, 22 Nov 2023 08:20:32 -0700 Subject: [PATCH 048/104] RoR2: 1.3.0 content update (#2425) --- worlds/ror2/Items.py | 194 ---------------- worlds/ror2/Locations.py | 119 ---------- worlds/ror2/RoR2Environments.py | 118 ---------- worlds/ror2/__init__.py | 246 +++++++++----------- worlds/ror2/docs/setup_en.md | 13 +- worlds/ror2/items.py | 309 +++++++++++++++++++++++++ worlds/ror2/locations.py | 89 +++++++ worlds/ror2/{Options.py => options.py} | 124 +++++++++- worlds/ror2/{Regions.py => regions.py} | 95 ++++++-- worlds/ror2/ror2environments.py | 118 ++++++++++ worlds/ror2/{Rules.py => rules.py} | 130 ++++++----- worlds/ror2/test/__init__.py | 5 + worlds/ror2/test/test_any_goal.py | 26 +++ worlds/ror2/test/test_classic.py | 7 + worlds/ror2/test/test_limbo_goal.py | 15 ++ worlds/ror2/test/test_mithrix_goal.py | 25 ++ worlds/ror2/test/test_voidling_goal.py | 28 +++ 17 files changed, 1002 insertions(+), 659 deletions(-) delete mode 100644 worlds/ror2/Items.py delete mode 100644 worlds/ror2/Locations.py delete mode 100644 worlds/ror2/RoR2Environments.py create mode 100644 worlds/ror2/items.py create mode 100644 worlds/ror2/locations.py rename worlds/ror2/{Options.py => options.py} (73%) rename worlds/ror2/{Regions.py => regions.py} (59%) create mode 100644 worlds/ror2/ror2environments.py rename worlds/ror2/{Rules.py => rules.py} (60%) create mode 100644 worlds/ror2/test/__init__.py create mode 100644 worlds/ror2/test/test_any_goal.py create mode 100644 worlds/ror2/test/test_classic.py create mode 100644 worlds/ror2/test/test_limbo_goal.py create mode 100644 worlds/ror2/test/test_mithrix_goal.py create mode 100644 worlds/ror2/test/test_voidling_goal.py diff --git a/worlds/ror2/Items.py b/worlds/ror2/Items.py deleted file mode 100644 index 448e3272aef8..000000000000 --- a/worlds/ror2/Items.py +++ /dev/null @@ -1,194 +0,0 @@ -from BaseClasses import Item -from .Options import ItemWeights -from .RoR2Environments import * - - -class RiskOfRainItem(Item): - game: str = "Risk of Rain 2" - - -# 37000 - 37699, 38000 -item_table: Dict[str, int] = { - "Dio's Best Friend": 37001, - "Common Item": 37002, - "Uncommon Item": 37003, - "Legendary Item": 37004, - "Boss Item": 37005, - "Lunar Item": 37006, - "Equipment": 37007, - "Item Scrap, White": 37008, - "Item Scrap, Green": 37009, - "Item Scrap, Red": 37010, - "Item Scrap, Yellow": 37011, - "Void Item": 37012, - "Beads of Fealty": 37013 -} - -# 37700 - 37699 -################################################## -# environments - -environment_offest = 37700 - -# add ALL environments into the item table -environment_offset_table = shift_by_offset(environment_ALL_table, environment_offest) -item_table.update(shift_by_offset(environment_ALL_table, environment_offest)) -# use the sotv dlc in the item table so that all names can be looked up regardless of use - -# end of environments -################################################## - -default_weights: Dict[str, int] = { - "Item Scrap, Green": 16, - "Item Scrap, Red": 4, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 32, - "Common Item": 64, - "Uncommon Item": 32, - "Legendary Item": 8, - "Boss Item": 4, - "Lunar Item": 16, - "Void Item": 16, - "Equipment": 32 -} - -new_weights: Dict[str, int] = { - "Item Scrap, Green": 15, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 75, - "Uncommon Item": 40, - "Legendary Item": 10, - "Boss Item": 5, - "Lunar Item": 10, - "Void Item": 16, - "Equipment": 20 -} - -uncommon_weights: Dict[str, int] = { - "Item Scrap, Green": 45, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 45, - "Uncommon Item": 100, - "Legendary Item": 10, - "Boss Item": 5, - "Lunar Item": 15, - "Void Item": 16, - "Equipment": 20 -} - -legendary_weights: Dict[str, int] = { - "Item Scrap, Green": 15, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 50, - "Uncommon Item": 25, - "Legendary Item": 100, - "Boss Item": 5, - "Lunar Item": 15, - "Void Item": 16, - "Equipment": 20 -} - -lunartic_weights: Dict[str, int] = { - "Item Scrap, Green": 0, - "Item Scrap, Red": 0, - "Item Scrap, Yellow": 0, - "Item Scrap, White": 0, - "Common Item": 0, - "Uncommon Item": 0, - "Legendary Item": 0, - "Boss Item": 0, - "Lunar Item": 100, - "Void Item": 0, - "Equipment": 0 -} - -chaos_weights: Dict[str, int] = { - "Item Scrap, Green": 80, - "Item Scrap, Red": 45, - "Item Scrap, Yellow": 30, - "Item Scrap, White": 100, - "Common Item": 100, - "Uncommon Item": 70, - "Legendary Item": 30, - "Boss Item": 20, - "Lunar Item": 60, - "Void Item": 60, - "Equipment": 40 -} - -no_scraps_weights: Dict[str, int] = { - "Item Scrap, Green": 0, - "Item Scrap, Red": 0, - "Item Scrap, Yellow": 0, - "Item Scrap, White": 0, - "Common Item": 100, - "Uncommon Item": 40, - "Legendary Item": 15, - "Boss Item": 5, - "Lunar Item": 10, - "Void Item": 16, - "Equipment": 25 -} - -even_weights: Dict[str, int] = { - "Item Scrap, Green": 1, - "Item Scrap, Red": 1, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 1, - "Common Item": 1, - "Uncommon Item": 1, - "Legendary Item": 1, - "Boss Item": 1, - "Lunar Item": 1, - "Void Item": 1, - "Equipment": 1 -} - -scraps_only: Dict[str, int] = { - "Item Scrap, Green": 70, - "Item Scrap, White": 100, - "Item Scrap, Red": 30, - "Item Scrap, Yellow": 5, - "Common Item": 0, - "Uncommon Item": 0, - "Legendary Item": 0, - "Boss Item": 0, - "Lunar Item": 0, - "Void Item": 0, - "Equipment": 0 -} - -void_weights: Dict[str, int] = { - "Item Scrap, Green": 0, - "Item Scrap, Red": 0, - "Item Scrap, Yellow": 0, - "Item Scrap, White": 0, - "Common Item": 0, - "Uncommon Item": 0, - "Legendary Item": 0, - "Boss Item": 0, - "Lunar Item": 0, - "Void Item": 100, - "Equipment": 0 -} - -item_pool_weights: Dict[int, Dict[str, int]] = { - ItemWeights.option_default: default_weights, - ItemWeights.option_new: new_weights, - ItemWeights.option_uncommon: uncommon_weights, - ItemWeights.option_legendary: legendary_weights, - ItemWeights.option_lunartic: lunartic_weights, - ItemWeights.option_chaos: chaos_weights, - ItemWeights.option_no_scraps: no_scraps_weights, - ItemWeights.option_even: even_weights, - ItemWeights.option_scraps_only: scraps_only, - ItemWeights.option_void: void_weights, -} - -lookup_id_to_name: Dict[int, str] = {id: name for name, id in item_table.items()} diff --git a/worlds/ror2/Locations.py b/worlds/ror2/Locations.py deleted file mode 100644 index 7db3ceca73b3..000000000000 --- a/worlds/ror2/Locations.py +++ /dev/null @@ -1,119 +0,0 @@ -from typing import Tuple -from BaseClasses import Location -from .Options import TotalLocations -from .Options import ChestsPerEnvironment -from .Options import ShrinesPerEnvironment -from .Options import ScavengersPerEnvironment -from .Options import ScannersPerEnvironment -from .Options import AltarsPerEnvironment -from .RoR2Environments import * - - -class RiskOfRainLocation(Location): - game: str = "Risk of Rain 2" - - -ror2_locations_start_id = 38000 - - -def get_classic_item_pickups(n: int) -> Dict[str, int]: - """Get n ItemPickups, capped at the max value for TotalLocations""" - n = max(n, 0) - n = min(n, TotalLocations.range_end) - return { f"ItemPickup{i+1}": ror2_locations_start_id+i for i in range(n) } - - -item_pickups = get_classic_item_pickups(TotalLocations.range_end) -location_table = item_pickups - - -def environment_abreviation(long_name:str) -> str: - """convert long environment names to initials""" - abrev = "" - # go through every word finding a letter (or number) for an initial - for word in long_name.split(): - initial = word[0] - for letter in word: - if letter.isalnum(): - initial = letter - break - abrev+= initial - return abrev - -# highest numbered orderedstages (this is so we can treat the easily caculate the check ids based on the environment and location "offset") -highest_orderedstage: int= max(compress_dict_list_horizontal(environment_orderedstages_table).values()) - -ror2_locations_start_orderedstage = ror2_locations_start_id + TotalLocations.range_end - -class orderedstage_location: - """A class to behave like a struct for storing the offsets of location types in the allocated space per orderedstage environments.""" - # TODO is there a better, more generic way to do this? - offset_ChestsPerEnvironment = 0 - offset_ShrinesPerEnvironment = offset_ChestsPerEnvironment + ChestsPerEnvironment.range_end - offset_ScavengersPerEnvironment = offset_ShrinesPerEnvironment + ShrinesPerEnvironment.range_end - offset_ScannersPerEnvironment = offset_ScavengersPerEnvironment + ScavengersPerEnvironment.range_end - offset_AltarsPerEnvironment = offset_ScannersPerEnvironment + ScannersPerEnvironment.range_end - - # total space allocated to the locations in a single orderedstage environment - allocation = offset_AltarsPerEnvironment + AltarsPerEnvironment.range_end - - def get_environment_locations(chests:int, shrines:int, scavengers:int, scanners:int, altars:int, environment: Tuple[str, int]) -> Dict[str, int]: - """Get the locations within a specific environment""" - environment_name = environment[0] - environment_index = environment[1] - locations = {} - - # due to this mapping, since environment ids are not consecutive, there are lots of "wasted" id numbers - # TODO perhaps a hashing algorithm could be used to compress this range and save "wasted" ids - environment_start_id = environment_index * orderedstage_location.allocation + ror2_locations_start_orderedstage - for n in range(chests): - locations.update({f"{environment_name}: Chest {n+1}": n + orderedstage_location.offset_ChestsPerEnvironment + environment_start_id}) - for n in range(shrines): - locations.update({f"{environment_name}: Shrine {n+1}": n + orderedstage_location.offset_ShrinesPerEnvironment + environment_start_id}) - for n in range(scavengers): - locations.update({f"{environment_name}: Scavenger {n+1}": n + orderedstage_location.offset_ScavengersPerEnvironment + environment_start_id}) - for n in range(scanners): - locations.update({f"{environment_name}: Radio Scanner {n+1}": n + orderedstage_location.offset_ScannersPerEnvironment + environment_start_id}) - for n in range(altars): - locations.update({f"{environment_name}: Newt Altar {n+1}": n + orderedstage_location.offset_AltarsPerEnvironment + environment_start_id}) - return locations - - def get_locations(chests:int, shrines:int, scavengers:int, scanners:int, altars:int, dlc_sotv:bool) -> Dict[str, int]: - """Get a dictionary of locations for the ordedstage environments with the locations from the parameters.""" - locations = {} - orderedstages = compress_dict_list_horizontal(environment_vanilla_orderedstages_table) - if(dlc_sotv): orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table)) - # for every environment, generate the respective locations - for environment_name, environment_index in orderedstages.items(): - # locations = locations | orderedstage_location.get_environment_locations( - locations.update(orderedstage_location.get_environment_locations( - chests=chests, - shrines=shrines, - scavengers=scavengers, - scanners=scanners, - altars=altars, - environment=(environment_name, environment_index) - )) - return locations - - def getall_locations(dlc_sotv:bool=True) -> Dict[str, int]: - """ - Get all locations in ordered stages. - Set dlc_sotv to true for the SOTV DLC to be included. - """ - # to get all locations, attempt using as many locations as possible - return orderedstage_location.get_locations( - chests=ChestsPerEnvironment.range_end, - shrines=ShrinesPerEnvironment.range_end, - scavengers=ScavengersPerEnvironment.range_end, - scanners=ScannersPerEnvironment.range_end, - altars=AltarsPerEnvironment.range_end, - dlc_sotv=dlc_sotv - ) - - -ror2_location_post_orderedstage = ror2_locations_start_orderedstage + highest_orderedstage*orderedstage_location.allocation -location_table.update(orderedstage_location.getall_locations()) -# use the sotv dlc in the lookup table so that all ids can be looked up regardless of use - -lookup_id_to_name: Dict[int, str] = {id: name for name, id in location_table.items()} diff --git a/worlds/ror2/RoR2Environments.py b/worlds/ror2/RoR2Environments.py deleted file mode 100644 index 2a9bf73e9805..000000000000 --- a/worlds/ror2/RoR2Environments.py +++ /dev/null @@ -1,118 +0,0 @@ -from typing import Dict, List, TypeVar - -# TODO probably move to Locations - -environment_vanilla_orderedstage_1_table: Dict[str, int] = { - "Distant Roost": 7, # blackbeach - "Distant Roost (2)": 8, # blackbeach2 - "Titanic Plains": 15, # golemplains - "Titanic Plains (2)": 16, # golemplains2 -} -environment_vanilla_orderedstage_2_table: Dict[str, int] = { - "Abandoned Aqueduct": 17, # goolake - "Wetland Aspect": 12, # foggyswamp -} -environment_vanilla_orderedstage_3_table: Dict[str, int] = { - "Rallypoint Delta": 13, # frozenwall - "Scorched Acres": 47, # wispgraveyard -} -environment_vanilla_orderedstage_4_table: Dict[str, int] = { - "Abyssal Depths": 10, # dampcavesimple - "Siren's Call": 37, # shipgraveyard - "Sundered Grove": 35, # rootjungle -} -environment_vanilla_orderedstage_5_table: Dict[str, int] = { - "Sky Meadow": 38, # skymeadow -} - -environment_vanilla_hidden_realm_table: Dict[str, int] = { - "Hidden Realm: Bulwark's Ambry": 5, # artifactworld - "Hidden Realm: Bazaar Between Time": 6, # bazaar - "Hidden Realm: Gilded Coast": 14, # goldshores - "Hidden Realm: A Moment, Whole": 27, # limbo - "Hidden Realm: A Moment, Fractured": 33, # mysteryspace -} - -environment_vanilla_special_table: Dict[str, int] = { - "Void Fields": 4, # arena - "Commencement": 32, # moon2 -} - -environment_sotv_orderedstage_1_table: Dict[str, int] = { - "Siphoned Forest": 39, # snowyforest -} -environment_sotv_orderedstage_2_table: Dict[str, int] = { - "Aphelian Sanctuary": 3, # ancientloft -} -environment_sotv_orderedstage_3_table: Dict[str, int] = { - "Sulfur Pools": 41, # sulfurpools -} -environment_sotv_orderedstage_4_table: Dict[str, int] = { } -environment_sotv_orderedstage_5_table: Dict[str, int] = { } - -# TODO idk much and idc much about simulacrum, is there a forced order or something? -environment_sotv_simulacrum_table: Dict[str, int] = { - "The Simulacrum (Aphelian Sanctuary)": 20, # itancientloft - "The Simulacrum (Abyssal Depths)": 21, # itdampcave - "The Simulacrum (Rallypoint Delta)": 22, # itfrozenwall - "The Simulacrum (Titanic Plains)": 23, # itgolemplains - "The Simulacrum (Abandoned Aqueduct)": 24, # itgoolake - "The Simulacrum (Commencement)": 25, # itmoon - "The Simulacrum (Sky Meadow)": 26, # itskymeadow -} - -environment_sotv_special_table: Dict[str, int] = { - "Void Locus": 46, # voidstage - "The Planetarium": 45, # voidraid -} - -X = TypeVar("X") -Y = TypeVar("Y") - - -def compress_dict_list_horizontal(list_of_dict: List[Dict[X, Y]]) -> Dict[X, Y]: - """Combine all dictionaries in a list together into one dictionary.""" - compressed: Dict[X,Y] = {} - for individual in list_of_dict: compressed.update(individual) - return compressed - -def collapse_dict_list_vertical(list_of_dict1: List[Dict[X, Y]], *args: List[Dict[X, Y]]) -> List[Dict[X, Y]]: - """Combine all parallel dictionaries in lists together to make a new list of dictionaries of the same length.""" - # find the length of the longest list - length = len(list_of_dict1) - for list_of_dictN in args: - length = max(length, len(list_of_dictN)) - - # create a combined list with a length the same as the longest list - collapsed = [{}] * (length) - # The reason the list_of_dict1 is not directly used to make collapsed is - # side effects can occur if all the dictionaries are not manually unioned. - - # merge contents from list_of_dict1 - for i in range(len(list_of_dict1)): - collapsed[i] = {**collapsed[i], **list_of_dict1[i]} - - # merge contents of remaining lists_of_dicts - for list_of_dictN in args: - for i in range(len(list_of_dictN)): - collapsed[i] = {**collapsed[i], **list_of_dictN[i]} - - return collapsed - -# TODO potentially these should only be created when they are directly referenced (unsure of the space/time cost of creating these initially) - -environment_vanilla_orderedstages_table = [ environment_vanilla_orderedstage_1_table, environment_vanilla_orderedstage_2_table, environment_vanilla_orderedstage_3_table, environment_vanilla_orderedstage_4_table, environment_vanilla_orderedstage_5_table ] -environment_vanilla_table = {**compress_dict_list_horizontal(environment_vanilla_orderedstages_table), **environment_vanilla_hidden_realm_table, **environment_vanilla_special_table} - -environment_sotv_orderedstages_table = [ environment_sotv_orderedstage_1_table, environment_sotv_orderedstage_2_table, environment_sotv_orderedstage_3_table, environment_sotv_orderedstage_4_table, environment_sotv_orderedstage_5_table ] -environment_sotv_non_simulacrum_table = {**compress_dict_list_horizontal(environment_sotv_orderedstages_table), **environment_sotv_special_table} -environment_sotv_table = {**environment_sotv_non_simulacrum_table} - -environment_non_orderedstages_table = {**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table, **environment_sotv_simulacrum_table, **environment_sotv_special_table} -environment_orderedstages_table = collapse_dict_list_vertical(environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table) -environment_ALL_table = {**environment_vanilla_table, **environment_sotv_table} - - -def shift_by_offset(dictionary: Dict[str, int], offset:int) -> Dict[str, int]: - """Shift all indexes in a dictionary by an offset""" - return {name:index+offset for name, index in dictionary.items()} diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 22c65dd9deb7..8735ce81fd5d 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -1,14 +1,16 @@ import string -from .Items import RiskOfRainItem, item_table, item_pool_weights, environment_offest -from .Locations import RiskOfRainLocation, get_classic_item_pickups, item_pickups, orderedstage_location -from .Rules import set_rules -from .RoR2Environments import * - -from BaseClasses import Region, Entrance, Item, ItemClassification, MultiWorld, Tutorial -from .Options import ItemWeights, ROR2Options +from .items import RiskOfRainItem, item_table, item_pool_weights, offset, filler_table, environment_offset +from .locations import RiskOfRainLocation, item_pickups, get_locations +from .rules import set_rules +from .ror2environments import environment_vanilla_table, environment_vanilla_orderedstages_table, \ + environment_sotv_orderedstages_table, environment_sotv_table, collapse_dict_list_vertical, shift_by_offset + +from BaseClasses import Item, ItemClassification, Tutorial +from .options import ItemWeights, ROR2Options from worlds.AutoWorld import World, WebWorld -from .Regions import create_regions +from .regions import create_explore_regions, create_classic_regions +from typing import List, Dict, Any class RiskOfWeb(WebWorld): @@ -18,7 +20,7 @@ class RiskOfWeb(WebWorld): "English", "setup_en.md", "setup/en", - ["Ijwu"] + ["Ijwu", "Kindasneaki"] )] @@ -32,38 +34,53 @@ class RiskOfRainWorld(World): options_dataclass = ROR2Options options: ROR2Options topology_present = False - - item_name_to_id = item_table + item_name_to_id = {name: data.code for name, data in item_table.items()} + item_name_groups = { + "Stages": {name for name, data in item_table.items() if data.category == "Stage"}, + "Environments": {name for name, data in item_table.items() if data.category == "Environment"}, + "Upgrades": {name for name, data in item_table.items() if data.category == "Upgrade"}, + "Fillers": {name for name, data in item_table.items() if data.category == "Filler"}, + "Traps": {name for name, data in item_table.items() if data.category == "Trap"}, + } location_name_to_id = item_pickups - data_version = 7 - required_client_version = (0, 4, 2) + data_version = 8 + required_client_version = (0, 4, 4) web = RiskOfWeb() total_revivals: int - def __init__(self, multiworld: "MultiWorld", player: int): - super().__init__(multiworld, player) - self.junk_pool: Dict[str, int] = {} - def generate_early(self) -> None: # figure out how many revivals should exist in the pool if self.options.goal == "classic": total_locations = self.options.total_locations.value else: total_locations = len( - orderedstage_location.get_locations( + get_locations( chests=self.options.chests_per_stage.value, shrines=self.options.shrines_per_stage.value, scavengers=self.options.scavengers_per_stage.value, scanners=self.options.scanner_per_stage.value, altars=self.options.altars_per_stage.value, - dlc_sotv=self.options.dlc_sotv.value + dlc_sotv=bool(self.options.dlc_sotv.value) ) ) self.total_revivals = int(self.options.total_revivals.value / 100 * total_locations) if self.options.start_with_revive: self.total_revivals -= 1 + if self.options.victory == "voidling" and not self.options.dlc_sotv: + self.options.victory.value = self.options.victory.option_any + + def create_regions(self) -> None: + + if self.options.goal == "classic": + # classic mode + create_classic_regions(self) + else: + # explore mode + create_explore_regions(self) + + self.create_events() def create_items(self) -> None: # shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend @@ -77,25 +94,26 @@ def create_items(self) -> None: # figure out all available ordered stages for each tier environment_available_orderedstages_table = environment_vanilla_orderedstages_table if self.options.dlc_sotv: - environment_available_orderedstages_table = collapse_dict_list_vertical(environment_available_orderedstages_table, environment_sotv_orderedstages_table) + environment_available_orderedstages_table = \ + collapse_dict_list_vertical(environment_available_orderedstages_table, + environment_sotv_orderedstages_table) - environments_pool = shift_by_offset(environment_vanilla_table, environment_offest) + environments_pool = shift_by_offset(environment_vanilla_table, environment_offset) if self.options.dlc_sotv: - environment_offset_table = shift_by_offset(environment_sotv_table, environment_offest) + environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset) environments_pool = {**environments_pool, **environment_offset_table} environments_to_precollect = 5 if self.options.begin_with_loop else 1 # percollect environments for each stage (or just stage 1) for i in range(environments_to_precollect): - unlock = self.multiworld.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1) + unlock = self.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1) self.multiworld.push_precollected(self.create_item(unlock[0])) environments_pool.pop(unlock[0]) # Generate item pool - itempool: List = [] + itempool: List[str] = ["Beads of Fealty", "Radar Scanner"] # Add revive items for the player itempool += ["Dio's Best Friend"] * self.total_revivals - itempool += ["Beads of Fealty"] for env_name, _ in environments_pool.items(): itempool += [env_name] @@ -105,38 +123,28 @@ def create_items(self) -> None: total_locations = self.options.total_locations.value else: # explore mode + # Add Stage items for logic gates + itempool += ["Stage 1", "Stage 2", "Stage 3", "Stage 4"] total_locations = len( - orderedstage_location.get_locations( + get_locations( chests=self.options.chests_per_stage.value, shrines=self.options.shrines_per_stage.value, scavengers=self.options.scavengers_per_stage.value, scanners=self.options.scanner_per_stage.value, altars=self.options.altars_per_stage.value, - dlc_sotv=self.options.dlc_sotv.value + dlc_sotv=bool(self.options.dlc_sotv.value) ) ) # Create junk items - self.junk_pool = self.create_junk_pool() + junk_pool = self.create_junk_pool() # Fill remaining items with randomly generated junk - while len(itempool) < total_locations: - itempool.append(self.get_filler_item_name()) + filler = self.random.choices(*zip(*junk_pool.items()), k=total_locations - len(itempool)) + itempool.extend(filler) # Convert itempool into real items - itempool = list(map(lambda name: self.create_item(name), itempool)) - self.multiworld.itempool += itempool + self.multiworld.itempool += map(self.create_item, itempool) - def set_rules(self) -> None: - set_rules(self.multiworld, self.player) - - def get_filler_item_name(self) -> str: - if not self.junk_pool: - self.junk_pool = self.create_junk_pool() - weights = [data for data in self.junk_pool.values()] - filler = self.multiworld.random.choices([filler for filler in self.junk_pool.keys()], weights, - k=1)[0] - return filler - - def create_junk_pool(self) -> Dict: + def create_junk_pool(self) -> Dict[str, int]: # if presets are enabled generate junk_pool from the selected preset pool_option = self.options.item_weights.value junk_pool: Dict[str, int] = {} @@ -144,7 +152,7 @@ def create_junk_pool(self) -> Dict: # generate chaos weights if the preset is chosen if pool_option == ItemWeights.option_chaos: for name, max_value in item_pool_weights[pool_option].items(): - junk_pool[name] = self.multiworld.random.randint(0, max_value) + junk_pool[name] = self.random.randint(0, max_value) else: junk_pool = item_pool_weights[pool_option].copy() else: # generate junk pool from user created presets @@ -159,10 +167,22 @@ def create_junk_pool(self) -> Dict: "Boss Item": self.options.boss_item.value, "Lunar Item": self.options.lunar_item.value, "Void Item": self.options.void_item.value, - "Equipment": self.options.equipment.value + "Equipment": self.options.equipment.value, + "Money": self.options.money.value, + "Lunar Coin": self.options.lunar_coin.value, + "1000 Exp": self.options.experience.value, + "Mountain Trap": self.options.mountain_trap.value, + "Time Warp Trap": self.options.time_warp_trap.value, + "Combat Trap": self.options.combat_trap.value, + "Teleport Trap": self.options.teleport_trap.value, } - - # remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled + # remove trap items from the pool (excluding lunar items) + if not self.options.enable_trap: + junk_pool.pop("Mountain Trap") + junk_pool.pop("Time Warp Trap") + junk_pool.pop("Combat Trap") + junk_pool.pop("Teleport Trap") + # remove lunar items from the pool if not (self.options.enable_lunar or pool_option == ItemWeights.option_lunartic): junk_pool.pop("Lunar Item") # remove void items from the pool @@ -171,98 +191,58 @@ def create_junk_pool(self) -> Dict: return junk_pool - def create_regions(self) -> None: - - if self.options.goal == "classic": - # classic mode - menu = create_region(self.multiworld, self.player, "Menu") - self.multiworld.regions.append(menu) - # By using a victory region, we can define it as being connected to by several regions - # which can then determine the availability of the victory. - victory_region = create_region(self.multiworld, self.player, "Victory") - self.multiworld.regions.append(victory_region) - petrichor = create_region(self.multiworld, self.player, "Petrichor V", - get_classic_item_pickups(self.options.total_locations.value)) - self.multiworld.regions.append(petrichor) - - # classic mode can get to victory from the beginning of the game - to_victory = Entrance(self.player, "beating game", petrichor) - petrichor.exits.append(to_victory) - to_victory.connect(victory_region) + def create_item(self, name: str) -> Item: + data = item_table[name] + return RiskOfRainItem(name, data.item_type, data.code, self.player) - connection = Entrance(self.player, "Lobby", menu) - menu.exits.append(connection) - connection.connect(petrichor) - else: - # explore mode - create_regions(self.multiworld, self.player) + def set_rules(self) -> None: + set_rules(self) - create_events(self.multiworld, self.player) + def get_filler_item_name(self) -> str: + weights = [data.weight for data in filler_table.values()] + filler = self.multiworld.random.choices([filler for filler in filler_table.keys()], weights, + k=1)[0] + return filler - def fill_slot_data(self): - options_dict = self.options.as_dict("item_pickup_step", "shrine_use_step", "goal", "total_locations", - "chests_per_stage", "shrines_per_stage", "scavengers_per_stage", - "scanner_per_stage", "altars_per_stage", "total_revivals", "start_with_revive", - "final_stage_death", "death_link", casing="camel") + def fill_slot_data(self) -> Dict[str, Any]: + options_dict = self.options.as_dict("item_pickup_step", "shrine_use_step", "goal", "victory", "total_locations", + "chests_per_stage", "shrines_per_stage", "scavengers_per_stage", + "scanner_per_stage", "altars_per_stage", "total_revivals", + "start_with_revive", "final_stage_death", "death_link", + casing="camel") return { **options_dict, - "seed": "".join(self.multiworld.per_slot_randoms[self.player].choice(string.digits) for _ in range(16)), + "seed": "".join(self.random.choice(string.digits) for _ in range(16)), + "offset": offset } - def create_item(self, name: str) -> Item: - item_id = item_table[name] - classification = ItemClassification.filler - if name in {"Dio's Best Friend", "Beads of Fealty"}: - classification = ItemClassification.progression - elif name in {"Legendary Item", "Boss Item"}: - classification = ItemClassification.useful - elif name == "Lunar Item": - classification = ItemClassification.trap - - # Only check for an item to be a environment unlock if those are known to be in the pool. - # This should shave down comparisons. - - elif name in environment_ALL_table.keys(): - if name in {"Hidden Realm: Bulwark's Ambry", "Hidden Realm: Gilded Coast,"}: - classification = ItemClassification.useful - else: - classification = ItemClassification.progression - - item = RiskOfRainItem(name, classification, item_id, self.player) - return item - - -def create_events(world: MultiWorld, player: int) -> None: - total_locations = world.worlds[player].options.total_locations.value - num_of_events = total_locations // 25 - if total_locations / 25 == num_of_events: - num_of_events -= 1 - world_region = world.get_region("Petrichor V", player) - if world.worlds[player].options.goal == "classic": - # only setup Pickups when using classic_mode - for i in range(num_of_events): - event_loc = RiskOfRainLocation(player, f"Pickup{(i + 1) * 25}", None, world_region) - event_loc.place_locked_item(RiskOfRainItem(f"Pickup{(i + 1) * 25}", ItemClassification.progression, None, player)) - event_loc.access_rule = \ - lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", player) - world_region.locations.append(event_loc) - elif world.worlds[player].options.goal == "explore": - for n in range(1, 6): - - event_region = world.get_region(f"OrderedStage_{n}", player) - event_loc = RiskOfRainLocation(player, f"Stage_{n}", None, event_region) - event_loc.place_locked_item(RiskOfRainItem(f"Stage_{n}", ItemClassification.progression, None, player)) + def create_events(self) -> None: + total_locations = self.options.total_locations.value + num_of_events = total_locations // 25 + if total_locations / 25 == num_of_events: + num_of_events -= 1 + world_region = self.multiworld.get_region("Petrichor V", self.player) + if self.options.goal == "classic": + # classic mode + # only setup Pickups when using classic_mode + for i in range(num_of_events): + event_loc = RiskOfRainLocation(self.player, f"Pickup{(i + 1) * 25}", None, world_region) + event_loc.place_locked_item( + RiskOfRainItem(f"Pickup{(i + 1) * 25}", ItemClassification.progression, None, + self.player)) + event_loc.access_rule = \ + lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", self.player) + world_region.locations.append(event_loc) + else: + # explore mode + event_region = self.multiworld.get_region("OrderedStage_5", self.player) + event_loc = RiskOfRainLocation(self.player, "Stage 5", None, event_region) + event_loc.place_locked_item(RiskOfRainItem("Stage 5", ItemClassification.progression, None, self.player)) event_loc.show_in_spoiler = False event_region.locations.append(event_loc) + event_loc.access_rule = lambda state: state.has("Sky Meadow", self.player) - victory_region = world.get_region("Victory", player) - victory_event = RiskOfRainLocation(player, "Victory", None, victory_region) - victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, player)) - world_region.locations.append(victory_event) - - -def create_region(world: MultiWorld, player: int, name: str, locations: Dict[str, int] = {}) -> Region: - ret = Region(name, player, world) - for location_name, location_id in locations.items(): - ret.locations.append(RiskOfRainLocation(player, location_name, location_id, ret)) - return ret + victory_region = self.multiworld.get_region("Victory", self.player) + victory_event = RiskOfRainLocation(self.player, "Victory", None, victory_region) + victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, self.player)) + victory_region.locations.append(victory_event) diff --git a/worlds/ror2/docs/setup_en.md b/worlds/ror2/docs/setup_en.md index 4e59d2bf4157..0fa99c071b9c 100644 --- a/worlds/ror2/docs/setup_en.md +++ b/worlds/ror2/docs/setup_en.md @@ -55,4 +55,15 @@ the player's YAML. You can talk to other in the multiworld chat using the RoR2 chat. All other multiworld remote commands list in the [commands guide](/tutorial/Archipelago/commands/en) work as well in the RoR2 chat. You can also optionally connect to the multiworld using the text client, which can be found in the -[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases). \ No newline at end of file +[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases). + +### In-Game Commands +These commands are to be used in-game by using ``Ctrl + Alt + ` `` and then typing the following: + - `archipelago_connect [password]` example: "archipelago_connect archipelago.gg 38281 SlotName". + - `archipelago_deathlink true/false` Toggle deathlink. + - `archipelago_disconnect` Disconnect from AP. + - `archipelago_final_stage_death true/false` Toggle final stage death. + +Explore Mode only + - `archipelago_show_unlocked_stages` Show which stages have been received. + - `archipelago_highlight_satellite true/false` This will highlight the satellite to make it easier to see (Default false). \ No newline at end of file diff --git a/worlds/ror2/items.py b/worlds/ror2/items.py new file mode 100644 index 000000000000..449686d04bf0 --- /dev/null +++ b/worlds/ror2/items.py @@ -0,0 +1,309 @@ +from BaseClasses import Item, ItemClassification +from .options import ItemWeights +from .ror2environments import environment_all_table +from typing import NamedTuple, Optional, Dict + + +class RiskOfRainItem(Item): + game: str = "Risk of Rain 2" + + +class RiskOfRainItemData(NamedTuple): + category: str + code: int + item_type: ItemClassification = ItemClassification.filler + weight: Optional[int] = None + + +offset: int = 37000 +filler_offset: int = offset + 300 +trap_offset: int = offset + 400 +stage_offset: int = offset + 500 +environment_offset: int = offset + 700 +# Upgrade item ids 37002 - 37012 +upgrade_table: Dict[str, RiskOfRainItemData] = { + "Common Item": RiskOfRainItemData("Upgrade", 2 + offset, ItemClassification.filler, 64), + "Uncommon Item": RiskOfRainItemData("Upgrade", 3 + offset, ItemClassification.filler, 32), + "Legendary Item": RiskOfRainItemData("Upgrade", 4 + offset, ItemClassification.useful, 8), + "Boss Item": RiskOfRainItemData("Upgrade", 5 + offset, ItemClassification.useful, 4), + "Equipment": RiskOfRainItemData("Upgrade", 7 + offset, ItemClassification.filler, 32), + "Item Scrap, White": RiskOfRainItemData("Upgrade", 8 + offset, ItemClassification.filler, 32), + "Item Scrap, Green": RiskOfRainItemData("Upgrade", 9 + offset, ItemClassification.filler, 16), + "Item Scrap, Red": RiskOfRainItemData("Upgrade", 10 + offset, ItemClassification.filler, 4), + "Item Scrap, Yellow": RiskOfRainItemData("Upgrade", 11 + offset, ItemClassification.filler, 1), + "Void Item": RiskOfRainItemData("Upgrade", 12 + offset, ItemClassification.filler, 16), +} +# Other item ids 37001, 37013-37014 +other_table: Dict[str, RiskOfRainItemData] = { + "Dio's Best Friend": RiskOfRainItemData("ExtraLife", 1 + offset, ItemClassification.progression_skip_balancing), + "Beads of Fealty": RiskOfRainItemData("Beads", 13 + offset, ItemClassification.progression), + "Radar Scanner": RiskOfRainItemData("Radar", 14 + offset, ItemClassification.useful), +} +# Filler item ids 37301 - 37303 +filler_table: Dict[str, RiskOfRainItemData] = { + "Money": RiskOfRainItemData("Filler", 1 + filler_offset, ItemClassification.filler, 64), + "Lunar Coin": RiskOfRainItemData("Filler", 2 + filler_offset, ItemClassification.filler, 20), + "1000 Exp": RiskOfRainItemData("Filler", 3 + filler_offset, ItemClassification.filler, 40), +} +# Trap item ids 37401 - 37404 (Lunar items used to be part of the upgrade item list, so keeping the id the same) +trap_table: Dict[str, RiskOfRainItemData] = { + "Lunar Item": RiskOfRainItemData("Trap", 6 + offset, ItemClassification.trap, 16), + "Mountain Trap": RiskOfRainItemData("Trap", 1 + trap_offset, ItemClassification.trap, 5), + "Time Warp Trap": RiskOfRainItemData("Trap", 2 + trap_offset, ItemClassification.trap, 20), + "Combat Trap": RiskOfRainItemData("Trap", 3 + trap_offset, ItemClassification.trap, 20), + "Teleport Trap": RiskOfRainItemData("Trap", 4 + trap_offset, ItemClassification.trap, 10), +} +# Stage item ids 37501 - 37504 +stage_table: Dict[str, RiskOfRainItemData] = { + "Stage 1": RiskOfRainItemData("Stage", 1 + stage_offset, ItemClassification.progression), + "Stage 2": RiskOfRainItemData("Stage", 2 + stage_offset, ItemClassification.progression), + "Stage 3": RiskOfRainItemData("Stage", 3 + stage_offset, ItemClassification.progression), + "Stage 4": RiskOfRainItemData("Stage", 4 + stage_offset, ItemClassification.progression), + +} + +item_table = {**upgrade_table, **other_table, **filler_table, **trap_table, **stage_table} +# Environment item ids 37700 - 37746 +################################################## +# environments + + +# add ALL environments into the item table +def create_environment_table(name: str, environment_id: int, environment_classification: ItemClassification) \ + -> Dict[str, RiskOfRainItemData]: + return {name: RiskOfRainItemData("Environment", environment_offset + environment_id, environment_classification)} + + +environment_table: Dict[str, RiskOfRainItemData] = {} +# use the sotv dlc in the item table so that all names can be looked up regardless of use +for data, key in environment_all_table.items(): + classification = ItemClassification.progression + if data in {"Hidden Realm: Bulwark's Ambry", "Hidden Realm: Gilded Coast"}: + classification = ItemClassification.useful + environment_table.update(create_environment_table(data, key, classification)) + +item_table.update(environment_table) + +# end of environments +################################################## + +default_weights: Dict[str, int] = { + "Item Scrap, Green": 16, + "Item Scrap, Red": 4, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 32, + "Common Item": 64, + "Uncommon Item": 32, + "Legendary Item": 8, + "Boss Item": 4, + "Void Item": 16, + "Equipment": 32, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +new_weights: Dict[str, int] = { + "Item Scrap, Green": 15, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 75, + "Uncommon Item": 40, + "Legendary Item": 10, + "Boss Item": 5, + "Void Item": 16, + "Equipment": 20, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +uncommon_weights: Dict[str, int] = { + "Item Scrap, Green": 45, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 45, + "Uncommon Item": 100, + "Legendary Item": 10, + "Boss Item": 5, + "Void Item": 16, + "Equipment": 20, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +legendary_weights: Dict[str, int] = { + "Item Scrap, Green": 15, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 50, + "Uncommon Item": 25, + "Legendary Item": 100, + "Boss Item": 5, + "Void Item": 16, + "Equipment": 20, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +chaos_weights: Dict[str, int] = { + "Item Scrap, Green": 80, + "Item Scrap, Red": 45, + "Item Scrap, Yellow": 30, + "Item Scrap, White": 100, + "Common Item": 100, + "Uncommon Item": 70, + "Legendary Item": 30, + "Boss Item": 20, + "Void Item": 60, + "Equipment": 40, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +no_scraps_weights: Dict[str, int] = { + "Item Scrap, Green": 0, + "Item Scrap, Red": 0, + "Item Scrap, Yellow": 0, + "Item Scrap, White": 0, + "Common Item": 100, + "Uncommon Item": 40, + "Legendary Item": 15, + "Boss Item": 5, + "Void Item": 16, + "Equipment": 25, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +even_weights: Dict[str, int] = { + "Item Scrap, Green": 1, + "Item Scrap, Red": 1, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 1, + "Common Item": 1, + "Uncommon Item": 1, + "Legendary Item": 1, + "Boss Item": 1, + "Void Item": 1, + "Equipment": 1, + "Money": 1, + "Lunar Coin": 1, + "1000 Exp": 1, + "Lunar Item": 1, + "Mountain Trap": 1, + "Time Warp Trap": 1, + "Combat Trap": 1, + "Teleport Trap": 1 +} + +scraps_only: Dict[str, int] = { + "Item Scrap, Green": 70, + "Item Scrap, White": 100, + "Item Scrap, Red": 30, + "Item Scrap, Yellow": 5, + "Common Item": 0, + "Uncommon Item": 0, + "Legendary Item": 0, + "Boss Item": 0, + "Void Item": 0, + "Equipment": 0, + "Money": 20, + "Lunar Coin": 10, + "1000 Exp": 10, + "Lunar Item": 0, + "Mountain Trap": 5, + "Time Warp Trap": 10, + "Combat Trap": 10, + "Teleport Trap": 10 +} +lunartic_weights: Dict[str, int] = { + "Item Scrap, Green": 0, + "Item Scrap, Red": 0, + "Item Scrap, Yellow": 0, + "Item Scrap, White": 0, + "Common Item": 0, + "Uncommon Item": 0, + "Legendary Item": 0, + "Boss Item": 0, + "Void Item": 0, + "Equipment": 0, + "Money": 20, + "Lunar Coin": 10, + "1000 Exp": 10, + "Lunar Item": 100, + "Mountain Trap": 5, + "Time Warp Trap": 10, + "Combat Trap": 10, + "Teleport Trap": 10 +} +void_weights: Dict[str, int] = { + "Item Scrap, Green": 0, + "Item Scrap, Red": 0, + "Item Scrap, Yellow": 0, + "Item Scrap, White": 0, + "Common Item": 0, + "Uncommon Item": 0, + "Legendary Item": 0, + "Boss Item": 0, + "Void Item": 100, + "Equipment": 0, + "Money": 20, + "Lunar Coin": 10, + "1000 Exp": 10, + "Lunar Item": 0, + "Mountain Trap": 5, + "Time Warp Trap": 10, + "Combat Trap": 10, + "Teleport Trap": 10 +} + +item_pool_weights: Dict[int, Dict[str, int]] = { + ItemWeights.option_default: default_weights, + ItemWeights.option_new: new_weights, + ItemWeights.option_uncommon: uncommon_weights, + ItemWeights.option_legendary: legendary_weights, + ItemWeights.option_chaos: chaos_weights, + ItemWeights.option_no_scraps: no_scraps_weights, + ItemWeights.option_even: even_weights, + ItemWeights.option_scraps_only: scraps_only, + ItemWeights.option_lunartic: lunartic_weights, + ItemWeights.option_void: void_weights, +} diff --git a/worlds/ror2/locations.py b/worlds/ror2/locations.py new file mode 100644 index 000000000000..13077b3e149c --- /dev/null +++ b/worlds/ror2/locations.py @@ -0,0 +1,89 @@ +from typing import Dict +from BaseClasses import Location +from .options import TotalLocations, ChestsPerEnvironment, ShrinesPerEnvironment, ScavengersPerEnvironment, \ + ScannersPerEnvironment, AltarsPerEnvironment +from .ror2environments import compress_dict_list_horizontal, environment_vanilla_orderedstages_table, \ + environment_sotv_orderedstages_table + + +class RiskOfRainLocation(Location): + game: str = "Risk of Rain 2" + + +ror2_locations_start_id = 38000 + + +def get_classic_item_pickups(n: int) -> Dict[str, int]: + """Get n ItemPickups, capped at the max value for TotalLocations""" + n = max(n, 0) + n = min(n, TotalLocations.range_end) + return {f"ItemPickup{i + 1}": ror2_locations_start_id + i for i in range(n)} + + +item_pickups = get_classic_item_pickups(TotalLocations.range_end) +location_table = item_pickups + +# this is so we can easily calculate the environment and location "offset" ids +ror2_locations_start_ordered_stage = ror2_locations_start_id + TotalLocations.range_end + +# TODO is there a better, more generic way to do this? +offset_chests = 0 +offset_shrines = offset_chests + ChestsPerEnvironment.range_end +offset_scavengers = offset_shrines + ShrinesPerEnvironment.range_end +offset_scanners = offset_scavengers + ScavengersPerEnvironment.range_end +offset_altars = offset_scanners + ScannersPerEnvironment.range_end + +# total space allocated to the locations in a single orderedstage environment +allocation = offset_altars + AltarsPerEnvironment.range_end + + +def get_environment_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, + environment_name: str, environment_index: int) -> Dict[str, int]: + """Get the locations within a specific environment""" + locations = {} + + # due to this mapping, since environment ids are not consecutive, there are lots of "wasted" id numbers + environment_start_id = environment_index * allocation + ror2_locations_start_ordered_stage + for n in range(chests): + locations.update({f"{environment_name}: Chest {n + 1}": n + offset_chests + environment_start_id}) + for n in range(shrines): + locations.update({f"{environment_name}: Shrine {n + 1}": n + offset_shrines + environment_start_id}) + for n in range(scavengers): + locations.update({f"{environment_name}: Scavenger {n + 1}": n + offset_scavengers + environment_start_id}) + for n in range(scanners): + locations.update({f"{environment_name}: Radio Scanner {n + 1}": n + offset_scanners + environment_start_id}) + for n in range(altars): + locations.update({f"{environment_name}: Newt Altar {n + 1}": n + offset_altars + environment_start_id}) + return locations + + +def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, dlc_sotv: bool) \ + -> Dict[str, int]: + """Get a dictionary of locations for the orderedstage environments with the locations from the parameters.""" + locations = {} + orderedstages = compress_dict_list_horizontal(environment_vanilla_orderedstages_table) + if dlc_sotv: + orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table)) + # for every environment, generate the respective locations + for environment_name, environment_index in orderedstages.items(): + locations.update(get_environment_locations( + chests=chests, + shrines=shrines, + scavengers=scavengers, + scanners=scanners, + altars=altars, + environment_name=environment_name, + environment_index=environment_index), + ) + return locations + + +# Get all locations in ordered stages. +location_table.update(get_locations( + chests=ChestsPerEnvironment.range_end, + shrines=ShrinesPerEnvironment.range_end, + scavengers=ScavengersPerEnvironment.range_end, + scanners=ScannersPerEnvironment.range_end, + altars=AltarsPerEnvironment.range_end, + dlc_sotv=True, +)) diff --git a/worlds/ror2/Options.py b/worlds/ror2/options.py similarity index 73% rename from worlds/ror2/Options.py rename to worlds/ror2/options.py index 0ed0a87b17d6..7daf8a844666 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/options.py @@ -4,7 +4,7 @@ # NOTE be aware that since the range of item ids that RoR2 uses is based off of the maximums of checks # Be careful when changing the range_end values not to go into another game's IDs -# NOTE that these changes to range_end must also be reflected in the RoR2 client so it understands the same ids. +# NOTE that these changes to range_end must also be reflected in the RoR2 client, so it understands the same ids. class Goal(Choice): """ @@ -19,6 +19,21 @@ class Goal(Choice): default = 1 +class Victory(Choice): + """ + Mithrix: Defeat Mithrix in Commencement + Voidling: Defeat the Voidling in The Planetarium (DLC required! Will select any if not enabled.) + Limbo: Defeat the Scavenger in Hidden Realm: A Moment, Whole + Any: Any victory in the game will count. See Final Stage Death for additional ways. + """ + display_name = "Victory Condition" + option_any = 0 + option_mithrix = 1 + option_voidling = 2 + option_limbo = 3 + default = 0 + + class TotalLocations(Range): """Classic Mode: Number of location checks which are added to the Risk of Rain playthrough.""" display_name = "Total Locations" @@ -100,6 +115,11 @@ class ShrineUseStep(Range): default = 0 +class AllowTrapItems(Toggle): + """Allows Trap items in the item pool.""" + display_name = "Enable Trap Items" + + class AllowLunarItems(DefaultOnToggle): """Allows Lunar items in the item pool.""" display_name = "Enable Lunar Item Shuffling" @@ -111,10 +131,14 @@ class StartWithRevive(DefaultOnToggle): class FinalStageDeath(Toggle): - """The following will count as a win if set to true: + """The following will count as a win if set to "true", and victory is set to "any": Dying in Commencement. Dying in The Planetarium. - Obliterating yourself""" + Obliterating yourself + If not use the following to tell if final stage death will count: + Victory: mithrix - only dying in Commencement will count. + Victory: voidling - only dying in The Planetarium will count. + Victory: limbo - Obliterating yourself will count.""" display_name = "Final Stage Death is Win" @@ -247,6 +271,76 @@ class Equipment(Range): default = 32 +class Money(Range): + """Weight of money items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Money" + range_start = 0 + range_end = 100 + default = 64 + + +class LunarCoin(Range): + """Weight of lunar coin items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Lunar Coins" + range_start = 0 + range_end = 100 + default = 20 + + +class Experience(Range): + """Weight of 1000 exp items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "1000 Exp" + range_start = 0 + range_end = 100 + default = 40 + + +class MountainTrap(Range): + """Weight of mountain trap items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Mountain Trap" + range_start = 0 + range_end = 100 + default = 5 + + +class TimeWarpTrap(Range): + """Weight of time warp trap items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Time Warp Trap" + range_start = 0 + range_end = 100 + default = 20 + + +class CombatTrap(Range): + """Weight of combat trap items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Combat Trap" + range_start = 0 + range_end = 100 + default = 20 + + +class TeleportTrap(Range): + """Weight of teleport trap items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Teleport Trap" + range_start = 0 + range_end = 100 + default = 20 + + class ItemPoolPresetToggle(Toggle): """Will use the item weight presets when set to true, otherwise will use the custom set item pool weights.""" display_name = "Use Item Weight Presets" @@ -258,28 +352,30 @@ class ItemWeights(Choice): - New is a test for a potential adjustment to the default weights. - Uncommon puts a large number of uncommon items in the pool. - Legendary puts a large number of legendary items in the pool. - - Lunartic makes everything a lunar item. - - Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy. + - Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being + too easy. - No Scraps removes all scrap items from the item pool. - Even generates the item pool with every item having an even weight. - Scraps Only will be only scrap items in the item pool. + - Lunartic makes everything a lunar item. - Void makes everything a void item.""" display_name = "Item Weights" option_default = 0 option_new = 1 option_uncommon = 2 option_legendary = 3 - option_lunartic = 4 - option_chaos = 5 - option_no_scraps = 6 - option_even = 7 - option_scraps_only = 8 + option_chaos = 4 + option_no_scraps = 5 + option_even = 6 + option_scraps_only = 7 + option_lunartic = 8 option_void = 9 @dataclass class ROR2Options(PerGameCommonOptions): goal: Goal + victory: Victory total_locations: TotalLocations chests_per_stage: ChestsPerEnvironment shrines_per_stage: ShrinesPerEnvironment @@ -294,6 +390,7 @@ class ROR2Options(PerGameCommonOptions): death_link: DeathLink item_pickup_step: ItemPickupStep shrine_use_step: ShrineUseStep + enable_trap: AllowTrapItems enable_lunar: AllowLunarItems item_weights: ItemWeights item_pool_presets: ItemPoolPresetToggle @@ -309,3 +406,10 @@ class ROR2Options(PerGameCommonOptions): lunar_item: LunarItem void_item: VoidItem equipment: Equipment + money: Money + lunar_coin: LunarCoin + experience: Experience + mountain_trap: MountainTrap + time_warp_trap: TimeWarpTrap + combat_trap: CombatTrap + teleport_trap: TeleportTrap diff --git a/worlds/ror2/Regions.py b/worlds/ror2/regions.py similarity index 59% rename from worlds/ror2/Regions.py rename to worlds/ror2/regions.py index 94f5aaf71ee8..13b229da9249 100644 --- a/worlds/ror2/Regions.py +++ b/worlds/ror2/regions.py @@ -1,7 +1,10 @@ -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 location_table, RiskOfRainLocation +from BaseClasses import Region, Entrance, MultiWorld +from .locations import location_table, RiskOfRainLocation, get_classic_item_pickups + +if TYPE_CHECKING: + from . import RiskOfRainWorld class RoRRegionData(NamedTuple): @@ -9,10 +12,14 @@ class RoRRegionData(NamedTuple): region_exits: Optional[List[str]] -def create_regions(multiworld: MultiWorld, player: int): +def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: + player = ror2_world.player + ror2_options = ror2_world.options + multiworld = ror2_world.multiworld # Default Locations non_dlc_regions: Dict[str, RoRRegionData] = { - "Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)", "Titanic Plains", "Titanic Plains (2)"]), + "Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)", + "Titanic Plains", "Titanic Plains (2)"]), "Distant Roost": RoRRegionData([], ["OrderedStage_1"]), "Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]), "Titanic Plains": RoRRegionData([], ["OrderedStage_1"]), @@ -34,33 +41,36 @@ def create_regions(multiworld: MultiWorld, player: int): } other_regions: Dict[str, RoRRegionData] = { "Commencement": RoRRegionData(None, ["Victory", "Petrichor V"]), - "OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured", "Commencement"]), + "OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured", + "Commencement"]), "OrderedStage_1": RoRRegionData(None, ["Hidden Realm: Bazaar Between Time", - "Hidden Realm: Gilded Coast", "Abandoned Aqueduct", "Wetland Aspect"]), + "Hidden Realm: Gilded Coast", "Abandoned Aqueduct", + "Wetland Aspect"]), "OrderedStage_2": RoRRegionData(None, ["Rallypoint Delta", "Scorched Acres"]), - "OrderedStage_3": RoRRegionData(None, ["Abyssal Depths", "Siren's Call", "Sundered Grove"]), + "OrderedStage_3": RoRRegionData(None, ["Abyssal Depths", "Siren's Call", + "Sundered Grove"]), "OrderedStage_4": RoRRegionData(None, ["Sky Meadow"]), "Hidden Realm: A Moment, Fractured": RoRRegionData(None, ["Hidden Realm: A Moment, Whole"]), - "Hidden Realm: A Moment, Whole": RoRRegionData(None, ["Victory"]), + "Hidden Realm: A Moment, Whole": RoRRegionData(None, ["Victory", "Petrichor V"]), "Void Fields": RoRRegionData(None, []), "Victory": RoRRegionData(None, None), - "Petrichor V": RoRRegionData(None, ["Victory"]), + "Petrichor V": RoRRegionData(None, []), "Hidden Realm: Bulwark's Ambry": RoRRegionData(None, None), "Hidden Realm: Bazaar Between Time": RoRRegionData(None, ["Void Fields"]), "Hidden Realm: Gilded Coast": RoRRegionData(None, None) } dlc_other_regions: Dict[str, RoRRegionData] = { - "The Planetarium": RoRRegionData(None, ["Victory"]), + "The Planetarium": RoRRegionData(None, ["Victory", "Petrichor V"]), "Void Locus": RoRRegionData(None, ["The Planetarium"]) } # Totals of each item - chests = int(multiworld.chests_per_stage[player]) - shrines = int(multiworld.shrines_per_stage[player]) - scavengers = int(multiworld.scavengers_per_stage[player]) - scanners = int(multiworld.scanner_per_stage[player]) - newt = int(multiworld.altars_per_stage[player]) + chests = int(ror2_options.chests_per_stage) + shrines = int(ror2_options.shrines_per_stage) + scavengers = int(ror2_options.scavengers_per_stage) + scanners = int(ror2_options.scanner_per_stage) + newt = int(ror2_options.altars_per_stage) all_location_regions = {**non_dlc_regions} - if multiworld.dlc_sotv[player]: + if ror2_options.dlc_sotv: all_location_regions = {**non_dlc_regions, **dlc_regions} # Locations @@ -88,23 +98,35 @@ def create_regions(multiworld: MultiWorld, player: int): regions_pool: Dict = {**all_location_regions, **other_regions} # DLC Locations - if multiworld.dlc_sotv[player]: + if ror2_options.dlc_sotv: non_dlc_regions["Menu"].region_exits.append("Siphoned Forest") other_regions["OrderedStage_1"].region_exits.append("Aphelian Sanctuary") other_regions["OrderedStage_2"].region_exits.append("Sulfur Pools") other_regions["Void Fields"].region_exits.append("Void Locus") + other_regions["Commencement"].region_exits.append("The Planetarium") regions_pool: Dict = {**all_location_regions, **other_regions, **dlc_other_regions} + # Check to see if Victory needs to be removed from regions + if ror2_options.victory == "mithrix": + other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0) + dlc_other_regions["The Planetarium"].region_exits.pop(0) + elif ror2_options.victory == "voidling": + other_regions["Commencement"].region_exits.pop(0) + other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0) + elif ror2_options.victory == "limbo": + other_regions["Commencement"].region_exits.pop(0) + dlc_other_regions["The Planetarium"].region_exits.pop(0) + # Create all the regions for name, data in regions_pool.items(): - multiworld.regions.append(create_region(multiworld, player, name, data)) + multiworld.regions.append(create_explore_region(multiworld, player, name, data)) # Connect all the regions to their exits for name, data in regions_pool.items(): create_connections_in_regions(multiworld, player, name, data) -def create_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData): +def create_explore_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData) -> Region: region = Region(name, player, multiworld) if data.locations: for location_name in data.locations: @@ -115,7 +137,7 @@ def create_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegio return region -def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData): +def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData) -> None: region = multiworld.get_region(name, player) if data.region_exits: for region_exit in data.region_exits: @@ -123,3 +145,34 @@ def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str exit_region = multiworld.get_region(region_exit, player) r_exit_stage.connect(exit_region) region.exits.append(r_exit_stage) + + +def create_classic_regions(ror2_world: "RiskOfRainWorld") -> None: + player = ror2_world.player + ror2_options = ror2_world.options + multiworld = ror2_world.multiworld + menu = create_classic_region(multiworld, player, "Menu") + multiworld.regions.append(menu) + # By using a victory region, we can define it as being connected to by several regions + # which can then determine the availability of the victory. + victory_region = create_classic_region(multiworld, player, "Victory") + multiworld.regions.append(victory_region) + petrichor = create_classic_region(multiworld, player, "Petrichor V", + get_classic_item_pickups(ror2_options.total_locations.value)) + multiworld.regions.append(petrichor) + + # classic mode can get to victory from the beginning of the game + to_victory = Entrance(player, "beating game", petrichor) + petrichor.exits.append(to_victory) + to_victory.connect(victory_region) + + connection = Entrance(player, "Lobby", menu) + menu.exits.append(connection) + connection.connect(petrichor) + + +def create_classic_region(multiworld: MultiWorld, player: int, name: str, locations: Dict[str, int] = {}) -> Region: + ret = Region(name, player, multiworld) + for location_name, location_id in locations.items(): + ret.locations.append(RiskOfRainLocation(player, location_name, location_id, ret)) + return ret diff --git a/worlds/ror2/ror2environments.py b/worlds/ror2/ror2environments.py new file mode 100644 index 000000000000..d821763ef40c --- /dev/null +++ b/worlds/ror2/ror2environments.py @@ -0,0 +1,118 @@ +from typing import Dict, List, TypeVar + +# TODO probably move to Locations + +environment_vanilla_orderedstage_1_table: Dict[str, int] = { + "Distant Roost": 7, # blackbeach + "Distant Roost (2)": 8, # blackbeach2 + "Titanic Plains": 15, # golemplains + "Titanic Plains (2)": 16, # golemplains2 +} +environment_vanilla_orderedstage_2_table: Dict[str, int] = { + "Abandoned Aqueduct": 17, # goolake + "Wetland Aspect": 12, # foggyswamp +} +environment_vanilla_orderedstage_3_table: Dict[str, int] = { + "Rallypoint Delta": 13, # frozenwall + "Scorched Acres": 47, # wispgraveyard +} +environment_vanilla_orderedstage_4_table: Dict[str, int] = { + "Abyssal Depths": 10, # dampcavesimple + "Siren's Call": 37, # shipgraveyard + "Sundered Grove": 35, # rootjungle +} +environment_vanilla_orderedstage_5_table: Dict[str, int] = { + "Sky Meadow": 38, # skymeadow +} + +environment_vanilla_hidden_realm_table: Dict[str, int] = { + "Hidden Realm: Bulwark's Ambry": 5, # artifactworld + "Hidden Realm: Bazaar Between Time": 6, # bazaar + "Hidden Realm: Gilded Coast": 14, # goldshores + "Hidden Realm: A Moment, Whole": 27, # limbo + "Hidden Realm: A Moment, Fractured": 33, # mysteryspace +} + +environment_vanilla_special_table: Dict[str, int] = { + "Void Fields": 4, # arena + "Commencement": 32, # moon2 +} + +environment_sotv_orderedstage_1_table: Dict[str, int] = { + "Siphoned Forest": 39, # snowyforest +} +environment_sotv_orderedstage_2_table: Dict[str, int] = { + "Aphelian Sanctuary": 3, # ancientloft +} +environment_sotv_orderedstage_3_table: Dict[str, int] = { + "Sulfur Pools": 41, # sulfurpools +} + +environment_sotv_special_table: Dict[str, int] = { + "Void Locus": 46, # voidstage + "The Planetarium": 45, # voidraid +} + +X = TypeVar("X") +Y = TypeVar("Y") + + +def compress_dict_list_horizontal(list_of_dict: List[Dict[X, Y]]) -> Dict[X, Y]: + """Combine all dictionaries in a list together into one dictionary.""" + compressed: Dict[X, Y] = {} + for individual in list_of_dict: + compressed.update(individual) + return compressed + + +def collapse_dict_list_vertical(list_of_dict_1: List[Dict[X, Y]], *args: List[Dict[X, Y]]) -> List[Dict[X, Y]]: + """Combine all parallel dictionaries in lists together to make a new list of dictionaries of the same length.""" + # find the length of the longest list + length = len(list_of_dict_1) + for list_of_dict_n in args: + length = max(length, len(list_of_dict_n)) + + # create a combined list with a length the same as the longest list + collapsed: List[Dict[X, Y]] = [{}] * length + # The reason the list_of_dict_1 is not directly used to make collapsed is + # side effects can occur if all the dictionaries are not manually unioned. + + # merge contents from list_of_dict_1 + for i in range(len(list_of_dict_1)): + collapsed[i] = {**collapsed[i], **list_of_dict_1[i]} + + # merge contents of remaining lists_of_dicts + for list_of_dict_n in args: + for i in range(len(list_of_dict_n)): + collapsed[i] = {**collapsed[i], **list_of_dict_n[i]} + + return collapsed + + +# TODO potentially these should only be created when they are directly referenced +# (unsure of the space/time cost of creating these initially) + +environment_vanilla_orderedstages_table = \ + [environment_vanilla_orderedstage_1_table, environment_vanilla_orderedstage_2_table, + environment_vanilla_orderedstage_3_table, environment_vanilla_orderedstage_4_table, + environment_vanilla_orderedstage_5_table] +environment_vanilla_table = \ + {**compress_dict_list_horizontal(environment_vanilla_orderedstages_table), + **environment_vanilla_hidden_realm_table, **environment_vanilla_special_table} + +environment_sotv_orderedstages_table = \ + [environment_sotv_orderedstage_1_table, environment_sotv_orderedstage_2_table, + environment_sotv_orderedstage_3_table] +environment_sotv_table = \ + {**compress_dict_list_horizontal(environment_sotv_orderedstages_table), **environment_sotv_special_table} + +environment_non_orderedstages_table = \ + {**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table, **environment_sotv_special_table} +environment_orderedstages_table = \ + collapse_dict_list_vertical(environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table) +environment_all_table = {**environment_vanilla_table, **environment_sotv_table} + + +def shift_by_offset(dictionary: Dict[str, int], offset: int) -> Dict[str, int]: + """Shift all indexes in a dictionary by an offset""" + return {name: index+offset for name, index in dictionary.items()} diff --git a/worlds/ror2/Rules.py b/worlds/ror2/rules.py similarity index 60% rename from worlds/ror2/Rules.py rename to worlds/ror2/rules.py index 65c04d06cba6..442e6c0002aa 100644 --- a/worlds/ror2/Rules.py +++ b/worlds/ror2/rules.py @@ -1,62 +1,71 @@ -from BaseClasses import MultiWorld, CollectionState from worlds.generic.Rules import set_rule, add_rule -from .Locations import orderedstage_location -from .RoR2Environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table, \ - environment_orderedstages_table +from BaseClasses import MultiWorld +from .locations import get_locations +from .ror2environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table +from typing import Set, TYPE_CHECKING + +if TYPE_CHECKING: + from . import RiskOfRainWorld # Rule to see if it has access to the previous stage -def has_entrance_access_rule(multiworld: MultiWorld, stage: str, entrance: str, player: int): +def has_entrance_access_rule(multiworld: MultiWorld, stage: str, entrance: str, player: int) -> None: multiworld.get_entrance(entrance, player).access_rule = \ lambda state: state.has(entrance, player) and state.has(stage, player) +def has_all_items(multiworld: MultiWorld, items: Set[str], entrance: str, player: int) -> None: + multiworld.get_entrance(entrance, player).access_rule = \ + lambda state: state.has_all(items, player) and state.has(entrance, player) + + # Checks to see if chest/shrine are accessible -def has_location_access_rule(multiworld: MultiWorld, environment: str, player: int, item_number: int, item_type: str): +def has_location_access_rule(multiworld: MultiWorld, environment: str, player: int, item_number: int, item_type: str)\ + -> None: if item_number == 1: multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ lambda state: state.has(environment, player) + # scavengers need to be locked till after a full loop since that is when they are capable of spawning. + # (While technically the requirement is just beating 5 stages, this will ensure that the player will have + # a long enough run to have enough director credits for scavengers and + # help prevent being stuck in the same stages until that point). if item_type == "Scavenger": multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ - lambda state: state.has(environment, player) and state.has("Stage_4", player) + lambda state: state.has(environment, player) and state.has("Stage 5", player) else: multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ lambda state: check_location(state, environment, player, item_number, item_type) -def check_location(state, environment: str, player: int, item_number: int, item_name: str): +def check_location(state, environment: str, player: int, item_number: int, item_name: str) -> bool: return state.can_reach(f"{environment}: {item_name} {item_number - 1}", "Location", player) # unlock event to next set of stages -def get_stage_event(multiworld: MultiWorld, player: int, stage_number: int): - if not multiworld.dlc_sotv[player]: - environment_name = multiworld.random.choices(list(environment_vanilla_orderedstages_table[stage_number].keys()), - k=1) - else: - environment_name = multiworld.random.choices(list(environment_orderedstages_table[stage_number].keys()), k=1) - multiworld.get_location(f"Stage_{stage_number + 1}", player).access_rule = \ - lambda state: get_one_of_the_stages(state, environment_name[0], player) - - -def get_one_of_the_stages(state: CollectionState, stage: str, player: int): - return state.has(stage, player) - - -def set_rules(multiworld: MultiWorld, player: int) -> None: - if multiworld.goal[player] == "classic": +def get_stage_event(multiworld: MultiWorld, player: int, stage_number: int) -> None: + if stage_number == 4: + return + multiworld.get_entrance(f"OrderedStage_{stage_number + 1}", player).access_rule = \ + lambda state: state.has(f"Stage {stage_number + 1}", player) + + +def set_rules(ror2_world: "RiskOfRainWorld") -> None: + player = ror2_world.player + multiworld = ror2_world.multiworld + ror2_options = ror2_world.options + if ror2_options.goal == "classic": # classic mode - total_locations = multiworld.total_locations[player].value # total locations for current player + total_locations = ror2_options.total_locations.value # total locations for current player else: # explore mode total_locations = len( - orderedstage_location.get_locations( - chests=multiworld.chests_per_stage[player].value, - shrines=multiworld.shrines_per_stage[player].value, - scavengers=multiworld.scavengers_per_stage[player].value, - scanners=multiworld.scanner_per_stage[player].value, - altars=multiworld.altars_per_stage[player].value, - dlc_sotv=multiworld.dlc_sotv[player].value + get_locations( + chests=ror2_options.chests_per_stage.value, + shrines=ror2_options.shrines_per_stage.value, + scavengers=ror2_options.scavengers_per_stage.value, + scanners=ror2_options.scanner_per_stage.value, + altars=ror2_options.altars_per_stage.value, + dlc_sotv=bool(ror2_options.dlc_sotv.value) ) ) @@ -64,14 +73,15 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: divisions = total_locations // event_location_step total_revivals = multiworld.worlds[player].total_revivals # pulling this info we calculated in generate_basic - if multiworld.goal[player] == "classic": + if ror2_options.goal == "classic": # classic mode if divisions: for i in range(1, divisions + 1): # since divisions is the floor of total_locations / 25 if i * event_location_step != total_locations: event_loc = multiworld.get_location(f"Pickup{i * event_location_step}", player) set_rule(event_loc, - lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}", "Location", player)) + lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}", + "Location", player)) # we want to create a rule for each of the 25 locations per division for n in range(i * event_location_step, (i + 1) * event_location_step + 1): if n > total_locations: @@ -84,27 +94,18 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: lambda state, n=n: state.can_reach(f"ItemPickup{n - 1}", "Location", player)) set_rule(multiworld.get_location("Victory", player), lambda state: state.can_reach(f"ItemPickup{total_locations}", "Location", player)) - if total_revivals or multiworld.start_with_revive[player].value: + if total_revivals or ror2_options.start_with_revive.value: add_rule(multiworld.get_location("Victory", player), lambda state: state.has("Dio's Best Friend", player, - total_revivals + multiworld.start_with_revive[player])) + total_revivals + ror2_options.start_with_revive)) - elif multiworld.goal[player] == "explore": - # When explore_mode is used, - # scavengers need to be locked till after a full loop since that is when they are capable of spawning. - # (While technically the requirement is just beating 5 stages, this will ensure that the player will have - # a long enough run to have enough director credits for scavengers and - # help prevent being stuck in the same stages until that point.) - - for location in multiworld.get_locations(player): - if "Scavenger" in location.name: - add_rule(location, lambda state: state.has("Stage_5", player)) - # Regions - chests = multiworld.chests_per_stage[player] - shrines = multiworld.shrines_per_stage[player] - newts = multiworld.altars_per_stage[player] - scavengers = multiworld.scavengers_per_stage[player] - scanners = multiworld.scanner_per_stage[player] + else: + # explore mode + chests = ror2_options.chests_per_stage.value + shrines = ror2_options.shrines_per_stage.value + newts = ror2_options.altars_per_stage.value + scavengers = ror2_options.scavengers_per_stage.value + scanners = ror2_options.scanner_per_stage.value for i in range(len(environment_vanilla_orderedstages_table)): for environment_name, _ in environment_vanilla_orderedstages_table[i].items(): # Make sure to go through each location @@ -120,10 +121,10 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: for newt in range(1, newts + 1): has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar") if i > 0: - has_entrance_access_rule(multiworld, f"Stage_{i}", environment_name, player) + has_entrance_access_rule(multiworld, f"Stage {i}", environment_name, player) get_stage_event(multiworld, player, i) - if multiworld.dlc_sotv[player]: + if ror2_options.dlc_sotv: for i in range(len(environment_sotv_orderedstages_table)): for environment_name, _ in environment_sotv_orderedstages_table[i].items(): # Make sure to go through each location @@ -139,16 +140,19 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: for newt in range(1, newts + 1): has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar") if i > 0: - has_entrance_access_rule(multiworld, f"Stage_{i}", environment_name, player) - has_entrance_access_rule(multiworld, f"Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole", + has_entrance_access_rule(multiworld, f"Stage {i}", environment_name, player) + has_entrance_access_rule(multiworld, "Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole", player) - has_entrance_access_rule(multiworld, f"Stage_1", "Hidden Realm: Bazaar Between Time", player) - has_entrance_access_rule(multiworld, f"Hidden Realm: Bazaar Between Time", "Void Fields", player) - has_entrance_access_rule(multiworld, f"Stage_5", "Commencement", player) - has_entrance_access_rule(multiworld, f"Stage_5", "Hidden Realm: A Moment, Fractured", player) + has_entrance_access_rule(multiworld, "Stage 1", "Hidden Realm: Bazaar Between Time", player) + has_entrance_access_rule(multiworld, "Hidden Realm: Bazaar Between Time", "Void Fields", player) + has_entrance_access_rule(multiworld, "Stage 5", "Commencement", player) + has_entrance_access_rule(multiworld, "Stage 5", "Hidden Realm: A Moment, Fractured", player) has_entrance_access_rule(multiworld, "Beads of Fealty", "Hidden Realm: A Moment, Whole", player) - if multiworld.dlc_sotv[player]: - has_entrance_access_rule(multiworld, f"Stage_5", "Void Locus", player) - has_entrance_access_rule(multiworld, f"Void Locus", "The Planetarium", player) + if ror2_options.dlc_sotv: + has_entrance_access_rule(multiworld, "Stage 5", "The Planetarium", player) + has_entrance_access_rule(multiworld, "Stage 5", "Void Locus", player) + if ror2_options.victory == "voidling": + has_all_items(multiworld, {"Stage 5", "The Planetarium"}, "Commencement", player) + # Win Condition multiworld.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/ror2/test/__init__.py b/worlds/ror2/test/__init__.py new file mode 100644 index 000000000000..87d8183ab847 --- /dev/null +++ b/worlds/ror2/test/__init__.py @@ -0,0 +1,5 @@ +from test.bases import WorldTestBase + + +class RoR2TestBase(WorldTestBase): + game = "Risk of Rain 2" diff --git a/worlds/ror2/test/test_any_goal.py b/worlds/ror2/test/test_any_goal.py new file mode 100644 index 000000000000..18d49944195d --- /dev/null +++ b/worlds/ror2/test/test_any_goal.py @@ -0,0 +1,26 @@ +from . import RoR2TestBase + + +class DLCTest(RoR2TestBase): + options = { + "dlc_sotv": "true", + "victory": "any" + } + + def test_commencement_victory(self) -> None: + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.assertBeatable(False) + self.collect_by_name("Commencement") + self.assertBeatable(True) + + def test_planetarium_victory(self) -> None: + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.assertBeatable(False) + self.collect_by_name("The Planetarium") + self.assertBeatable(True) + + def test_moment_whole_victory(self) -> None: + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.assertBeatable(False) + self.collect_by_name("Hidden Realm: A Moment, Whole") + self.assertBeatable(True) diff --git a/worlds/ror2/test/test_classic.py b/worlds/ror2/test/test_classic.py new file mode 100644 index 000000000000..90ed2302b272 --- /dev/null +++ b/worlds/ror2/test/test_classic.py @@ -0,0 +1,7 @@ +from . import RoR2TestBase + + +class ClassicTest(RoR2TestBase): + options = { + "goal": "classic", + } diff --git a/worlds/ror2/test/test_limbo_goal.py b/worlds/ror2/test/test_limbo_goal.py new file mode 100644 index 000000000000..f8757a917641 --- /dev/null +++ b/worlds/ror2/test/test_limbo_goal.py @@ -0,0 +1,15 @@ +from . import RoR2TestBase + + +class LimboGoalTest(RoR2TestBase): + options = { + "victory": "limbo" + } + + def test_limbo(self) -> None: + self.collect_all_but(["Hidden Realm: A Moment, Whole", "Victory"]) + self.assertFalse(self.can_reach_entrance("Hidden Realm: A Moment, Whole")) + self.assertBeatable(False) + self.collect_by_name("Hidden Realm: A Moment, Whole") + self.assertTrue(self.can_reach_entrance("Hidden Realm: A Moment, Whole")) + self.assertBeatable(True) diff --git a/worlds/ror2/test/test_mithrix_goal.py b/worlds/ror2/test/test_mithrix_goal.py new file mode 100644 index 000000000000..7ed9a2cd73a2 --- /dev/null +++ b/worlds/ror2/test/test_mithrix_goal.py @@ -0,0 +1,25 @@ +from . import RoR2TestBase + + +class MithrixGoalTest(RoR2TestBase): + options = { + "victory": "mithrix" + } + + def test_mithrix(self) -> None: + self.collect_all_but(["Commencement", "Victory"]) + self.assertFalse(self.can_reach_entrance("Commencement")) + self.assertBeatable(False) + self.collect_by_name("Commencement") + self.assertTrue(self.can_reach_entrance("Commencement")) + self.assertBeatable(True) + + def test_stage5(self) -> None: + self.collect_all_but(["Stage 4", "Sky Meadow", "Victory"]) + self.assertFalse(self.can_reach_entrance("Sky Meadow")) + self.assertBeatable(False) + self.collect_by_name("Sky Meadow") + self.assertFalse(self.can_reach_entrance("Sky Meadow")) + self.collect_by_name("Stage 4") + self.assertTrue(self.can_reach_entrance("Sky Meadow")) + self.assertBeatable(True) diff --git a/worlds/ror2/test/test_voidling_goal.py b/worlds/ror2/test/test_voidling_goal.py new file mode 100644 index 000000000000..a7520a5c5f95 --- /dev/null +++ b/worlds/ror2/test/test_voidling_goal.py @@ -0,0 +1,28 @@ +from . import RoR2TestBase + + +class VoidlingGoalTest(RoR2TestBase): + options = { + "dlc_sotv": "true", + "victory": "voidling" + } + + def test_planetarium(self) -> None: + self.collect_all_but(["The Planetarium", "Victory"]) + self.assertFalse(self.can_reach_entrance("The Planetarium")) + self.assertBeatable(False) + self.collect_by_name("The Planetarium") + self.assertTrue(self.can_reach_entrance("The Planetarium")) + self.assertBeatable(True) + + def test_void_locus_to_victory(self) -> None: + self.collect_all_but(["Void Locus", "Commencement"]) + self.assertFalse(self.can_reach_location("Victory")) + self.collect_by_name("Void Locus") + self.assertTrue(self.can_reach_entrance("Victory")) + + def test_commencement_to_victory(self) -> None: + self.collect_all_but(["Void Locus", "Commencement"]) + self.assertFalse(self.can_reach_location("Victory")) + self.collect_by_name("Commencement") + self.assertTrue(self.can_reach_location("Victory")) From 4a9d075b776501ea94b6805f04a822eac70e4d3f Mon Sep 17 00:00:00 2001 From: digiholic Date: Wed, 22 Nov 2023 08:45:32 -0700 Subject: [PATCH 049/104] MMBN3: Adds instructions for using the Legacy Collection ROM for setup (#2120) Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> --- worlds/mmbn3/docs/setup_en.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/worlds/mmbn3/docs/setup_en.md b/worlds/mmbn3/docs/setup_en.md index 309c07f5cfc4..b5ff1625c819 100644 --- a/worlds/mmbn3/docs/setup_en.md +++ b/worlds/mmbn3/docs/setup_en.md @@ -12,7 +12,8 @@ As we are using Bizhawk, this guide is only applicable to Windows and Linux syst - Windows users must run the prereq installer first, which can also be found at the above link. - The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases) (select `MegaMan Battle Network 3 Client` during installation). -- A US MegaMan Battle Network 3 Blue Rom +- A US MegaMan Battle Network 3 Blue Rom. If you have the [MegaMan Battle Network Legacy Collection Vol. 1](https://store.steampowered.com/app/1798010/Mega_Man_Battle_Network_Legacy_Collection_Vol_1/) +on Steam, you can obtain a copy of this ROM from the game's files, see instructions below. ## Configuring Bizhawk @@ -35,6 +36,14 @@ To do so, we simply have to search any GBA rom we happened to own, right click a the list that appears and select the bottom option "Look for another application", then browse to the Bizhawk folder and select EmuHawk.exe. +## Extracting a ROM from the Legacy Collection + +The Steam version of the Legacy Collection contains unmodified GBA ROMs in its files. You can extract these for use with Archipelago. + +1. Open the Legacy Collection Vol. 1's Game Files (Right click on the game in your Library, then open Properties -> Installed Files -> Browse) +2. Open the file `exe/data/exe3b.dat` in a zip-extracting program such as 7-Zip or WinRAR. +3. Extract the file `rom_b_e.srl` somewhere and rename it to `Mega Man Battle Network 3 - Blue Version (USA).gba` + ## Configuring your YAML file ### What is a YAML file and why do I need one? @@ -76,4 +85,4 @@ Don't forget to start manipulating RNG early by shouting during generation: JACK IN! [Your name]! EXECUTE! -``` \ No newline at end of file +``` From cfd2e9c47f3f3bd06defbf05c6fd2d42d9e697dd Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Wed, 22 Nov 2023 10:04:10 -0600 Subject: [PATCH 050/104] Core: Increment Archipelago Version (#2483) --- Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index bb68602cceb3..5955e924322f 100644 --- a/Utils.py +++ b/Utils.py @@ -47,7 +47,7 @@ def as_simple_string(self) -> str: return ".".join(str(item) for item in self) -__version__ = "0.4.3" +__version__ = "0.4.4" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") From 0f98cf525f89e1c8d608a431923fb9c42df8c99c Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 22 Nov 2023 11:04:33 -0500 Subject: [PATCH 051/104] Stardew Valley: Generate proper filler for item links (#2069) Co-authored-by: Zach Parks --- worlds/stardew_valley/__init__.py | 35 +++++++- worlds/stardew_valley/data/bundle_data.py | 3 +- worlds/stardew_valley/items.py | 26 ++++-- worlds/stardew_valley/test/TestItemLink.py | 100 +++++++++++++++++++++ 4 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 worlds/stardew_valley/test/TestItemLink.py diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 24ffa8c1add8..aa825af302eb 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -1,16 +1,16 @@ import logging from typing import Dict, Any, Iterable, Optional, Union, Set, List -from BaseClasses import Region, Entrance, Location, Item, Tutorial, CollectionState, ItemClassification, MultiWorld +from BaseClasses import Region, Entrance, Location, Item, Tutorial, CollectionState, ItemClassification, MultiWorld, Group as ItemLinkGroup from Options import PerGameCommonOptions from worlds.AutoWorld import World, WebWorld from . import rules from .bundles import get_all_bundles, Bundle -from .items import item_table, create_items, ItemData, Group, items_by_group +from .items import item_table, create_items, ItemData, Group, items_by_group, get_all_filler_items, remove_limited_amount_packs from .locations import location_table, create_locations, LocationData from .logic import StardewLogic, StardewRule, True_, MAX_MONTHS from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, NumberOfLuckBuffs, NumberOfMovementBuffs, \ - BackpackProgression, BuildingProgression, ExcludeGingerIsland + BackpackProgression, BuildingProgression, ExcludeGingerIsland, TrapItems from .presets import sv_options_presets from .regions import create_regions from .rules import set_rules @@ -74,6 +74,7 @@ class StardewValleyWorld(World): def __init__(self, world: MultiWorld, player: int): super().__init__(world, player) self.all_progression_items = set() + self.filler_item_pool_names = [] def generate_early(self): self.force_change_options_if_incompatible() @@ -270,7 +271,33 @@ def generate_basic(self): pass def get_filler_item_name(self) -> str: - return "Joja Cola" + if not self.filler_item_pool_names: + self.generate_filler_item_pool_names() + return self.random.choice(self.filler_item_pool_names) + + def generate_filler_item_pool_names(self): + include_traps, exclude_island = self.get_filler_item_rules() + available_filler = get_all_filler_items(include_traps, exclude_island) + available_filler = remove_limited_amount_packs(available_filler) + self.filler_item_pool_names = [item.name for item in available_filler] + + def get_filler_item_rules(self): + if self.player in self.multiworld.groups: + link_group: ItemLinkGroup = self.multiworld.groups[self.player] + include_traps = True + exclude_island = False + for player in link_group["players"]: + player_options = self.multiworld.worlds[player].options + if self.multiworld.game[player] != self.game: + + continue + if player_options.trap_items == TrapItems.option_no_traps: + include_traps = False + if player_options.exclude_ginger_island == ExcludeGingerIsland.option_true: + exclude_island = True + return include_traps, exclude_island + else: + return self.options.trap_items != TrapItems.option_no_traps, self.options.exclude_ginger_island == ExcludeGingerIsland.option_true def fill_slot_data(self) -> Dict[str, Any]: diff --git a/worlds/stardew_valley/data/bundle_data.py b/worlds/stardew_valley/data/bundle_data.py index 8a1a6a5bcf53..183383ccbf3a 100644 --- a/worlds/stardew_valley/data/bundle_data.py +++ b/worlds/stardew_valley/data/bundle_data.py @@ -303,8 +303,7 @@ def __lt__(self, other): river_fish_items = [chub, catfish, rainbow_trout, lingcod, walleye, perch, pike, bream, salmon, sunfish, tiger_trout, shad, smallmouth_bass, dorado] -lake_fish_items = [chub, rainbow_trout, lingcod, walleye, perch, carp, midnight_carp, - largemouth_bass, sturgeon, bullhead, midnight_carp] +lake_fish_items = [chub, rainbow_trout, lingcod, walleye, perch, carp, midnight_carp, largemouth_bass, sturgeon, bullhead] ocean_fish_items = [tilapia, pufferfish, tuna, super_cucumber, flounder, anchovy, sardine, red_mullet, herring, eel, octopus, red_snapper, squid, sea_cucumber, albacore, halibut] night_fish_items = [walleye, bream, super_cucumber, eel, squid, midnight_carp] diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index a5a370aa08cd..1f0735f4aebc 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -468,10 +468,6 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options items_already_added: List[Item], number_locations: int) -> List[Item]: include_traps = options.trap_items != TrapItems.option_no_traps - all_filler_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK]] - all_filler_packs.extend(items_by_group[Group.TRASH]) - if include_traps: - all_filler_packs.extend(items_by_group[Group.TRAP]) items_already_added_names = [item.name for item in items_already_added] useful_resource_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK_USEFUL] if pack.name not in items_already_added_names] @@ -484,8 +480,9 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options if include_traps: priority_filler_items.extend(trap_items) - all_filler_packs = remove_excluded_packs(all_filler_packs, options) - priority_filler_items = remove_excluded_packs(priority_filler_items, options) + exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true + all_filler_packs = get_all_filler_items(include_traps, exclude_ginger_island) + priority_filler_items = remove_excluded_packs(priority_filler_items, exclude_ginger_island) number_priority_items = len(priority_filler_items) required_resource_pack = number_locations - len(items_already_added) @@ -519,8 +516,21 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options return items -def remove_excluded_packs(packs, options: StardewValleyOptions): +def remove_excluded_packs(packs, exclude_ginger_island: bool): included_packs = [pack for pack in packs if Group.DEPRECATED not in pack.groups] - if options.exclude_ginger_island == ExcludeGingerIsland.option_true: + if exclude_ginger_island: included_packs = [pack for pack in included_packs if Group.GINGER_ISLAND not in pack.groups] return included_packs + + +def remove_limited_amount_packs(packs): + return [pack for pack in packs if Group.MAXIMUM_ONE not in pack.groups and Group.EXACTLY_TWO not in pack.groups] + + +def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool): + all_filler_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK]] + all_filler_packs.extend(items_by_group[Group.TRASH]) + if include_traps: + all_filler_packs.extend(items_by_group[Group.TRAP]) + all_filler_packs = remove_excluded_packs(all_filler_packs, exclude_ginger_island) + return all_filler_packs diff --git a/worlds/stardew_valley/test/TestItemLink.py b/worlds/stardew_valley/test/TestItemLink.py new file mode 100644 index 000000000000..f55ab8ca347d --- /dev/null +++ b/worlds/stardew_valley/test/TestItemLink.py @@ -0,0 +1,100 @@ +from . import SVTestBase +from .. import options, item_table, Group + +max_iterations = 2000 + + +class TestItemLinksEverythingIncluded(SVTestBase): + options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, + options.TrapItems.internal_name: options.TrapItems.option_medium} + + def test_filler_of_all_types_generated(self): + max_number_filler = 115 + filler_generated = [] + at_least_one_trap = False + at_least_one_island = False + for i in range(0, max_iterations): + filler = self.multiworld.worlds[1].get_filler_item_name() + if filler in filler_generated: + continue + filler_generated.append(filler) + self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups) + self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups) + if Group.TRAP in item_table[filler].groups: + at_least_one_trap = True + if Group.GINGER_ISLAND in item_table[filler].groups: + at_least_one_island = True + if len(filler_generated) >= max_number_filler: + break + self.assertTrue(at_least_one_trap) + self.assertTrue(at_least_one_island) + self.assertGreaterEqual(len(filler_generated), max_number_filler) + + +class TestItemLinksNoIsland(SVTestBase): + options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.TrapItems.internal_name: options.TrapItems.option_medium} + + def test_filler_has_no_island_but_has_traps(self): + max_number_filler = 109 + filler_generated = [] + at_least_one_trap = False + for i in range(0, max_iterations): + filler = self.multiworld.worlds[1].get_filler_item_name() + if filler in filler_generated: + continue + filler_generated.append(filler) + self.assertNotIn(Group.GINGER_ISLAND, item_table[filler].groups) + self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups) + self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups) + if Group.TRAP in item_table[filler].groups: + at_least_one_trap = True + if len(filler_generated) >= max_number_filler: + break + self.assertTrue(at_least_one_trap) + self.assertGreaterEqual(len(filler_generated), max_number_filler) + + +class TestItemLinksNoTraps(SVTestBase): + options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, + options.TrapItems.internal_name: options.TrapItems.option_no_traps} + + def test_filler_has_no_traps_but_has_island(self): + max_number_filler = 100 + filler_generated = [] + at_least_one_island = False + for i in range(0, max_iterations): + filler = self.multiworld.worlds[1].get_filler_item_name() + if filler in filler_generated: + continue + filler_generated.append(filler) + self.assertNotIn(Group.TRAP, item_table[filler].groups) + self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups) + self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups) + if Group.GINGER_ISLAND in item_table[filler].groups: + at_least_one_island = True + if len(filler_generated) >= max_number_filler: + break + self.assertTrue(at_least_one_island) + self.assertGreaterEqual(len(filler_generated), max_number_filler) + + +class TestItemLinksNoTrapsAndIsland(SVTestBase): + options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.TrapItems.internal_name: options.TrapItems.option_no_traps} + + def test_filler_generated_without_island_or_traps(self): + max_number_filler = 94 + filler_generated = [] + for i in range(0, max_iterations): + filler = self.multiworld.worlds[1].get_filler_item_name() + if filler in filler_generated: + continue + filler_generated.append(filler) + self.assertNotIn(Group.GINGER_ISLAND, item_table[filler].groups) + self.assertNotIn(Group.TRAP, item_table[filler].groups) + self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups) + self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups) + if len(filler_generated) >= max_number_filler: + break + self.assertGreaterEqual(len(filler_generated), max_number_filler) From ee76cce1a35a186f09db62a41bcb36b927732ba2 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Wed, 22 Nov 2023 10:42:21 -0600 Subject: [PATCH 052/104] Rogue Legacy: Fix a preset including an option that prevents generation. (#2473) --- worlds/rogue_legacy/Presets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/rogue_legacy/Presets.py b/worlds/rogue_legacy/Presets.py index a4284e9f7d34..2dfeee64d8ca 100644 --- a/worlds/rogue_legacy/Presets.py +++ b/worlds/rogue_legacy/Presets.py @@ -35,7 +35,7 @@ "equip_pool": "random", "crit_chance_pool": "random", "crit_damage_pool": "random", - "allow_default_names": False, + "allow_default_names": True, "death_link": "random", }, # A preset I actually use, using some literal values and some from the option itself. From af0d47b444871a4309bcefab04f02e615f8cd348 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Wed, 22 Nov 2023 12:13:02 -0500 Subject: [PATCH 053/104] Core: Provide a better error message if only weights.yaml is provided with players: 0 (#2227) --- Generate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Generate.py b/Generate.py index 8113d8a0d7da..74244ec23102 100644 --- a/Generate.py +++ b/Generate.py @@ -127,6 +127,13 @@ def main(args=None, callback=ERmain): player_id += 1 args.multi = max(player_id - 1, args.multi) + + if args.multi == 0: + raise ValueError( + "No individual player files found and number of players is 0. " + "Provide individual player files or specify the number of players via host.yaml or --multi." + ) + logging.info(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, " f"{seed_name} Seed {seed} with plando: {args.plando}") From b2e7ce2c36df5546510c046a0592e4233c35c79c Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 22 Nov 2023 10:21:15 -0800 Subject: [PATCH 054/104] Pokemon Emerald: Fix using wrong key for extracted constant (#2484) --- worlds/pokemon_emerald/pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py index b461d006a46f..13c92ddc09bd 100644 --- a/worlds/pokemon_emerald/pokemon.py +++ b/worlds/pokemon_emerald/pokemon.py @@ -173,7 +173,7 @@ def get_random_move( # We're either matching types or failed to pick a move above if type_target is None: - possible_moves = [i for i in range(data.constants["MOVE_COUNT"]) if i not in expanded_blacklist] + possible_moves = [i for i in range(data.constants["MOVES_COUNT"]) if i not in expanded_blacklist] else: possible_moves = [move for move in _moves_by_type[type_target[0]] if move not in expanded_blacklist] + \ [move for move in _moves_by_type[type_target[1]] if move not in expanded_blacklist] From 0d38b415404bca2b5bcb7bca4ef4c2c7ca43d0b6 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 23 Nov 2023 06:00:46 -0800 Subject: [PATCH 055/104] BizHawkClient: Add support for multiple concurrent instances (#2475) This allows multiple client/connector pairs to run at the same time. It also includes a few other miscellaneous small changes that accumulated as I went. They can be split if desired - Whatever the `client_socket:send` line (~440) was doing with that missing operator, it's no longer doing. Don't ask me how it was working before. Lua is witchcraft. - Removed the `settimeout(2)` which causes the infamous emulator freeze (and replaced it with a `settimeout(0)` when the server socket is created). It appears to be unnecessary to set a timeout for discovering a client. Maybe at some point in time it was useful to keep the success rate for connecting high, but it seems to not be a problem if the timeout is 0 instead. - Also updated the Emerald setup to remove mention of the freezing. - Connector script now picks the first port that's not in use in a range of 5 ports. - To summarize why I was previously under the impression that multiple running scripts would not detect when a port was in use: 1. Calling `socket.bind` in the existing script will first create an ipv6 socket. 2. A second concurrent script trying to bind to the same port would I think fail to create an ipv6 socket but then succeed in creating an ipv4 socket on the same port. 3. That second socket could never communicate with a client; extra clients would just bounce off the first script. 4. The third concurrent script will then fail on both and actually give an `address already in use` error. - I'm not _really_ sure what's going on there. But forcing one or the other by calling `socket.tcp4()` or `socket.tcp6()` means that only one script will believe it has the port while any others will give `address already in use` as you'd expect. - As a side note, our `socket.lua` is much wonkier than I had previously thought. I understand some parts were added for LADX and when BizHawk 2.9 came out, but as far back as the file's history in this repo, it has provided a strange, modified interface as compared to the file it was originally derived from, to no benefit as far as I can tell. - The connector script closes `server` once it finds a client and opens a new one if the connection drops. I'm not sure this ultimately has an effect, but it seems more proper. - If the connector script's main function returns because of some error or refusal to proceed, the script no longer tries to resume the coroutine it was part of, which would flood the log with irrelevant errors. - Creating `SyncError`s in `guarded_read` and `guarded_write` would raise its own error because the wrong variable was being used in its message. - A call to `_bizhawk.connect` can take a while as the client tries the possible ports. There's a modification that will wait on either the `connect` or the exit event. And if the exit event fires while still looking for a connector script, this cancels the `connect` so the window can close. - Related: It takes 2-3 seconds for a call to `asyncio.open_connection` to come back with any sort of response on my machine, which can be significant now that we're trying multiple ports in sequence. I guess it could fire off 5 tasks at once. Might cause some weirdness if there exist multiple scripts and multiple clients looking for each other at the same time. - Also related: The first time a client attempts to connect to a script, they accept each other and start communicating as expected. The second client to try that port seems to believe it connects and will then time out on the first message. And then all subsequent attempts to connect to that port by any client will be refused (as expected) until the script shuts down or restarts. I haven't been able to explain this behavior. It adds more time to a client's search for a script, but doesn't ultimately cause problems. --- data/lua/connector_bizhawk_generic.lua | 99 ++++++++++++++++--------- worlds/_bizhawk/__init__.py | 36 ++++++--- worlds/_bizhawk/context.py | 13 +++- worlds/pokemon_emerald/docs/setup_en.md | 4 +- 4 files changed, 104 insertions(+), 48 deletions(-) diff --git a/data/lua/connector_bizhawk_generic.lua b/data/lua/connector_bizhawk_generic.lua index b0b06de447bb..c4e729300dac 100644 --- a/data/lua/connector_bizhawk_generic.lua +++ b/data/lua/connector_bizhawk_generic.lua @@ -249,6 +249,24 @@ Response: - `err` (`string`): A description of the problem ]] +local bizhawk_version = client.getversion() +local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)") +bizhawk_major = tonumber(bizhawk_major) +bizhawk_minor = tonumber(bizhawk_minor) +if bizhawk_patch == "" then + bizhawk_patch = 0 +else + bizhawk_patch = tonumber(bizhawk_patch) +end + +local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)") +lua_major = tonumber(lua_major) +lua_minor = tonumber(lua_minor) + +if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then + require("lua_5_3_compat") +end + local base64 = require("base64") local socket = require("socket") local json = require("json") @@ -257,7 +275,9 @@ local json = require("json") -- Will cause lag due to large console output local DEBUG = false -local SOCKET_PORT = 43055 +local SOCKET_PORT_FIRST = 43055 +local SOCKET_PORT_RANGE_SIZE = 5 +local SOCKET_PORT_LAST = SOCKET_PORT_FIRST + SOCKET_PORT_RANGE_SIZE local STATE_NOT_CONNECTED = 0 local STATE_CONNECTED = 1 @@ -277,24 +297,6 @@ local locked = false local rom_hash = nil -local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)") -lua_major = tonumber(lua_major) -lua_minor = tonumber(lua_minor) - -if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then - require("lua_5_3_compat") -end - -local bizhawk_version = client.getversion() -local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)") -bizhawk_major = tonumber(bizhawk_major) -bizhawk_minor = tonumber(bizhawk_minor) -if bizhawk_patch == "" then - bizhawk_patch = 0 -else - bizhawk_patch = tonumber(bizhawk_patch) -end - function queue_push (self, value) self[self.right] = value self.right = self.right + 1 @@ -435,7 +437,7 @@ function send_receive () end if message == "VERSION" then - local result, err client_socket:send(tostring(SCRIPT_VERSION).."\n") + client_socket:send(tostring(SCRIPT_VERSION).."\n") else local res = {} local data = json.decode(message) @@ -463,14 +465,45 @@ function send_receive () end end -function main () - server, err = socket.bind("localhost", SOCKET_PORT) +function initialize_server () + local err + local port = SOCKET_PORT_FIRST + local res = nil + + server, err = socket.socket.tcp4() + while res == nil and port <= SOCKET_PORT_LAST do + res, err = server:bind("localhost", port) + if res == nil and err ~= "address already in use" then + print(err) + return + end + + if res == nil then + port = port + 1 + end + end + + if port > SOCKET_PORT_LAST then + print("Too many instances of connector script already running. Exiting.") + return + end + + res, err = server:listen(0) + if err ~= nil then print(err) return end + server:settimeout(0) +end + +function main () while true do + if server == nil then + initialize_server() + end + current_time = socket.socket.gettime() timeout_timer = timeout_timer - (current_time - prev_time) message_timer = message_timer - (current_time - prev_time) @@ -482,16 +515,16 @@ function main () end if current_state == STATE_NOT_CONNECTED then - if emu.framecount() % 60 == 0 then - server:settimeout(2) + if emu.framecount() % 30 == 0 then + print("Looking for client...") local client, timeout = server:accept() if timeout == nil then print("Client connected") current_state = STATE_CONNECTED client_socket = client + server:close() + server = nil client_socket:settimeout(0) - else - print("No client found. Trying again...") end end else @@ -527,27 +560,27 @@ else emu.frameadvance() end end - + rom_hash = gameinfo.getromhash() - print("Waiting for client to connect. Emulation will freeze intermittently until a client is found.\n") + print("Waiting for client to connect. This may take longer the more instances of this script you have open at once.\n") local co = coroutine.create(main) function tick () local status, err = coroutine.resume(co) - - if not status then + + if not status and err ~= "cannot resume dead coroutine" then print("\nERROR: "..err) print("Consider reporting this crash.\n") if server ~= nil then server:close() end - + co = coroutine.create(main) end end - + -- Gambatte has a setting which can cause script execution to become -- misaligned, so for GB and GBC we explicitly set the callback on -- vblank instead. @@ -557,7 +590,7 @@ else else event.onframeend(tick) end - + while true do emu.frameadvance() end diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py index 340399083217..c3314e18dcc0 100644 --- a/worlds/_bizhawk/__init__.py +++ b/worlds/_bizhawk/__init__.py @@ -12,7 +12,8 @@ import typing -BIZHAWK_SOCKET_PORT = 43055 +BIZHAWK_SOCKET_PORT_RANGE_START = 43055 +BIZHAWK_SOCKET_PORT_RANGE_SIZE = 5 class ConnectionStatus(enum.IntEnum): @@ -45,11 +46,13 @@ class BizHawkContext: streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]] connection_status: ConnectionStatus _lock: asyncio.Lock + _port: typing.Optional[int] def __init__(self) -> None: self.streams = None self.connection_status = ConnectionStatus.NOT_CONNECTED self._lock = asyncio.Lock() + self._port = None async def _send_message(self, message: str): async with self._lock: @@ -86,15 +89,24 @@ async def _send_message(self, message: str): async def connect(ctx: BizHawkContext) -> bool: - """Attempts to establish a connection with the connector script. Returns True if successful.""" - try: - ctx.streams = await asyncio.open_connection("localhost", BIZHAWK_SOCKET_PORT) - ctx.connection_status = ConnectionStatus.TENTATIVE - return True - except (TimeoutError, ConnectionRefusedError): - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - return False + """Attempts to establish a connection with a connector script. Returns True if successful.""" + rotation_steps = 0 if ctx._port is None else ctx._port - BIZHAWK_SOCKET_PORT_RANGE_START + ports = [*range(BIZHAWK_SOCKET_PORT_RANGE_START, BIZHAWK_SOCKET_PORT_RANGE_START + BIZHAWK_SOCKET_PORT_RANGE_SIZE)] + ports = ports[rotation_steps:] + ports[:rotation_steps] + + for port in ports: + try: + ctx.streams = await asyncio.open_connection("localhost", port) + ctx.connection_status = ConnectionStatus.TENTATIVE + ctx._port = port + return True + except (TimeoutError, ConnectionRefusedError): + continue + + # No ports worked + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + return False def disconnect(ctx: BizHawkContext) -> None: @@ -233,7 +245,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[ return None else: if item["type"] != "READ_RESPONSE": - raise SyncError(f"Expected response of type READ_RESPONSE or GUARD_RESPONSE but got {res['type']}") + raise SyncError(f"Expected response of type READ_RESPONSE or GUARD_RESPONSE but got {item['type']}") ret.append(base64.b64decode(item["value"])) @@ -285,7 +297,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tupl return False else: if item["type"] != "WRITE_RESPONSE": - raise SyncError(f"Expected response of type WRITE_RESPONSE or GUARD_RESPONSE but got {res['type']}") + raise SyncError(f"Expected response of type WRITE_RESPONSE or GUARD_RESPONSE but got {item['type']}") return True diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index ccf747f15afe..2699b0f5f106 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -130,7 +130,18 @@ async def _game_watcher(ctx: BizHawkClientContext): logger.info("Waiting to connect to BizHawk...") showed_connecting_message = True - if not await connect(ctx.bizhawk_ctx): + # Since a call to `connect` can take a while to return, this will cancel connecting + # if the user has decided to close the client. + connect_task = asyncio.create_task(connect(ctx.bizhawk_ctx), name="BizHawkConnect") + exit_task = asyncio.create_task(ctx.exit_event.wait(), name="ExitWait") + await asyncio.wait([connect_task, exit_task], return_when=asyncio.FIRST_COMPLETED) + + if exit_task.done(): + connect_task.cancel() + return + + if not connect_task.result(): + # Failed to connect continue showed_no_handler_message = False diff --git a/worlds/pokemon_emerald/docs/setup_en.md b/worlds/pokemon_emerald/docs/setup_en.md index 6a1df8e5c32c..3c5c8c193aa9 100644 --- a/worlds/pokemon_emerald/docs/setup_en.md +++ b/worlds/pokemon_emerald/docs/setup_en.md @@ -52,8 +52,8 @@ you can re-open it from the launcher. 3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing. 4. In the Lua Console window, go to `Script > Open Script…`. 5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`. -6. The emulator may freeze every few seconds until it manages to connect to the client. This is expected. The BizHawk -Client window should indicate that it connected and recognized Pokemon Emerald. +6. The emulator and client will eventually connect to each other. The BizHawk Client window should indicate that it +connected and recognized Pokemon Emerald. 7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the top text field of the client and click Connect. From a7aed71fbe18e3498ceca73fe96c729a4cb46cec Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 23 Nov 2023 09:55:50 -0800 Subject: [PATCH 056/104] Pokemon Emerald: Fix opponent trainer moves sometimes being MOVE_NONE (#2487) --- worlds/pokemon_emerald/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index d3ced5f3ca62..b7730fbdf785 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -677,7 +677,7 @@ def randomize_opponent_parties() -> None: level_up_movepool = list({ move.move_id for move in new_species.learnset - if move.level <= pokemon.level + if move.move_id != 0 and move.level <= pokemon.level }) new_moves = ( From ae8a81c0cbf9ed3002bb0fd630a9a75601fc05b3 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Thu, 23 Nov 2023 12:56:55 -0500 Subject: [PATCH 057/104] Lingo: Change docs to link to the client in the Steam Workshop (#2486) --- worlds/lingo/docs/setup_en.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/worlds/lingo/docs/setup_en.md b/worlds/lingo/docs/setup_en.md index 97f3ce594063..0e68c7ed45c4 100644 --- a/worlds/lingo/docs/setup_en.md +++ b/worlds/lingo/docs/setup_en.md @@ -3,7 +3,7 @@ ## Required Software - [Lingo](https://store.steampowered.com/app/1814170/Lingo/) -- [Lingo Archipelago Randomizer](https://code.fourisland.com/lingo-archipelago/about/CHANGELOG.md) +- [Lingo Archipelago Randomizer](https://steamcommunity.com/sharedfiles/filedetails/?id=3092505110) ## Optional Software @@ -12,14 +12,12 @@ ## Installation -1. Download the Lingo Archipelago Randomizer from the above link. -2. Open up Lingo, go to settings, and click View Game Data. This should open up - a folder in Windows Explorer. -3. Unzip the contents of the randomizer into the "maps" folder. You may need to - create the "maps" folder if you have not played a custom Lingo map before. -4. Installation complete! You may have to click Return to go back to the main - menu and then click Settings again in order to get the randomizer to show up - in the level selection list. +You can use the above Steam Workshop link to subscribe to the Lingo Archipelago Randomizer. This will automatically +download the client, as well as update it whenever an update is available. + +If you don't want to use Steam Workshop, you can also +[download the randomizer manually](https://code.fourisland.com/lingo-archipelago/about/) using the instructions on the +linked page. ## Joining a Multiworld game From a1759ed7e158a013ce44f0336e581e3dd830e1b9 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:06:57 -0500 Subject: [PATCH 058/104] KH2: Update Game Docs (#2188) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- worlds/kh2/docs/en_Kingdom Hearts 2.md | 16 ++++++++++++++++ worlds/kh2/docs/setup_en.md | 19 +++++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/worlds/kh2/docs/en_Kingdom Hearts 2.md b/worlds/kh2/docs/en_Kingdom Hearts 2.md index 8258a099cc95..365ed37cb69b 100644 --- a/worlds/kh2/docs/en_Kingdom Hearts 2.md +++ b/worlds/kh2/docs/en_Kingdom Hearts 2.md @@ -63,6 +63,22 @@ For example, if you are fighting Roxas, receive Reflect Element, then die mid-fi - Customize the amount and level of progressive movement (Growth Abilities) you start with. - Customize start inventory, i.e., begin every run with certain items or spells of your choice. +

What are Lucky Emblems?

+Lucky Emblems are items that are required to beat the game if your goal is "Lucky Emblem Hunt".
+You can think of these as requiring X number of Proofs of Nonexistence to open the final door. + +

What is Hitlist/Bounties?

+The Hitlist goal adds "bounty" items to select late-game fights and locations, and you need to collect X number of them to win.
+The list of possible locations that can contain a bounty: + +- Each of the 13 Data Fights +- Max level (7) for each Drive Form +- Sephiroth +- Lingering Will +- Starry Hill +- Transport to Remembrance +- Each of the Goddess of Fate and Paradox Cups +

Quality of life:

diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md index 17235042e1fa..e0c8330632ef 100644 --- a/worlds/kh2/docs/setup_en.md +++ b/worlds/kh2/docs/setup_en.md @@ -66,30 +66,33 @@ Enter `The room's port number` into the top box where the x's are and pr - If you don't want to have a save in the GoA. Disconnect the client, load the auto save, and then reconnect the client after it loads the auto save. - Set fps limit to 60fps. - Run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out. +- Make sure to save in a different save slot when playing in an async or disconnecting from the server to play a different seed

Requirement/logic sheet

Have any questions on what's in logic? This spreadsheet has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1Embae0t7pIrbzvX-NRywk7bTHHEvuFzzQBUUpSUL7Ak/edit?usp=sharing)

F.A.Q.

+- Why is my HP/MP continuously increasing without stopping? + - You do not have `JaredWeakStrike/APCompanion` set up correctly. Make sure it is above the `GoA ROM Mod` in the mod manager. +- Why is my HP/MP continuously increasing without stopping when I have the APCompanion Mod? + - You have a leftover GOA lua script in your `Documents\KINGDOM HEARTS HD 1.5+2.5 ReMIX\scripts\KH2`. +- Why am I missing worlds/portals in the GoA? + - You are missing the required visit-locking item to access the world/portal. +- Why did I not load into the correct visit? + - You need to trigger a cutscene or visit The World That Never Was for it to register that you have received the item. +- What versions of Kingdom Hearts 2 are supported? + - Currently `only` the most up to date version on the Epic Game Store is supported: version `1.0.0.8_WW`. - Why am I getting wallpapered while going into a world for the first time? - Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide. - Why am I not getting magic? - If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. -- Why am I missing worlds/portals in the GoA? - - You are missing the required visit locking item to access the world/portal. -- What versions of Kingdom Hearts 2 are supported? - - Currently `only` the most up to date version on the Epic Game Store is supported `1.0.0.8_WW`. Emulator may be added in the future. - Why did I crash? - The port of Kingdom Hearts 2 can and will randomly crash, this is the fault of the game not the randomizer or the archipelago client. - If you have a continuous/constant crash (in the same area/event every time) you will want to reverify your installed files. This can be done by doing the following: Open Epic Game Store --> Library --> Click Triple Dots --> Manage --> Verify - Why am I getting dummy items or letters? - You will need to get the `JaredWeakStrike/APCompanion` (you can find how to get this if you scroll up) -- Why is my HP/MP continuously increasing without stopping? - - You do not have `JaredWeakStrike/APCompanion` setup correctly. Make Sure it is above the GOA in the mod manager. - Why am I not sending or receiving items? - Make sure you are connected to the KH2 client and the correct room (for more information scroll up) -- Why did I not load in to the correct visit - - You need to trigger a cutscene or visit The World That Never Was for it to update you have recevied the item. - Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save`? - Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. - How do I load an auto save? From 286dfd84c096c2cf6e3b594722a82b38a2a4601d Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Thu, 23 Nov 2023 19:10:32 +0100 Subject: [PATCH 059/104] sm64ex: Replace old launcher tutorial (#2383) --- worlds/sm64ex/docs/setup_en.md | 87 ++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index 38edeb2c4ab6..2817d3c324c0 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -2,71 +2,77 @@ ## Required Software -- Super Mario 64 US Rom (Japanese may work also. Europe and Shindou not supported) +- Super Mario 64 US or JP Rom (Europe and Shindou not supported) - Either of - - [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/releases) or + - [SM64AP-Launcher](https://github.com/N00byKing/SM64AP-Launcher/releases) or - Cloning and building [sm64ex](https://github.com/N00byKing/sm64ex) manually - Optional, for sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases) -NOTE: The above linked sm64pclauncher is a special version designed to work with the Archipelago build of sm64ex. +NOTE: The above linked launcher is a special version designed to work with the Archipelago build of sm64ex. You can use other sm64-port based builds with it, but you can't use a different launcher with the Archipelago build of sm64ex. ## Installation and Game Start Procedures -### Installation via sm64pclauncher (For Windows) +### Installation via SM64AP-Launcher + +*Windows Preparations* First, install [MSYS](https://www.msys2.org/) as described on the page. DO NOT INSTALL INTO A FOLDER PATH WITH SPACES. -Do all steps up to including step 6. -Best use default install directory. -Then follow the steps below - -1. Go to the page linked for sm64pclauncher, and press on the topmost entry -3. Scroll down, and download the zip file -4. Unpack the zip file in an empty folder -5. Run the Launcher and press build. -6. Set the location where you installed MSYS when prompted. Check the "Install Dependencies" Checkbox -7. Set the Repo link to `https://github.com/N00byKing/sm64ex` and the Branch to `archipelago` (Top two boxes). You can choose the folder (Secound Box) at will, as long as it does not exist yet -8. Point the Launcher to your Super Mario 64 US/JP Rom, and set the Region correspondingly -9. Set Build Options and press build. - - Recommended: To build faster, use `-jn` where `n` is the number of CPU cores to use (e.g., `-j4` to use 4 cores). - - Optional: Add options from [this list](https://github.com/sm64pc/sm64ex/wiki/Build-options), separated by spaces (e.g., `-j4 BETTERCAMERA=1`). -10. SM64EX will now be compiled. The Launcher will appear to have crashed, but this is not likely the case. Best wait a bit, but there may be a problem if it takes longer than 10 Minutes - -After it's done, the Build list should have another entry titled with what you named the folder in step 7. - -NOTE: For some reason first start of the game always crashes the launcher. Just restart it. -If it still crashes, recheck if you typed the launch options correctly (Described in "Joining a MultiWorld Game") +It is extremely encouraged to use the default install directory! +Then continue to `Using the Launcher` + +*Linux Preparations* + +You will need to install some dependencies before using the launcher. +The launcher itself needs `qt6`, `patch` and `git`, and building the game requires `sdl2 glew cmake python make` (If you install `jsoncpp` as well, it will be linked dynamically). +Then continue to `Using the Launcher` + +*Using the Launcher* + +1. Go to the page linked for SM64AP-Launcher, and press on the topmost entry +2. Scroll down, and download the zip file for your OS. +3. Unpack the zip file in an empty folder +4. Run the Launcher. On first start, press `Check Requirements`, which will guide you through the rest of the needed steps. + - Windows: If you did not use the default install directory for MSYS, close this window, check `Show advanced options` and reopen using `Re-check Requirements`. You can then set the path manually. +5. When finished, use `Compile default SM64AP build` to continue + - Advanced user can use `Show advanced options` to build with custom makeflags (`BETTERCAMERA`, `NODRAWINGDISTANCE`, ...), different repos and branches, and game patches such as 60FPS, Enhanced Moveset and others. +6. Press `Download Files` to prepare the build, afterwards `Create Build`. +7. SM64EX will now be compiled. This can take a while. + +After it's done, the build list should have another entry with the name you gave it. + +NOTE: If it does not start when pressing `Play selected build`, recheck if you typed the launch options correctly (Described in "Joining a MultiWorld Game") ### Manual Compilation (Linux/Windows) -*Windows Instructions* +*Windows Preparations* First, install [MSYS](https://www.msys2.org/) as described on the page. DO NOT INSTALL INTO A FOLDER PATH WITH SPACES. -After launching msys2, and update by entering `pacman -Syuu` in the command prompt. Next, install the relevant dependencies by entering `pacman -S unzip mingw-w64-x86_64-gcc mingw-w64-x86_64-glew mingw-w64-x86_64-SDL2 git make python3 mingw-w64-x86_64-cmake`. SM64EX will link `jsoncpp` dynamic if installed. If not, it will compile and link statically. +After launching msys2 using a MinGW x64 shell (there should be a start menu entry), update by entering `pacman -Syuu` in the command prompt. Next, install the relevant dependencies by entering `pacman -S unzip mingw-w64-x86_64-gcc mingw-w64-x86_64-glew mingw-w64-x86_64-SDL2 git make python3 mingw-w64-x86_64-cmake`. -After this, obtain the code base by cloning the relevant repository manually via `git clone --recursive https://github.com/N00byKing/sm64ex`. Ready your ROM by copying your legally dumped rom into your sm64ex folder (if you are not sure where your folder is located, do a quick Windows search for sm64ex). The name of the ROM needs to be `baserom.REGION.z64` where `REGION` is either `us` or `jp` respectively. +Continue to `Compiling`. -After all these preparatory steps have succeeded, type `make` in your command prompt and get ready to wait for a bit. If you want to speed up compilation, tell the compiler how many CPU cores to use by using `make -jn` where n is the number of cores you want. +*Linux Preparations* -After the compliation was successful, there will be a binary in your `sm64ex/build/REGION_pc/` folder. +Install the relevant dependencies `sdl2 glew cmake python make patch git`. SM64EX will link `jsoncpp` dynamic if installed. If not, it will compile and link statically. -*Linux Instructions* +Continue to `Compiling`. -Install the relevant dependencies `sdl2 glew cmake python make`. SM64EX will link `jsoncpp` dynamic if installed. If not, it will compile and link statically. +*Compiling* -After this, obtain the code base by cloning the relevant repository manually via `git clone --recursive https://github.com/N00byKing/sm64ex`. Ready your ROM by copying your legally dumped rom into your sm64ex folder. The name of the ROM needs to be `baserom.REGION.z64` where `REGION` is either `us` or `jp` respectively. +Obtain the code base by cloning the relevant repository via `git clone --recursive https://github.com/N00byKing/sm64ex`. Copy your legally dumped rom into your sm64ex folder (if you are not sure where your folder is located, do a quick Windows search for sm64ex). The name of the ROM needs to be `baserom.REGION.z64` where `REGION` is either `us` or `jp` respectively. -After all these preparatory steps have succeeded, type `make` in your command prompt and get ready to wait for a bit. If you want to speed up compilation, tell the compiler how many CPU cores to use by using `make -jn` where n is the number of cores you want. +After all these preparatory steps have succeeded, type `cd sm64ex && make` in your command prompt and get ready to wait for a bit. If you want to speed up compilation, tell the compiler how many CPU cores to use by using `make -jn` instead, where n is the number of cores you want. After the compliation was successful, there will be a binary in your `sm64ex/build/REGION_pc/` folder. ### Joining a MultiWorld Game To join, set the following launch options: `--sm64ap_name YourName --sm64ap_ip ServerIP:Port`. +For example, if you are hosting a game using the website, `YourName` will be the name from the Settings Page, `ServerIP` is `archipelago.gg` and `Port` the port given on the Archipelago room page. Optionally, add `--sm64ap_passwd "YourPassword"` if the room you are using requires a password. -The Name in this case is the one specified in your generated .yaml file. -In case you are using the Archipelago Website, the IP should be `archipelago.gg`. +Should your name or password have spaces, enclose it in quotes: `"YourPassword"` and `"YourName"`. Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that. Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text. @@ -81,7 +87,7 @@ Create a room and download the `.apsm64ex` file, and start the game with the `-- ### Optional: Using Batch Files to play offline and MultiWorld games -As an alternative to launching the game with sm64pclauncher, it is also possible to launch the completed build with the use of Windows batch files. This has the added benefit of streamlining the join process so that manual editing of connection info is not needed for each new game. However, you'll need to be somewhat comfortable with creating and using batch files. +As an alternative to launching the game with SM64AP-Launcher, it is also possible to launch the completed build with the use of Windows batch files. This has the added benefit of streamlining the join process so that manual editing of connection info is not needed for each new game. However, you'll need to be somewhat comfortable with creating and using batch files. IMPORTANT NOTE: The remainder of this section uses copy-and-paste code that assumes you're using the US version. If you instead use the Japanese version, you'll need to edit the EXE name accordingly by changing "sm64.us.f3dex2e.exe" to "sm64.jp.f3dex2e.exe". @@ -91,7 +97,7 @@ Open Notepad. Paste in the following text: `start sm64.us.f3dex2e.exe --sm64ap_f Go to File > Save As... -Navigate to the folder you selected for your SM64 build when you followed the Build guide for SM64PCLauncher earlier. Once there, navigate further into `build` and then `us_pc`. This folder should be the same folder that `sm64.us.f3dex2e.exe` resides in. +Navigate to the folder you selected for your SM64 build when you followed the Build guide for SM64AP-Launcher earlier. Once there, navigate further into `build` and then `us_pc`. This folder should be the same folder that `sm64.us.f3dex2e.exe` resides in. Make the file name `"offline.bat"` . THE QUOTE MARKS ARE IMPORTANT! Otherwise, it will create a text file instead ("offline.bat.txt"), which won't work as a batch file. @@ -120,8 +126,8 @@ To use this batch file, double-click it. A window will open. Type the five-digi - The port number is provided on the room page. The game host should share this page with all players. - The slot name is whatever you typed in the "Name" field when creating a config file. All slot names are visible on the room page. -Once you provide those two bits of information, the game will open. If the info is correct, when the game starts, you will see "Connected to Archipelago" on the bottom of your screen, and you will be able to enter the castle. -- If you don't see this text and crash upon entering the castle, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail. +Once you provide those two bits of information, the game will open. +- If the game only says `Connecting`, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail. ### Addendum - Deleting old saves @@ -170,6 +176,5 @@ Should the problem still be there after about a minute or two, just save and res ### How do I update the Game to a new Build? +When using the Launcher follow the normal build steps, but when choosing a folder name use the same as before. The launcher will recognize this, and offer to replace it. When manually compiling just pull in changes and run `make` again. Sometimes it helps to run `make clean` before. - -When using the Launcher follow the normal build steps, but when choosing a folder name use the same as before. Then continue as normal. From f840ed3a9416eed6e1ace1cf617021974e96bee8 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:17:09 -0500 Subject: [PATCH 060/104] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Fix=20trainer=20r?= =?UTF-8?q?egions=20(#2474)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix Mt Moon B2F trainer regions * Fix Trainer Party regions --- worlds/pokemon_rb/locations.py | 91 +++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index 4f1b55a00dd7..3fff3b88c1ea 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -502,8 +502,8 @@ def __init__(self, flag): LocationData("Mt Moon 1F", "Lass 2", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_2_ITEM"], EventFlag(134), inclusion=trainersanity), LocationData("Mt Moon 1F", "Youngster", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_1_ITEM"], EventFlag(135), inclusion=trainersanity), LocationData("Mt Moon 1F", "Hiker", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_0_ITEM"], EventFlag(136), inclusion=trainersanity), - LocationData("Mt Moon B2F-NE", "Rocket 1", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_1_ITEM"], EventFlag(127), inclusion=trainersanity), - LocationData("Mt Moon B2F-C", "Rocket 2", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_2_ITEM"], EventFlag(126), inclusion=trainersanity), + LocationData("Mt Moon B2F-C", "Rocket 1", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_1_ITEM"], EventFlag(127), inclusion=trainersanity), + LocationData("Mt Moon B2F-NE", "Rocket 2", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_2_ITEM"], EventFlag(126), inclusion=trainersanity), LocationData("Mt Moon B2F", "Rocket 3", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_3_ITEM"], EventFlag(125), inclusion=trainersanity), LocationData("Mt Moon B2F", "Rocket 4", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_0_ITEM"], EventFlag(128), inclusion=trainersanity), LocationData("Viridian Forest", "Bug Catcher 1", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM"], EventFlag(139), inclusion=trainersanity), @@ -2310,43 +2310,50 @@ def __init__(self, flag): 'Cerulean Gym': [{'level': 19, 'party': ['Goldeen'], 'party_address': 'Trainer_Party_Cerulean_Gym_JrTrainerF_A'}, {'level': 16, 'party': ['Horsea', 'Shellder'], 'party_address': 'Trainer_Party_Cerulean_Gym_Swimmer_A'}, - {'level': [18, 21], 'party': ['Staryu', 'Starmie'], 'party_address': 'Trainer_Party_Misty_A'},], 'Route 10-N': [ ### - {'level': 20, 'party': ['Pikachu', 'Clefairy'], 'party_address': 'Trainer_Party_Route_10_JrTrainerF_A'}, + {'level': [18, 21], 'party': ['Staryu', 'Starmie'], 'party_address': 'Trainer_Party_Misty_A'},], + 'Route 10-N': [{'level': 20, 'party': ['Pikachu', 'Clefairy'], 'party_address': 'Trainer_Party_Route_10_JrTrainerF_A'}], + 'Route 10-C': [ + {'level': 30, 'party': ['Rhyhorn', 'Lickitung'], 'party_address': 'Trainer_Party_Route_10_Pokemaniac_A'}], + 'Route 10-S': [ {'level': 21, 'party': ['Pidgey', 'Pidgeotto'], 'party_address': 'Trainer_Party_Route_10_JrTrainerF_B'}, - {'level': 30, 'party': ['Rhyhorn', 'Lickitung'], 'party_address': 'Trainer_Party_Route_10_Pokemaniac_A'}, {'level': 20, 'party': ['Cubone', 'Slowpoke'], 'party_address': 'Trainer_Party_Route_10_Pokemaniac_B'}, {'level': 21, 'party': ['Geodude', 'Onix'], 'party_address': 'Trainer_Party_Route_10_Hiker_A'}, {'level': 19, 'party': ['Onix', 'Graveler'], 'party_address': 'Trainer_Party_Route_10_Hiker_B'}], - 'Rock Tunnel B1F-E': [{'level': 21, 'party': ['Jigglypuff', 'Pidgey', 'Meowth'], ### + 'Rock Tunnel B1F-W': [{'level': 21, 'party': ['Jigglypuff', 'Pidgey', 'Meowth'], 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_JrTrainerF_A'}, - {'level': 22, 'party': ['Oddish', 'Bulbasaur'], - 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_JrTrainerF_B'}, {'level': 20, 'party': ['Slowpoke', 'Slowpoke', 'Slowpoke'], 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_A'}, - {'level': 22, 'party': ['Charmander', 'Cubone'], - 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_B'}, - {'level': 25, 'party': ['Slowpoke'], - 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_C'}, {'level': 21, 'party': ['Geodude', 'Geodude', 'Graveler'], - 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_A'}, - {'level': 25, 'party': ['Geodude'], 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_B'}, - {'level': 20, 'party': ['Machop', 'Onix'], - 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_D'}], 'Route 13': [ + 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_A'},], + 'Rock Tunnel B1F-E': [ + {'level': 22, 'party': ['Oddish', 'Bulbasaur'], + 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_JrTrainerF_B'}, + {'level': 22, 'party': ['Charmander', 'Cubone'], + 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_B'}, + {'level': 25, 'party': ['Slowpoke'], + 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_C'}, + {'level': 25, 'party': ['Geodude'], 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_B'}, + {'level': 20, 'party': ['Machop', 'Onix'], + 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_D'}], + 'Route 13-E': [ + {'level': 28, 'party': ['Goldeen', 'Poliwag', 'Horsea'], + 'party_address': 'Trainer_Party_Route_13_JrTrainerF_D'}, + {'level': 29, 'party': ['Pidgey', 'Pidgeotto'], 'party_address': 'Trainer_Party_Route_13_BirdKeeper_A'}, {'level': 24, 'party': ['Pidgey', 'Meowth', 'Rattata', 'Pikachu', 'Meowth'], 'party_address': 'Trainer_Party_Route_13_JrTrainerF_A'}, + ], + 'Route 13': [ {'level': 30, 'party': ['Poliwag', 'Poliwag'], 'party_address': 'Trainer_Party_Route_13_JrTrainerF_B'}, {'level': 27, 'party': ['Pidgey', 'Meowth', 'Pidgey', 'Pidgeotto'], 'party_address': 'Trainer_Party_Route_13_JrTrainerF_C'}, - {'level': 28, 'party': ['Goldeen', 'Poliwag', 'Horsea'], - 'party_address': 'Trainer_Party_Route_13_JrTrainerF_D'}, {'level': 28, 'party': ['Koffing', 'Koffing', 'Koffing'], 'party_address': 'Trainer_Party_Route_13_Biker_A'}, {'level': 27, 'party': ['Rattata', 'Pikachu', 'Rattata'], 'party_address': 'Trainer_Party_Route_13_Beauty_A'}, {'level': 29, 'party': ['Clefairy', 'Meowth'], 'party_address': 'Trainer_Party_Route_13_Beauty_B'}, - {'level': 29, 'party': ['Pidgey', 'Pidgeotto'], 'party_address': 'Trainer_Party_Route_13_BirdKeeper_A'}, {'level': 25, 'party': ['Spearow', 'Pidgey', 'Pidgey', 'Spearow', 'Spearow'], 'party_address': 'Trainer_Party_Route_13_BirdKeeper_B'}, {'level': 26, 'party': ['Pidgey', 'Pidgeotto', 'Spearow', 'Fearow'], 'party_address': 'Trainer_Party_Route_13_BirdKeeper_C'}], + 'Route 20-E': [ {'level': 31, 'party': ['Shellder', 'Cloyster'], 'party_address': 'Trainer_Party_Route_20_Swimmer_A'}, {'level': 28, 'party': ['Horsea', 'Horsea', 'Seadra', 'Horsea'], @@ -2354,9 +2361,9 @@ def __init__(self, flag): 'party_address': 'Trainer_Party_Route_20_Swimmer_C'}, {'level': 30, 'party': ['Seadra', 'Horsea', 'Seadra'], - 'party_address': 'Trainer_Party_Route_20_Beauty_E'}], + 'party_address': 'Trainer_Party_Route_20_Beauty_E'}, + {'level': 35, 'party': ['Seaking'], 'party_address': 'Trainer_Party_Route_20_Beauty_A'},], 'Route 20-W': [ - {'level': 35, 'party': ['Seaking'], 'party_address': 'Trainer_Party_Route_20_Beauty_A'}, {'level': 31, 'party': ['Goldeen', 'Seaking'], 'party_address': 'Trainer_Party_Route_20_JrTrainerF_A'}, {'level': 30, 'party': ['Tentacool', 'Horsea', 'Seel'], 'party_address': 'Trainer_Party_Route_20_JrTrainerF_C'}, @@ -2374,16 +2381,19 @@ def __init__(self, flag): {'level': 20, 'party': ['Meowth', 'Oddish', 'Pidgey'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_JrTrainerF_B'}, {'level': 19, 'party': ['Pidgey', 'Rattata', 'Rattata', 'Bellsprout'], - 'party_address': 'Trainer_Party_Rock_Tunnel_1F_JrTrainerF_C'}, - {'level': 23, 'party': ['Cubone', 'Slowpoke'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Pokemaniac_A'}, + 'party_address': 'Trainer_Party_Rock_Tunnel_1F_JrTrainerF_C'}], + 'Rock Tunnel 1F-NE': [ + {'level': 23, 'party': ['Cubone', 'Slowpoke'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Pokemaniac_A'}], + 'Rock Tunnel 1F-NW': [ {'level': 19, 'party': ['Geodude', 'Machop', 'Geodude', 'Geodude'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Hiker_A'}, {'level': 20, 'party': ['Onix', 'Onix', 'Geodude'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Hiker_B'}, {'level': 21, 'party': ['Geodude', 'Graveler'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Hiker_C'}], + 'Route 15-N': [ + {'level': 33, 'party': ['Clefairy'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_C'}], 'Route 15': [ {'level': 28, 'party': ['Gloom', 'Oddish', 'Oddish'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_A'}, {'level': 29, 'party': ['Pikachu', 'Raichu'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_B'}, - {'level': 33, 'party': ['Clefairy'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_C'}, {'level': 29, 'party': ['Bellsprout', 'Oddish', 'Tangela'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_D'}, {'level': 25, 'party': ['Koffing', 'Koffing', 'Weezing', 'Koffing', 'Grimer'], @@ -2394,15 +2404,16 @@ def __init__(self, flag): {'level': 26, 'party': ['Pidgeotto', 'Farfetchd', 'Doduo', 'Pidgey'], 'party_address': 'Trainer_Party_Route_15_BirdKeeper_A'}, {'level': 28, 'party': ['Dodrio', 'Doduo', 'Doduo'], 'party_address': 'Trainer_Party_Route_15_BirdKeeper_B'}], - 'Victory Road 2F-C': [{'level': 40, 'party': ['Charmeleon', 'Lapras', 'Lickitung'], ### - 'party_address': 'Trainer_Party_Victory_Road_2F_Pokemaniac_A'}, - {'level': 41, 'party': ['Drowzee', 'Hypno', 'Kadabra', 'Kadabra'], - 'party_address': 'Trainer_Party_Victory_Road_2F_Juggler_A'}, - {'level': 48, 'party': ['Mr Mime'], 'party_address': 'Trainer_Party_Victory_Road_2F_Juggler_C'}, - {'level': 44, 'party': ['Persian', 'Golduck'], - 'party_address': 'Trainer_Party_Victory_Road_2F_Tamer_A'}, - {'level': 43, 'party': ['Machoke', 'Machop', 'Machoke'], - 'party_address': 'Trainer_Party_Victory_Road_2F_Blackbelt_A'}], 'Mt Moon B2F': [ + 'Victory Road 2F-NW': [{'level': 40, 'party': ['Charmeleon', 'Lapras', 'Lickitung'], + 'party_address': 'Trainer_Party_Victory_Road_2F_Pokemaniac_A'}], + 'Victory Road 2F-C': [ + {'level': 41, 'party': ['Drowzee', 'Hypno', 'Kadabra', 'Kadabra'], + 'party_address': 'Trainer_Party_Victory_Road_2F_Juggler_A'}, + {'level': 48, 'party': ['Mr Mime'], 'party_address': 'Trainer_Party_Victory_Road_2F_Juggler_C'}, + {'level': 44, 'party': ['Persian', 'Golduck'], + 'party_address': 'Trainer_Party_Victory_Road_2F_Tamer_A'}, + {'level': 43, 'party': ['Machoke', 'Machop', 'Machoke'], + 'party_address': 'Trainer_Party_Victory_Road_2F_Blackbelt_A'}], 'Mt Moon B2F': [ {'level': 12, 'party': ['Grimer', 'Voltorb', 'Koffing'], 'party_address': 'Trainer_Party_Mt_Moon_B2F_SuperNerd_A'}, {'level': 13, 'party': ['Rattata', 'Zubat'], 'party_address': 'Trainer_Party_Mt_Moon_B2F_Rocket_A'}, @@ -2585,7 +2596,7 @@ def __init__(self, flag): ['Pidgeotto', 'Abra', 'Rattata', 'Charmander']], 'party_address': ['Trainer_Party_Cerulean_City_Green1_A', 'Trainer_Party_Cerulean_City_Green1_B', 'Trainer_Party_Cerulean_City_Green1_C']}, {'level': 17, 'party': ['Machop', 'Drowzee'], - 'party_address': 'Trainer_Party_Cerulean_City_Rocket_A'}], 'Pokemon Mansion 1F': [ + 'party_address': 'Trainer_Party_Cerulean_City_Rocket_A'}], 'Pokemon Mansion 1F-SE': [ {'level': 29, 'party': ['Electrode', 'Weezing'], 'party_address': 'Trainer_Party_Mansion_1F_Scientist_A'}], 'Silph Co 2F-SW': [{'level': 26, 'party': ['Grimer', 'Weezing', 'Koffing', 'Weezing'], 'party_address': 'Trainer_Party_Silph_Co_2F_Scientist_A'}], @@ -2595,7 +2606,7 @@ def __init__(self, flag): {'level': 25, 'party': ['Golbat', 'Zubat', 'Zubat', 'Raticate', 'Zubat'], 'party_address': 'Trainer_Party_Silph_Co_2F_Rocket_B'}], 'Silph Co 3F-W': [ {'level': 29, 'party': ['Electrode', 'Weezing'], 'party_address': 'Trainer_Party_Silph_Co_3F_Scientist_A'}], - 'Silph Co 3F': [ {'level': 28, 'party': ['Raticate', 'Hypno', 'Raticate'], + 'Silph Co 3F': [{'level': 28, 'party': ['Raticate', 'Hypno', 'Raticate'], 'party_address': 'Trainer_Party_Silph_Co_3F_Rocket_A'}], 'Silph Co 4F-N': [{'level': 33, 'party': ['Electrode'], 'party_address': 'Trainer_Party_Silph_Co_4F_Scientist_A'}], 'Silph Co 4F': [{'level': 29, 'party': ['Machop', 'Drowzee'], @@ -2670,15 +2681,17 @@ def __init__(self, flag): {'level': 26, 'party': ['Koffing', 'Drowzee'], 'party_address': 'Trainer_Party_Pokemon_Tower_7F_Rocket_B'}, {'level': 23, 'party': ['Zubat', 'Rattata', 'Raticate', 'Zubat'], - 'party_address': 'Trainer_Party_Pokemon_Tower_7F_Rocket_C'}], 'Victory Road 3F': [ - {'level': 43, 'party': ['Exeggutor', 'Cloyster', 'Arcanine'], + 'party_address': 'Trainer_Party_Pokemon_Tower_7F_Rocket_C'}], + 'Victory Road 3F': [{'level': 43, 'party': ['Exeggutor', 'Cloyster', 'Arcanine'], 'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerM_A'}, + {'level': 43, 'party': ['Parasect', 'Dewgong', 'Chansey'], + 'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerF_B'}], + 'Victory Road 3F-S': [ {'level': 43, 'party': ['Kingler', 'Tentacruel', 'Blastoise'], 'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerM_B'}, {'level': 43, 'party': ['Bellsprout', 'Weepinbell', 'Victreebel'], 'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerF_A'}, - {'level': 43, 'party': ['Parasect', 'Dewgong', 'Chansey'], - 'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerF_B'}], 'Victory Road 1F': [ +], 'Victory Road 1F': [ {'level': 42, 'party': ['Ivysaur', 'Wartortle', 'Charmeleon', 'Charizard'], 'party_address': 'Trainer_Party_Victory_Road_1F_CooltrainerM_A'}, {'level': 44, 'party': ['Persian', 'Ninetales'], From 7efec647458bf33a792cdc47d7c80f0723cebbc9 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 23 Nov 2023 11:51:53 -0800 Subject: [PATCH 061/104] BizHawkClient: Restore use of ConnectorErrors (#2480) --- worlds/_bizhawk/__init__.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py index c3314e18dcc0..cddfde4ff37f 100644 --- a/worlds/_bizhawk/__init__.py +++ b/worlds/_bizhawk/__init__.py @@ -9,6 +9,7 @@ import base64 import enum import json +import sys import typing @@ -125,7 +126,20 @@ async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[s """Sends a list of requests to the BizHawk connector and returns their responses. It's likely you want to use the wrapper functions instead of this.""" - return json.loads(await ctx._send_message(json.dumps(req_list))) + responses = json.loads(await ctx._send_message(json.dumps(req_list))) + errors: typing.List[ConnectorError] = [] + + for response in responses: + if response["type"] == "ERROR": + errors.append(ConnectorError(response["err"])) + + if errors: + if sys.version_info >= (3, 11, 0): + raise ExceptionGroup("Connector script returned errors", errors) # noqa + else: + raise errors[0] + + return responses async def ping(ctx: BizHawkContext) -> None: From 28ed78660997a8462da6ce44cfb3b1223ae267d9 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 23 Nov 2023 21:36:05 +0100 Subject: [PATCH 062/104] LttP: fix Ganons Tower - Compass Room - Bottom Left being listed twice in Ganons Tower location group and add missing Ganons Tower - Compass Room - Bottom Right (#2490) --- worlds/alttp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 2cae70e0ea49..32667249f225 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -195,7 +195,7 @@ class ALTTPWorld(World): "Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganons Tower - Hope Room - Left", "Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room", "Ganons Tower - Compass Room - Top Left", "Ganons Tower - Compass Room - Top Right", - "Ganons Tower - Compass Room - Bottom Left", "Ganons Tower - Compass Room - Bottom Left", + "Ganons Tower - Compass Room - Bottom Left", "Ganons Tower - Compass Room - Bottom Right", "Ganons Tower - DMs Room - Top Left", "Ganons Tower - DMs Room - Top Right", "Ganons Tower - DMs Room - Bottom Left", "Ganons Tower - DMs Room - Bottom Right", "Ganons Tower - Map Chest", "Ganons Tower - Firesnake Room", From cb6467cfe61caabb3c8f603245a07b92b5b5c6e6 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 23 Nov 2023 21:36:20 +0100 Subject: [PATCH 063/104] Core: update modules, move orjson to core (#2489) --- requirements.txt | 7 ++++--- worlds/factorio/requirements.txt | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7f9cddc2879c..7d93928bb5fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,13 @@ colorama>=0.4.5 websockets>=11.0.3 PyYAML>=6.0.1 -jellyfish>=1.0.1 +jellyfish>=1.0.3 jinja2>=3.1.2 schema>=0.7.5 kivy>=2.2.0 bsdiff4>=1.2.4 -platformdirs>=3.9.1 -certifi>=2023.7.22 +platformdirs>=4.0.0 +certifi>=2023.11.17 cython>=3.0.5 cymem>=2.0.8 +orjson>=3.9.10 \ No newline at end of file diff --git a/worlds/factorio/requirements.txt b/worlds/factorio/requirements.txt index 8fb74e933045..c45fb771da6a 100644 --- a/worlds/factorio/requirements.txt +++ b/worlds/factorio/requirements.txt @@ -1,2 +1 @@ factorio-rcon-py>=2.0.1 -orjson>=3.9.7 From 9312ad9bfe6e364cc05e011301fbde0a6ed4e566 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:02:20 -0500 Subject: [PATCH 064/104] KH2: Fix grammar to clarify which locations can have a bounty (#2488) --- worlds/kh2/docs/en_Kingdom Hearts 2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/kh2/docs/en_Kingdom Hearts 2.md b/worlds/kh2/docs/en_Kingdom Hearts 2.md index 365ed37cb69b..a07f29be54b9 100644 --- a/worlds/kh2/docs/en_Kingdom Hearts 2.md +++ b/worlds/kh2/docs/en_Kingdom Hearts 2.md @@ -77,7 +77,7 @@ The list of possible locations that can contain a bounty: - Lingering Will - Starry Hill - Transport to Remembrance -- Each of the Goddess of Fate and Paradox Cups +- Godess of Fate cup and Hades Paradox cup

Quality of life:

From 5d9896773d34f2beba0bd1e9ef83f7c653a63efe Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Thu, 23 Nov 2023 16:03:56 -0600 Subject: [PATCH 065/104] Generate: Add `--skip_output` flag to bypass assertion and output stages. (#2416) --- Generate.py | 6 +++++- Main.py | 13 +++++++++---- WebHostLib/generate.py | 11 ++++++----- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/Generate.py b/Generate.py index 74244ec23102..e19a7a973f23 100644 --- a/Generate.py +++ b/Generate.py @@ -20,7 +20,7 @@ from BaseClasses import seeddigits, get_seed, PlandoOptions from Main import main as ERmain from settings import get_settings -from Utils import parse_yamls, version_tuple, __version__, tuplize_version, user_path +from Utils import parse_yamls, version_tuple, __version__, tuplize_version from worlds.alttp import Options as LttPOptions from worlds.alttp.EntranceRandomizer import parse_arguments from worlds.alttp.Text import TextTable @@ -53,6 +53,9 @@ def mystery_argparse(): help='List of options that can be set manually. Can be combined, for example "bosses, items"') parser.add_argument("--skip_prog_balancing", action="store_true", help="Skip progression balancing step during generation.") + parser.add_argument("--skip_output", action="store_true", + help="Skips generation assertion and output stages and skips multidata and spoiler output. " + "Intended for debugging and testing purposes.") args = parser.parse_args() if not os.path.isabs(args.weights_file_path): args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path) @@ -150,6 +153,7 @@ def main(args=None, callback=ERmain): erargs.outputname = seed_name erargs.outputpath = args.outputpath erargs.skip_prog_balancing = args.skip_prog_balancing + erargs.skip_output = args.skip_output settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None) diff --git a/Main.py b/Main.py index 568bf0208f78..b64650478bfe 100644 --- a/Main.py +++ b/Main.py @@ -13,8 +13,8 @@ from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items from Options import StartInventoryPool -from settings import get_settings from Utils import __version__, output_path, version_tuple +from settings import get_settings from worlds import AutoWorld from worlds.generic.Rules import exclusion_rules, locality_rules @@ -101,7 +101,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No del item_digits, location_digits, item_count, location_count - AutoWorld.call_stage(world, "assert_generate") + # This assertion method should not be necessary to run if we are not outputting any multidata. + if not args.skip_output: + AutoWorld.call_stage(world, "assert_generate") AutoWorld.call_all(world, "generate_early") @@ -287,11 +289,14 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ else: logger.info("Progression balancing skipped.") - logger.info(f'Beginning output...') - # we're about to output using multithreading, so we're removing the global random state to prevent accidental use world.random.passthrough = False + if args.skip_output: + logger.info('Done. Skipped output/spoiler generation. Total Time: %s', time.perf_counter() - start) + return world + + logger.info(f'Beginning output...') outfilebase = 'AP_' + world.seed_name output = tempfile.TemporaryDirectory() diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index ddcc5ffb6c7b..ee1ce591ee84 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -1,18 +1,18 @@ +import concurrent.futures import json import os import pickle import random import tempfile import zipfile -import concurrent.futures from collections import Counter -from typing import Dict, Optional, Any, Union, List +from typing import Any, Dict, List, Optional, Union -from flask import request, flash, redirect, url_for, session, render_template +from flask import flash, redirect, render_template, request, session, url_for from pony.orm import commit, db_session -from BaseClasses import seeddigits, get_seed -from Generate import handle_name, PlandoOptions +from BaseClasses import get_seed, seeddigits +from Generate import PlandoOptions, handle_name from Main import main as ERmain from Utils import __version__ from WebHostLib import app @@ -131,6 +131,7 @@ def task(): erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options", {"bosses", "items", "connections", "texts"})) erargs.skip_prog_balancing = False + erargs.skip_output = False name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): From 844481a0027f7e8770be42d0236f59d008e8b8af Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 24 Nov 2023 00:35:37 +0100 Subject: [PATCH 066/104] Core: remove duplicate state.item_count (#2463) --- BaseClasses.py | 10 +++++-- docs/world api.md | 4 +-- worlds/alttp/StateHelpers.py | 8 +++--- worlds/checksfinder/Rules.py | 53 +++++++++++++++++------------------- worlds/kh2/logic.py | 2 +- worlds/noita/Rules.py | 4 +-- worlds/overcooked2/Logic.py | 4 +-- worlds/rogue_legacy/Rules.py | 4 +-- worlds/soe/Logic.py | 4 +-- worlds/spire/Rules.py | 4 +-- worlds/zillion/logic.py | 2 +- 11 files changed, 50 insertions(+), 49 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b25d998311a1..7965eb8b0d0d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -714,6 +714,7 @@ def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[ assert isinstance(event.item, Item), "tried to collect Event with no Item" self.collect(event.item, True, event) + # item name related def has(self, item: str, player: int, count: int = 1) -> bool: return self.prog_items[player][item] >= count @@ -728,6 +729,11 @@ def has_any(self, items: Iterable[str], player: int) -> bool: def count(self, item: str, player: int) -> int: return self.prog_items[player][item] + def item_count(self, item: str, player: int) -> int: + Utils.deprecate("Use count instead.") + return self.count(item, player) + + # item name group related def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool: found: int = 0 player_prog_items = self.prog_items[player] @@ -744,9 +750,7 @@ def count_group(self, item_name_group: str, player: int) -> int: found += player_prog_items[item_name] return found - def item_count(self, item: str, player: int) -> int: - return self.prog_items[player][item] - + # Item related def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool: if location: self.locations_checked.add(location) diff --git a/docs/world api.md b/docs/world api.md index 4008c9c4dddf..71710ac2932e 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -691,7 +691,7 @@ def generate_basic(self) -> None: ### Setting Rules ```python -from worlds.generic.Rules import add_rule, set_rule, forbid_item +from worlds.generic.Rules import add_rule, set_rule, forbid_item, add_item_rule from .items import get_item_type @@ -718,7 +718,7 @@ def set_rules(self) -> None: # require one item from an item group add_rule(self.multiworld.get_location("Chest3", self.player), lambda state: state.has_group("weapons", self.player)) - # state also has .item_count() for items, .has_any() and .has_all() for sets + # state also has .count() for items, .has_any() and .has_all() for multiple # and .count_group() for groups # set_rule is likely to be a bit faster than add_rule diff --git a/worlds/alttp/StateHelpers.py b/worlds/alttp/StateHelpers.py index 95e31e5ba328..38ce00ef4537 100644 --- a/worlds/alttp/StateHelpers.py +++ b/worlds/alttp/StateHelpers.py @@ -31,7 +31,7 @@ def can_shoot_arrows(state: CollectionState, player: int) -> bool: def has_triforce_pieces(state: CollectionState, player: int) -> bool: count = state.multiworld.treasure_hunt_count[player] - return state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= count + return state.count('Triforce Piece', player) + state.count('Power Star', player) >= count def has_crystals(state: CollectionState, count: int, player: int) -> bool: @@ -60,9 +60,9 @@ def has_hearts(state: CollectionState, player: int, count: int) -> int: def heart_count(state: CollectionState, player: int) -> int: # Warning: This only considers items that are marked as advancement items diff = state.multiworld.difficulty_requirements[player] - return min(state.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \ - + state.item_count('Sanctuary Heart Container', player) \ - + min(state.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \ + return min(state.count('Boss Heart Container', player), diff.boss_heart_container_limit) \ + + state.count('Sanctuary Heart Container', player) \ + + min(state.count('Piece of Heart', player), diff.heart_piece_limit) // 4 \ + 3 # starting hearts diff --git a/worlds/checksfinder/Rules.py b/worlds/checksfinder/Rules.py index 4e12668798dc..38d7d77ad393 100644 --- a/worlds/checksfinder/Rules.py +++ b/worlds/checksfinder/Rules.py @@ -1,37 +1,34 @@ -from ..generic.Rules import set_rule, add_rule -from BaseClasses import MultiWorld -from ..AutoWorld import LogicMixin +from ..generic.Rules import set_rule +from BaseClasses import MultiWorld, CollectionState -class ChecksFinderLogic(LogicMixin): - - def _has_total(self, player: int, total: int): - return (self.item_count('Map Width', player)+self.item_count('Map Height', player)+ - self.item_count('Map Bombs', player)) >= total +def _has_total(state: CollectionState, player: int, total: int): + return (state.count('Map Width', player) + state.count('Map Height', player) + + state.count('Map Bombs', player)) >= total # Sets rules on entrances and advancements that are always applied def set_rules(world: MultiWorld, player: int): - set_rule(world.get_location(("Tile 6"), player), lambda state: state._has_total(player, 1)) - set_rule(world.get_location(("Tile 7"), player), lambda state: state._has_total(player, 2)) - set_rule(world.get_location(("Tile 8"), player), lambda state: state._has_total(player, 3)) - set_rule(world.get_location(("Tile 9"), player), lambda state: state._has_total(player, 4)) - set_rule(world.get_location(("Tile 10"), player), lambda state: state._has_total(player, 5)) - set_rule(world.get_location(("Tile 11"), player), lambda state: state._has_total(player, 6)) - set_rule(world.get_location(("Tile 12"), player), lambda state: state._has_total(player, 7)) - set_rule(world.get_location(("Tile 13"), player), lambda state: state._has_total(player, 8)) - set_rule(world.get_location(("Tile 14"), player), lambda state: state._has_total(player, 9)) - set_rule(world.get_location(("Tile 15"), player), lambda state: state._has_total(player, 10)) - set_rule(world.get_location(("Tile 16"), player), lambda state: state._has_total(player, 11)) - set_rule(world.get_location(("Tile 17"), player), lambda state: state._has_total(player, 12)) - set_rule(world.get_location(("Tile 18"), player), lambda state: state._has_total(player, 13)) - set_rule(world.get_location(("Tile 19"), player), lambda state: state._has_total(player, 14)) - set_rule(world.get_location(("Tile 20"), player), lambda state: state._has_total(player, 15)) - set_rule(world.get_location(("Tile 21"), player), lambda state: state._has_total(player, 16)) - set_rule(world.get_location(("Tile 22"), player), lambda state: state._has_total(player, 17)) - set_rule(world.get_location(("Tile 23"), player), lambda state: state._has_total(player, 18)) - set_rule(world.get_location(("Tile 24"), player), lambda state: state._has_total(player, 19)) - set_rule(world.get_location(("Tile 25"), player), lambda state: state._has_total(player, 20)) + set_rule(world.get_location("Tile 6", player), lambda state: _has_total(state, player, 1)) + set_rule(world.get_location("Tile 7", player), lambda state: _has_total(state, player, 2)) + set_rule(world.get_location("Tile 8", player), lambda state: _has_total(state, player, 3)) + set_rule(world.get_location("Tile 9", player), lambda state: _has_total(state, player, 4)) + set_rule(world.get_location("Tile 10", player), lambda state: _has_total(state, player, 5)) + set_rule(world.get_location("Tile 11", player), lambda state: _has_total(state, player, 6)) + set_rule(world.get_location("Tile 12", player), lambda state: _has_total(state, player, 7)) + set_rule(world.get_location("Tile 13", player), lambda state: _has_total(state, player, 8)) + set_rule(world.get_location("Tile 14", player), lambda state: _has_total(state, player, 9)) + set_rule(world.get_location("Tile 15", player), lambda state: _has_total(state, player, 10)) + set_rule(world.get_location("Tile 16", player), lambda state: _has_total(state, player, 11)) + set_rule(world.get_location("Tile 17", player), lambda state: _has_total(state, player, 12)) + set_rule(world.get_location("Tile 18", player), lambda state: _has_total(state, player, 13)) + set_rule(world.get_location("Tile 19", player), lambda state: _has_total(state, player, 14)) + set_rule(world.get_location("Tile 20", player), lambda state: _has_total(state, player, 15)) + set_rule(world.get_location("Tile 21", player), lambda state: _has_total(state, player, 16)) + set_rule(world.get_location("Tile 22", player), lambda state: _has_total(state, player, 17)) + set_rule(world.get_location("Tile 23", player), lambda state: _has_total(state, player, 18)) + set_rule(world.get_location("Tile 24", player), lambda state: _has_total(state, player, 19)) + set_rule(world.get_location("Tile 25", player), lambda state: _has_total(state, player, 20)) # Sets rules on completion condition diff --git a/worlds/kh2/logic.py b/worlds/kh2/logic.py index 1c5883f5ce8a..10af4144a7fb 100644 --- a/worlds/kh2/logic.py +++ b/worlds/kh2/logic.py @@ -63,7 +63,7 @@ def kh_visit_locking_amount(self, player, amount): ItemName.MembershipCard, ItemName.IceCream, ItemName.WaytotheDawn, ItemName.IdentityDisk, ItemName.NamineSketches}: - visit += self.item_count(item, player) + visit += self.count(item, player) return visit >= amount def kh_three_proof_unlocked(self, player): diff --git a/worlds/noita/Rules.py b/worlds/noita/Rules.py index 808dd3a200a6..8190b80dc710 100644 --- a/worlds/noita/Rules.py +++ b/worlds/noita/Rules.py @@ -57,11 +57,11 @@ class EntranceLock(NamedTuple): def has_perk_count(state: CollectionState, player: int, amount: int) -> bool: - return sum(state.item_count(perk, player) for perk in perk_list) >= amount + return sum(state.count(perk, player) for perk in perk_list) >= amount def has_orb_count(state: CollectionState, player: int, amount: int) -> bool: - return state.item_count("Orb", player) >= amount + return state.count("Orb", player) >= amount def forbid_items_at_location(multiworld: MultiWorld, location_name: str, items: Set[str], player: int): diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py index d8468cb59af1..20111aa01d66 100644 --- a/worlds/overcooked2/Logic.py +++ b/worlds/overcooked2/Logic.py @@ -18,7 +18,7 @@ def has_requirements_for_level_access(state: CollectionState, level_name: str, p return state.has(level_name, player) # Must have enough stars to purchase level - star_count = state.item_count("Star", player) + state.item_count("Bonus Star", player) + star_count = state.count("Star", player) + state.count("Bonus Star", player) if star_count < required_star_count: return False @@ -64,7 +64,7 @@ def meets_requirements(state: CollectionState, name: str, stars: int, player: in total: float = 0.0 for (item_name, weight) in additive_reqs: - for _ in range(0, state.item_count(item_name, player)): + for _ in range(0, state.count(item_name, player)): total += weight if total >= 0.99: # be nice to rounding errors :) return True diff --git a/worlds/rogue_legacy/Rules.py b/worlds/rogue_legacy/Rules.py index 90f6cc08b1fb..2fac8d561399 100644 --- a/worlds/rogue_legacy/Rules.py +++ b/worlds/rogue_legacy/Rules.py @@ -7,8 +7,8 @@ def get_upgrade_total(multiworld: MultiWorld, player: int) -> int: def get_upgrade_count(state: CollectionState, player: int) -> int: - return state.item_count("Health Up", player) + state.item_count("Mana Up", player) + \ - state.item_count("Attack Up", player) + state.item_count("Magic Damage Up", player) + return state.count("Health Up", player) + state.count("Mana Up", player) + \ + state.count("Attack Up", player) + state.count("Magic Damage Up", player) def has_vendors(state: CollectionState, player: int) -> bool: diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py index e464b7fd3b8e..fe5339c955b9 100644 --- a/worlds/soe/Logic.py +++ b/worlds/soe/Logic.py @@ -18,7 +18,7 @@ class LogicProtocol(Protocol): def has(self, name: str, player: int) -> bool: ... - def item_count(self, name: str, player: int) -> int: ... + def count(self, name: str, player: int) -> int: ... def soe_has(self, progress: int, world: MultiWorld, player: int, count: int) -> bool: ... def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int) -> int: ... @@ -35,7 +35,7 @@ def _soe_count(self: LogicProtocol, progress: int, world: MultiWorld, player: in for pvd in item.provides: if pvd[1] == progress: if self.has(item.name, player): - n += self.item_count(item.name, player) * pvd[0] + n += self.count(item.name, player) * pvd[0] if n >= max_count > 0: return n for rule in rules: diff --git a/worlds/spire/Rules.py b/worlds/spire/Rules.py index 7c8c1c0f3d86..3c6f09b34dce 100644 --- a/worlds/spire/Rules.py +++ b/worlds/spire/Rules.py @@ -5,11 +5,11 @@ class SpireLogic(LogicMixin): def _spire_has_relics(self, player: int, amount: int) -> bool: - count: int = self.item_count("Relic", player) + self.item_count("Boss Relic", player) + count: int = self.count("Relic", player) + self.count("Boss Relic", player) return count >= amount def _spire_has_cards(self, player: int, amount: int) -> bool: - count = self.item_count("Card Draw", player) + self.item_count("Rare Card Draw", player) + count = self.count("Card Draw", player) + self.count("Rare Card Draw", player) return count >= amount diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py index e99867c742aa..12f1875b4047 100644 --- a/worlds/zillion/logic.py +++ b/worlds/zillion/logic.py @@ -38,7 +38,7 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]: ((item_name, count), (item_name, count), ...) """ - return tuple((item_name, cs.item_count(item_name, p)) for item_name in item_name_to_id) + return tuple((item_name, cs.count(item_name, p)) for item_name in item_name_to_id) LogicCacheType = Dict[int, Tuple[_Counter[Tuple[str, int]], FrozenSet[Location]]] From 2f6b6838cd230c5b97b809ca8e420dd7d2657822 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 23 Nov 2023 17:38:57 -0600 Subject: [PATCH 067/104] The Messenger: more optimizations (#2451) More speed optimizations for The Messenger. Moves Figurines into their own region, so their complicated access rule only needs to be calculated once when doing a sweep. Removes a redundant loop for shop locations by just directly assigning the access rule in the class instead of retroactively. Reduces slot_data to only information that can't be derived, and removes some additional extraneous data. Removes some unused sets and lists. Removes a redundant event location, and increments the required_client_version to prevent clients that don't expect the new slot_data. Drops data version since it's going away soon anyways, to remove conflicts. --- worlds/messenger/__init__.py | 18 +++------- worlds/messenger/regions.py | 4 ++- worlds/messenger/rules.py | 52 ++++++++++++++--------------- worlds/messenger/subclasses.py | 14 +++----- worlds/messenger/test/test_logic.py | 1 - worlds/messenger/test/test_shop.py | 1 - 6 files changed, 38 insertions(+), 52 deletions(-) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 304b43cf5316..f12687361b70 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -62,8 +62,7 @@ class MessengerWorld(World): "Money Wrench", ], base_offset)} - data_version = 3 - required_client_version = (0, 4, 0) + required_client_version = (0, 4, 1) web = MessengerWeb() @@ -148,19 +147,12 @@ def set_rules(self) -> None: MessengerOOBRules(self).set_messenger_rules() def fill_slot_data(self) -> Dict[str, Any]: - shop_prices = {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()} - figure_prices = {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()} - return { - "deathlink": self.options.death_link.value, - "goal": self.options.goal.current_key, - "music_box": self.options.music_box.value, - "required_seals": self.required_seals, - "mega_shards": self.options.shuffle_shards.value, - "logic": self.options.logic_level.current_key, - "shop": shop_prices, - "figures": figure_prices, + "shop": {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()}, + "figures": {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}, "max_price": self.total_shards, + "required_seals": self.required_seals, + **self.options.as_dict("music_box", "death_link", "logic_level"), } def get_filler_item_name(self) -> str: diff --git a/worlds/messenger/regions.py b/worlds/messenger/regions.py index 3a6c95bff5a2..43de4dd1f6d0 100644 --- a/worlds/messenger/regions.py +++ b/worlds/messenger/regions.py @@ -4,6 +4,7 @@ "Menu": [], "Tower HQ": [], "The Shop": [], + "The Craftsman's Corner": [], "Tower of Time": [], "Ninja Village": ["Ninja Village - Candle", "Ninja Village - Astral Seed"], "Autumn Hills": ["Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills - Leaf Golem"], @@ -82,7 +83,8 @@ REGION_CONNECTIONS: Dict[str, Set[str]] = { "Menu": {"Tower HQ"}, "Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time", - "Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"}, + "Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop", + "The Craftsman's Corner", "Music Box"}, "Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"}, "Forlorn Temple": {"Catacombs", "Bamboo Creek"}, "Catacombs": {"Autumn Hills", "Bamboo Creek", "Dark Cave"}, diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index 876acd42c108..793de50afb70 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -1,26 +1,32 @@ -from typing import Callable, Dict, TYPE_CHECKING +from typing import Dict, TYPE_CHECKING from BaseClasses import CollectionState -from worlds.generic.Rules import add_rule, allow_self_locking_items +from worlds.generic.Rules import add_rule, allow_self_locking_items, CollectionRule from .constants import NOTES, PHOBEKINS from .options import MessengerAccessibility if TYPE_CHECKING: from . import MessengerWorld -else: - MessengerWorld = object class MessengerRules: player: int - world: MessengerWorld - region_rules: Dict[str, Callable[[CollectionState], bool]] - location_rules: Dict[str, Callable[[CollectionState], bool]] + world: "MessengerWorld" + region_rules: Dict[str, CollectionRule] + location_rules: Dict[str, CollectionRule] + maximum_price: int + required_seals: int - def __init__(self, world: MessengerWorld) -> None: + def __init__(self, world: "MessengerWorld") -> None: self.player = world.player self.world = world + # these locations are at the top of the shop tree, and the entire shop tree needs to be purchased + maximum_price = (world.multiworld.get_location("The Shop - Demon's Bane", self.player).cost + + world.multiworld.get_location("The Shop - Focused Power Sense", self.player).cost) + self.maximum_price = min(maximum_price, world.total_shards) + self.required_seals = max(1, world.required_seals) + self.region_rules = { "Ninja Village": self.has_wingsuit, "Autumn Hills": self.has_wingsuit, @@ -36,9 +42,9 @@ def __init__(self, world: MessengerWorld) -> None: "Forlorn Temple": lambda state: state.has_all({"Wingsuit", *PHOBEKINS}, self.player) and self.can_dboost(state), "Glacial Peak": self.has_vertical, "Elemental Skylands": lambda state: state.has("Magic Firefly", self.player) and self.has_wingsuit(state), - "Music Box": lambda state: (state.has_all(set(NOTES), self.player) - or state.has("Power Seal", self.player, max(1, self.world.required_seals))) - and self.has_dart(state), + "Music Box": lambda state: (state.has_all(NOTES, self.player) + or self.has_enough_seals(state)) and self.has_dart(state), + "The Craftsman's Corner": lambda state: state.has("Money Wrench", self.player) and self.can_shop(state), } self.location_rules = { @@ -110,7 +116,7 @@ def has_vertical(self, state: CollectionState) -> bool: return self.has_wingsuit(state) or self.has_dart(state) def has_enough_seals(self, state: CollectionState) -> bool: - return not self.world.required_seals or state.has("Power Seal", self.player, self.world.required_seals) + return state.has("Power Seal", self.player, self.required_seals) def can_destroy_projectiles(self, state: CollectionState) -> bool: return state.has("Strike of the Ninja", self.player) @@ -127,9 +133,7 @@ def true(self, state: CollectionState) -> bool: return True def can_shop(self, state: CollectionState) -> bool: - prices = self.world.shop_prices - most_expensive_loc = max(prices, key=prices.get) - return state.can_reach(f"The Shop - {most_expensive_loc}", "Location", self.player) + return state.has("Shards", self.player, self.maximum_price) def set_messenger_rules(self) -> None: multiworld = self.world.multiworld @@ -141,9 +145,6 @@ def set_messenger_rules(self) -> None: for loc in region.locations: if loc.name in self.location_rules: loc.access_rule = self.location_rules[loc.name] - if region.name == "The Shop": - for loc in region.locations: - loc.access_rule = loc.can_afford multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player) if multiworld.accessibility[self.player]: # not locations accessibility @@ -151,9 +152,9 @@ def set_messenger_rules(self) -> None: class MessengerHardRules(MessengerRules): - extra_rules: Dict[str, Callable[[CollectionState], bool]] + extra_rules: Dict[str, CollectionRule] - def __init__(self, world: MessengerWorld) -> None: + def __init__(self, world: "MessengerWorld") -> None: super().__init__(world) self.region_rules.update({ @@ -162,7 +163,7 @@ def __init__(self, world: MessengerWorld) -> None: "Catacombs": self.has_vertical, "Bamboo Creek": self.has_vertical, "Riviere Turquoise": self.true, - "Forlorn Temple": lambda state: self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player), + "Forlorn Temple": lambda state: self.has_vertical(state) and state.has_all(PHOBEKINS, self.player), "Searing Crags Upper": lambda state: self.can_destroy_projectiles(state) or self.has_windmill(state) or self.has_vertical(state), "Glacial Peak": lambda state: self.can_destroy_projectiles(state) or self.has_windmill(state) @@ -215,14 +216,15 @@ def set_messenger_rules(self) -> None: class MessengerOOBRules(MessengerRules): - def __init__(self, world: MessengerWorld) -> None: + def __init__(self, world: "MessengerWorld") -> None: self.world = world self.player = world.player + self.required_seals = max(1, world.required_seals) self.region_rules = { "Elemental Skylands": lambda state: state.has_any({"Windmill Shuriken", "Wingsuit", "Rope Dart", "Magic Firefly"}, self.player), - "Music Box": lambda state: state.has_all(set(NOTES), self.player) + "Music Box": lambda state: state.has_all(set(NOTES), self.player) or self.has_enough_seals(state), } self.location_rules = { @@ -238,16 +240,14 @@ def __init__(self, world: MessengerWorld) -> None: "Underworld Seal - Fireball Wave": lambda state: state.has_any({"Wingsuit", "Windmill Shuriken"}, self.player), "Tower of Time Seal - Time Waster": self.has_dart, - "Shop Chest": self.has_enough_seals } def set_messenger_rules(self) -> None: super().set_messenger_rules() - self.world.multiworld.completion_condition[self.player] = lambda state: True self.world.options.accessibility.value = MessengerAccessibility.option_minimal -def set_self_locking_items(world: MessengerWorld, player: int) -> None: +def set_self_locking_items(world: "MessengerWorld", player: int) -> None: multiworld = world.multiworld # do the ones for seal shuffle on and off first diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index 0c04bc015c35..b6a0b80b21a6 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -3,7 +3,6 @@ from BaseClasses import CollectionState, Item, ItemClassification, Location, Region from .constants import NOTES, PHOBEKINS, PROG_ITEMS, USEFUL_ITEMS -from .options import Goal from .regions import MEGA_SHARDS, REGIONS, SEALS from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS @@ -19,8 +18,10 @@ def __init__(self, name: str, world: "MessengerWorld") -> None: if self.name == "The Shop": shop_locations = {f"The Shop - {shop_loc}": world.location_name_to_id[f"The Shop - {shop_loc}"] for shop_loc in SHOP_ITEMS} - shop_locations.update(**{figurine: world.location_name_to_id[figurine] for figurine in FIGURINES}) self.add_locations(shop_locations, MessengerShopLocation) + elif self.name == "The Craftsman's Corner": + self.add_locations({figurine: world.location_name_to_id[figurine] for figurine in FIGURINES}, + MessengerLocation) elif self.name == "Tower HQ": locations.append("Money Wrench") if world.options.shuffle_seals and self.name in SEALS: @@ -46,10 +47,6 @@ class MessengerShopLocation(MessengerLocation): def cost(self) -> int: name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped world = cast("MessengerWorld", self.parent_region.multiworld.worlds[self.player]) - # short circuit figurines which all require demon's bane be purchased, but nothing else - if "Figurine" in name: - return world.figurine_prices[name] +\ - cast(MessengerShopLocation, world.multiworld.get_location("The Shop - Demon's Bane", self.player)).cost shop_data = SHOP_ITEMS[name] if shop_data.prerequisite: prereq_cost = 0 @@ -65,12 +62,9 @@ def cost(self) -> int: return world.shop_prices[name] + prereq_cost return world.shop_prices[name] - def can_afford(self, state: CollectionState) -> bool: + def access_rule(self, state: CollectionState) -> bool: world = cast("MessengerWorld", state.multiworld.worlds[self.player]) can_afford = state.has("Shards", self.player, min(self.cost, world.total_shards)) - if "Figurine" in self.name: - can_afford = state.has("Money Wrench", self.player) and can_afford\ - and state.can_reach("Money Wrench", "Location", self.player) return can_afford diff --git a/worlds/messenger/test/test_logic.py b/worlds/messenger/test/test_logic.py index 53ea92992212..15df89b92097 100644 --- a/worlds/messenger/test/test_logic.py +++ b/worlds/messenger/test/test_logic.py @@ -111,4 +111,3 @@ def test_access(self) -> None: for loc in all_locations: with self.subTest("Default unreachables", location=loc): self.assertFalse(self.can_reach_location(loc)) - self.assertBeatable(True) diff --git a/worlds/messenger/test/test_shop.py b/worlds/messenger/test/test_shop.py index bfd3b417a875..afb1b32b88e3 100644 --- a/worlds/messenger/test/test_shop.py +++ b/worlds/messenger/test/test_shop.py @@ -106,6 +106,5 @@ def test_costs(self) -> None: elif loc == "Demon Hive Figurine": self.assertIn(price, self.options["shop_price_plan"]["Demon Hive Figurine"]) - self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost) self.assertTrue(loc in FIGURINES) self.assertEqual(len(figures), len(FIGURINES)) From 205c6acb495bdc6c213abfe77e134c52f5ca2339 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Fri, 24 Nov 2023 01:59:41 +0100 Subject: [PATCH 068/104] lufia2ac: fix client behavior at max blue chests combined with party member or capsule monster shuffle (#2478) When option combinations at (or near) the maximum location count were used, the client could trip over a wrongly coded limit and stop sending checks. --- worlds/lufia2ac/Client.py | 2 +- worlds/lufia2ac/Locations.py | 2 +- worlds/lufia2ac/Options.py | 1 + worlds/lufia2ac/__init__.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/worlds/lufia2ac/Client.py b/worlds/lufia2ac/Client.py index bc0cb6a7d8dd..ac0de19bfdd6 100644 --- a/worlds/lufia2ac/Client.py +++ b/worlds/lufia2ac/Client.py @@ -113,7 +113,7 @@ async def game_watcher(self, ctx: SNIContext) -> None: }], }]) - total_blue_chests_checked: int = min(sum(blue_chests_checked.values()), BlueChestCount.range_end) + total_blue_chests_checked: int = min(sum(blue_chests_checked.values()), BlueChestCount.overall_max) snes_buffered_write(ctx, L2AC_TX_ADDR + 8, total_blue_chests_checked.to_bytes(2, "little")) location_ids: List[int] = [locations_start_id + i for i in range(total_blue_chests_checked)] diff --git a/worlds/lufia2ac/Locations.py b/worlds/lufia2ac/Locations.py index 2f433f72e2ae..510ecbbbf7f4 100644 --- a/worlds/lufia2ac/Locations.py +++ b/worlds/lufia2ac/Locations.py @@ -6,7 +6,7 @@ start_id: int = 0xAC0000 l2ac_location_name_to_id: Dict[str, int] = { - **{f"Blue chest {i + 1}": (start_id + i) for i in range(BlueChestCount.range_end + 7 + 6)}, + **{f"Blue chest {i + 1}": (start_id + i) for i in range(BlueChestCount.overall_max)}, **{f"Iris treasure {i + 1}": (start_id + 0x039C + i) for i in range(9)}, "Boss": start_id + 0x01C2, } diff --git a/worlds/lufia2ac/Options.py b/worlds/lufia2ac/Options.py index 783da8e407b7..419532cded6b 100644 --- a/worlds/lufia2ac/Options.py +++ b/worlds/lufia2ac/Options.py @@ -121,6 +121,7 @@ class BlueChestCount(Range): range_start = 10 range_end = 100 default = 25 + overall_max = range_end + 7 + 6 # Have to account for capsule monster and party member items class Boss(RandomGroupsChoice): diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index 8f9b8d58231a..9bd436fa0d2f 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -66,7 +66,7 @@ class L2ACWorld(World): "Party members": {name for name, data in l2ac_item_table.items() if data.type is ItemType.PARTY_MEMBER}, } data_version: ClassVar[int] = 2 - required_client_version: Tuple[int, int, int] = (0, 4, 2) + required_client_version: Tuple[int, int, int] = (0, 4, 4) # L2ACWorld specific properties rom_name: bytearray From e93842a52cf853b04f759b1138db7813b233ab62 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 24 Nov 2023 06:27:03 +0100 Subject: [PATCH 069/104] =?UTF-8?q?The=20Witness:=20Big=E2=84=A2=20new?= =?UTF-8?q?=E2=84=A2=20content=20update=E2=84=A2=20(#2114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: blastron Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/witness/Options.py | 126 +++-- worlds/witness/WitnessItems.txt | 115 +++- worlds/witness/WitnessLogic.txt | 182 +++--- worlds/witness/WitnessLogicExpert.txt | 198 ++++--- worlds/witness/WitnessLogicVanilla.txt | 180 +++--- worlds/witness/__init__.py | 227 +++++--- worlds/witness/hints.py | 158 ++++-- worlds/witness/items.py | 68 ++- worlds/witness/locations.py | 241 ++++---- worlds/witness/player_logic.py | 525 +++++++++--------- worlds/witness/regions.py | 136 +++-- worlds/witness/rules.py | 385 +++++++------ .../witness/settings/Door_Panel_Shuffle.txt | 31 -- worlds/witness/settings/Door_Shuffle/Boat.txt | 2 + .../Complex_Additional_Panels.txt | 25 + .../Door_Shuffle/Complex_Door_Panels.txt | 38 ++ .../Complex_Doors.txt} | 21 +- .../Door_Shuffle/Elevators_Come_To_You.txt | 11 + .../Door_Shuffle/Simple_Additional_Panels.txt | 11 + .../Simple_Doors.txt} | 57 +- .../settings/Door_Shuffle/Simple_Panels.txt | 22 + worlds/witness/settings/Doors_Max.txt | 211 ------- worlds/witness/settings/EP_Shuffle/EP_All.txt | 270 ++++----- .../witness/settings/EP_Shuffle/EP_Easy.txt | 31 +- .../settings/EP_Shuffle/EP_NoCavesEPs.txt | 5 - .../settings/EP_Shuffle/EP_NoEclipse.txt | 4 +- .../settings/EP_Shuffle/EP_NoMountainEPs.txt | 4 - .../witness/settings/EP_Shuffle/EP_Sides.txt | 67 +-- .../witness/settings/EP_Shuffle/EP_Videos.txt | 6 - worlds/witness/settings/Early_Caves.txt | 6 + .../{Early_UTM.txt => Early_Caves_Start.txt} | 4 +- .../{ => Exclusions}/Disable_Unrandomized.txt | 62 ++- .../witness/settings/Exclusions/Discards.txt | 15 + worlds/witness/settings/Exclusions/Vaults.txt | 31 ++ .../settings/Postgame/Beyond_Challenge.txt | 4 + .../Postgame/Bottom_Floor_Discard.txt | 2 + .../Bottom_Floor_Discard_NonDoors.txt | 6 + worlds/witness/settings/Postgame/Caves.txt | 65 +++ .../settings/Postgame/Challenge_Vault_Box.txt | 3 + .../settings/Postgame/Mountain_Lower.txt | 27 + .../settings/Postgame/Mountain_Upper.txt | 41 ++ .../settings/Postgame/Path_To_Challenge.txt | 30 + worlds/witness/static_logic.py | 78 +-- worlds/witness/utils.py | 148 +++-- 44 files changed, 2210 insertions(+), 1669 deletions(-) delete mode 100644 worlds/witness/settings/Door_Panel_Shuffle.txt create mode 100644 worlds/witness/settings/Door_Shuffle/Boat.txt create mode 100644 worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt create mode 100644 worlds/witness/settings/Door_Shuffle/Complex_Door_Panels.txt rename worlds/witness/settings/{Doors_Complex.txt => Door_Shuffle/Complex_Doors.txt} (94%) create mode 100644 worlds/witness/settings/Door_Shuffle/Elevators_Come_To_You.txt create mode 100644 worlds/witness/settings/Door_Shuffle/Simple_Additional_Panels.txt rename worlds/witness/settings/{Doors_Simple.txt => Door_Shuffle/Simple_Doors.txt} (74%) create mode 100644 worlds/witness/settings/Door_Shuffle/Simple_Panels.txt delete mode 100644 worlds/witness/settings/Doors_Max.txt delete mode 100644 worlds/witness/settings/EP_Shuffle/EP_NoCavesEPs.txt delete mode 100644 worlds/witness/settings/EP_Shuffle/EP_NoMountainEPs.txt delete mode 100644 worlds/witness/settings/EP_Shuffle/EP_Videos.txt create mode 100644 worlds/witness/settings/Early_Caves.txt rename worlds/witness/settings/{Early_UTM.txt => Early_Caves_Start.txt} (65%) rename worlds/witness/settings/{ => Exclusions}/Disable_Unrandomized.txt (70%) create mode 100644 worlds/witness/settings/Exclusions/Discards.txt create mode 100644 worlds/witness/settings/Exclusions/Vaults.txt create mode 100644 worlds/witness/settings/Postgame/Beyond_Challenge.txt create mode 100644 worlds/witness/settings/Postgame/Bottom_Floor_Discard.txt create mode 100644 worlds/witness/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt create mode 100644 worlds/witness/settings/Postgame/Caves.txt create mode 100644 worlds/witness/settings/Postgame/Challenge_Vault_Box.txt create mode 100644 worlds/witness/settings/Postgame/Mountain_Lower.txt create mode 100644 worlds/witness/settings/Postgame/Mountain_Upper.txt create mode 100644 worlds/witness/settings/Postgame/Path_To_Challenge.txt diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index b7364b5e70ea..4c4b4f76267f 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -1,23 +1,25 @@ -from typing import Dict, Union -from BaseClasses import MultiWorld -from Options import Toggle, DefaultOnToggle, Range, Choice +from dataclasses import dataclass +from Options import Toggle, DefaultOnToggle, Range, Choice, PerGameCommonOptions -# class HardMode(Toggle): -# "Play the randomizer in hardmode" -# display_name = "Hard Mode" - class DisableNonRandomizedPuzzles(Toggle): """Disables puzzles that cannot be randomized. This includes many puzzles that heavily involve the environment, such as Shadows, Monastery or Orchard. - The lasers for those areas will be activated as you solve optional puzzles throughout the island.""" + The lasers for those areas will activate as you solve optional puzzles, such as Discarded Panels. + Additionally, the panels activating Monastery Laser and Jungle Popup Wall will be on from the start.""" display_name = "Disable non randomized puzzles" -class EarlySecretArea(Toggle): - """Opens the Mountainside shortcut to the Caves from the start. - (Otherwise known as "UTM", "Caves" or the "Challenge Area")""" +class EarlyCaves(Choice): + """Adds an item that opens the Caves Shortcuts to Swamp and Mountain, + allowing early access to the Caves even if you are not playing a remote Door Shuffle mode. + You can either add this item to the pool to be found on one of your randomized checks, + or you can outright start with it and have immediate access to the Caves. + If you choose "add_to_pool" and you are already playing a remote Door Shuffle mode, this setting will do nothing.""" display_name = "Early Caves" + option_off = 0 + option_add_to_pool = 1 + option_starting_inventory = 2 class ShuffleSymbols(DefaultOnToggle): @@ -34,27 +36,41 @@ class ShuffleLasers(Toggle): class ShuffleDoors(Choice): - """If on, opening doors will require their respective "keys". - If set to "panels", those keys will unlock the panels on doors. - In "doors_simple" and "doors_complex", the doors will magically open by themselves upon receiving the key. - The last option, "max", is a combination of "doors_complex" and "panels".""" + """If on, opening doors, moving bridges etc. will require a "key". + If set to "panels", the panel on the door will be locked until receiving its corresponding key. + If set to "doors", the door will open immediately upon receiving its key. Door panels are added as location checks. + "Mixed" includes all doors from "doors", and all control panels (bridges, elevators etc.) from "panels".""" display_name = "Shuffle Doors" - option_none = 0 + option_off = 0 option_panels = 1 - option_doors_simple = 2 - option_doors_complex = 3 - option_max = 4 + option_doors = 2 + option_mixed = 3 + + +class DoorGroupings(Choice): + """If set to "none", there will be one key for every door, resulting in up to 120 keys being added to the item pool. + If set to "regional", all doors in the same general region will open at once with a single key, + reducing the amount of door items and complexity.""" + display_name = "Door Groupings" + option_off = 0 + option_regional = 1 + + +class ShuffleBoat(DefaultOnToggle): + """If set, adds a "Boat" item to the item pool. Before receiving this item, you will not be able to use the boat.""" + display_name = "Shuffle Boat" class ShuffleDiscardedPanels(Toggle): """Add Discarded Panels into the location pool. - Solving certain Discarded Panels may still be necessary to beat the game, even if this is off.""" + Solving certain Discarded Panels may still be necessary to beat the game, even if this is off - The main example + of this being the alternate activation triggers in disable_non_randomized.""" display_name = "Shuffle Discarded Panels" class ShuffleVaultBoxes(Toggle): - """Vault Boxes will have items on them.""" + """Add Vault Boxes to the location pool.""" display_name = "Shuffle Vault Boxes" @@ -132,6 +148,12 @@ class ChallengeLasers(Range): default = 11 +class ElevatorsComeToYou(Toggle): + """If true, the Quarry Elevator, Bunker Elevator and Swamp Long Bridge will "come to you" if you approach them. + This does actually affect logic as it allows unintended backwards / early access into these areas.""" + display_name = "All Bridges & Elevators come to you" + + class TrapPercentage(Range): """Replaces junk items with traps, at the specified rate.""" display_name = "Trap Percentage" @@ -150,8 +172,8 @@ class PuzzleSkipAmount(Range): class HintAmount(Range): - """Adds hints to Audio Logs. Hints will have the same number of duplicates, as many as will fit. Remaining Audio - Logs will have junk hints.""" + """Adds hints to Audio Logs. If set to a low amount, up to 2 additional duplicates of each hint will be added. + Remaining Audio Logs will have junk hints.""" display_name = "Hints on Audio Logs" range_start = 0 range_end = 49 @@ -164,38 +186,26 @@ class DeathLink(Toggle): display_name = "Death Link" -the_witness_options: Dict[str, type] = { - "puzzle_randomization": PuzzleRandomization, - "shuffle_symbols": ShuffleSymbols, - "shuffle_doors": ShuffleDoors, - "shuffle_lasers": ShuffleLasers, - "disable_non_randomized_puzzles": DisableNonRandomizedPuzzles, - "shuffle_discarded_panels": ShuffleDiscardedPanels, - "shuffle_vault_boxes": ShuffleVaultBoxes, - "shuffle_EPs": ShuffleEnvironmentalPuzzles, - "EP_difficulty": EnvironmentalPuzzlesDifficulty, - "shuffle_postgame": ShufflePostgame, - "victory_condition": VictoryCondition, - "mountain_lasers": MountainLasers, - "challenge_lasers": ChallengeLasers, - "early_secret_area": EarlySecretArea, - "trap_percentage": TrapPercentage, - "puzzle_skip_amount": PuzzleSkipAmount, - "hint_amount": HintAmount, - "death_link": DeathLink, -} - - -def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool: - return get_option_value(world, player, name) > 0 - - -def get_option_value(world: MultiWorld, player: int, name: str) -> Union[bool, int]: - option = getattr(world, name, None) - - if option is None: - return 0 - - if issubclass(the_witness_options[name], Toggle) or issubclass(the_witness_options[name], DefaultOnToggle): - return bool(option[player].value) - return option[player].value +@dataclass +class TheWitnessOptions(PerGameCommonOptions): + puzzle_randomization: PuzzleRandomization + shuffle_symbols: ShuffleSymbols + shuffle_doors: ShuffleDoors + door_groupings: DoorGroupings + shuffle_boat: ShuffleBoat + shuffle_lasers: ShuffleLasers + disable_non_randomized_puzzles: DisableNonRandomizedPuzzles + shuffle_discarded_panels: ShuffleDiscardedPanels + shuffle_vault_boxes: ShuffleVaultBoxes + shuffle_EPs: ShuffleEnvironmentalPuzzles + EP_difficulty: EnvironmentalPuzzlesDifficulty + shuffle_postgame: ShufflePostgame + victory_condition: VictoryCondition + mountain_lasers: MountainLasers + challenge_lasers: ChallengeLasers + early_caves: EarlyCaves + elevators_come_to_you: ElevatorsComeToYou + trap_percentage: TrapPercentage + puzzle_skip_amount: PuzzleSkipAmount + hint_amount: HintAmount + death_link: DeathLink diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index 71ffe276a60e..750d6bd4ebec 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -37,22 +37,42 @@ Jokes: Doors: 1100 - Glass Factory Entry (Panel) - 0x01A54 +1101 - Tutorial Outpost Entry (Panel) - 0x0A171 +1102 - Tutorial Outpost Exit (Panel) - 0x04CA4 1105 - Symmetry Island Lower (Panel) - 0x000B0 1107 - Symmetry Island Upper (Panel) - 0x1C349 1110 - Desert Light Room Entry (Panel) - 0x0C339 1111 - Desert Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B +1112 - Desert Light Control (Panel) - 0x09FAA +1113 - Desert Flood Room Entry (Panel) - 0x0A249 +1115 - Quarry Elevator Control (Panel) - 0x17CC4 +1117 - Quarry Entry 1 (Panel) - 0x09E57 +1118 - Quarry Entry 2 (Panel) - 0x17C09 1119 - Quarry Stoneworks Entry (Panel) - 0x01E5A,0x01E59 1120 - Quarry Stoneworks Ramp Controls (Panel) - 0x03678,0x03676 1122 - Quarry Stoneworks Lift Controls (Panel) - 0x03679,0x03675 1125 - Quarry Boathouse Ramp Height Control (Panel) - 0x03852 1127 - Quarry Boathouse Ramp Horizontal Control (Panel) - 0x03858 +1129 - Quarry Boathouse Hook Control (Panel) - 0x275FA 1131 - Shadows Door Timer (Panel) - 0x334DB,0x334DC +1140 - Keep Hedge Maze 1 (Panel) - 0x00139 +1142 - Keep Hedge Maze 2 (Panel) - 0x019DC +1144 - Keep Hedge Maze 3 (Panel) - 0x019E7 +1146 - Keep Hedge Maze 4 (Panel) - 0x01A0F 1150 - Monastery Entry Left (Panel) - 0x00B10 1151 - Monastery Entry Right (Panel) - 0x00C92 -1162 - Town Tinted Glass Door (Panel) - 0x28998 +1156 - Monastery Shutters Control (Panel) - 0x09D9B +1162 - Town RGB House Entry (Panel) - 0x28998 1163 - Town Church Entry (Panel) - 0x28A0D -1166 - Town Maze Panel (Drop-Down Staircase) (Panel) - 0x28A79 -1169 - Windmill Entry (Panel) - 0x17F5F +1164 - Town RGB Control (Panel) - 0x334D8 +1166 - Town Maze Stairs (Panel) - 0x28A79 +1167 - Town Maze Rooftop Bridge (Panel) - 0x2896A +1169 - Town Windmill Entry (Panel) - 0x17F5F +1172 - Town Cargo Box Entry (Panel) - 0x0A0C8 +1182 - Windmill Turn Control (Panel) - 0x17D02 +1184 - Theater Entry (Panel) - 0x17F89 +1185 - Theater Video Input (Panel) - 0x00815 +1189 - Theater Exit (Panel) - 0x0A168,0x33AB2 1200 - Treehouse First & Second Doors (Panel) - 0x0288C,0x02886 1202 - Treehouse Third Door (Panel) - 0x0A182 1205 - Treehouse Laser House Door Timer (Panel) - 0x2700B,0x17CBC @@ -61,10 +81,24 @@ Doors: 1180 - Bunker Entry (Panel) - 0x17C2E 1183 - Bunker Tinted Glass Door (Panel) - 0x0A099 1186 - Bunker Elevator Control (Panel) - 0x0A079 +1188 - Bunker Drop-Down Door Controls (Panel) - 0x34BC5,0x34BC6 1190 - Swamp Entry (Panel) - 0x0056E 1192 - Swamp Sliding Bridge (Panel) - 0x00609,0x18488 +1194 - Swamp Platform Shortcut (Panel) - 0x17C0D 1195 - Swamp Rotating Bridge (Panel) - 0x181F5 -1197 - Swamp Maze Control (Panel) - 0x17C0A,0x17E07 +1196 - Swamp Long Bridge (Panel) - 0x17E2B +1197 - Swamp Maze Controls (Panel) - 0x17C0A,0x17E07 +1220 - Mountain Floor 1 Light Bridge (Panel) - 0x09E39 +1225 - Mountain Floor 2 Light Bridge Near (Panel) - 0x09E86 +1230 - Mountain Floor 2 Light Bridge Far (Panel) - 0x09ED8 +1235 - Mountain Floor 2 Elevator Control (Panel) - 0x09EEB +1240 - Caves Entry (Panel) - 0x00FF8 +1242 - Caves Elevator Controls (Panel) - 0x335AB,0x335AC,0x3369D +1245 - Challenge Entry (Panel) - 0x0A16E +1250 - Tunnels Entry (Panel) - 0x039B4 +1255 - Tunnels Town Shortcut (Panel) - 0x09E85 + + 1310 - Boat - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054 1400 - Caves Mountain Shortcut (Door) - 0x2D73F @@ -82,6 +116,7 @@ Doors: 1624 - Desert Pond Room Entry (Door) - 0x0C2C3 1627 - Desert Flood Room Entry (Door) - 0x0A24B 1630 - Desert Elevator Room Entry (Door) - 0x0C316 +1631 - Desert Elevator (Door) - 0x01317 1633 - Quarry Entry 1 (Door) - 0x09D6F 1636 - Quarry Entry 2 (Door) - 0x17C07 1639 - Quarry Stoneworks Entry (Door) - 0x02010 @@ -109,13 +144,13 @@ Doors: 1699 - Keep Pressure Plates 4 Exit (Door) - 0x01D40 1702 - Keep Shadows Shortcut (Door) - 0x09E3D 1705 - Keep Tower Shortcut (Door) - 0x04F8F -1708 - Monastery Shortcut (Door) - 0x0364E +1708 - Monastery Laser Shortcut (Door) - 0x0364E 1711 - Monastery Entry Inner (Door) - 0x0C128 1714 - Monastery Entry Outer (Door) - 0x0C153 1717 - Monastery Garden Entry (Door) - 0x03750 1718 - Town Cargo Box Entry (Door) - 0x0A0C9 1720 - Town Wooden Roof Stairs (Door) - 0x034F5 -1723 - Town Tinted Glass Door - 0x28A61 +1723 - Town RGB House Entry (Door) - 0x28A61 1726 - Town Church Entry (Door) - 0x03BB0 1729 - Town Maze Stairs (Door) - 0x28AA2 1732 - Town Windmill Entry (Door) - 0x1845B @@ -129,7 +164,7 @@ Doors: 1756 - Theater Exit Right (Door) - 0x3CCDF 1759 - Jungle Bamboo Laser Shortcut (Door) - 0x3873B 1760 - Jungle Popup Wall (Door) - 0x1475B -1762 - River Monastery Shortcut (Door) - 0x0CF2A +1762 - River Monastery Garden Shortcut (Door) - 0x0CF2A 1765 - Bunker Entry (Door) - 0x0C2A4 1768 - Bunker Tinted Glass Door - 0x17C79 1771 - Bunker UV Room Entry (Door) - 0x0C2A3 @@ -166,36 +201,66 @@ Doors: 1870 - Tunnels Town Shortcut (Door) - 0x09E87 1903 - Outside Tutorial Outpost Doors - 0x03BA2,0x0A170,0x04CA3 +1904 - Glass Factory Doors - 0x0D7ED,0x01A29 1906 - Symmetry Island Doors - 0x17F3E,0x18269 1909 - Orchard Gates - 0x03313,0x03307 -1912 - Desert Doors - 0x09FEE,0x0C2C3,0x0A24B,0x0C316 -1915 - Quarry Main Entry - 0x09D6F,0x17C07 -1918 - Quarry Stoneworks Shortcuts - 0x17CE8,0x0368A,0x275FF -1921 - Quarry Boathouse Barriers - 0x17C50,0x3865F -1924 - Shadows Laser Room Door - 0x194B2,0x19665 -1927 - Shadows Barriers - 0x19865,0x0A2DF,0x1855B,0x19ADE +1912 - Desert Doors & Elevator - 0x09FEE,0x0C2C3,0x0A24B,0x0C316,0x01317 +1915 - Quarry Entry Doors - 0x09D6F,0x17C07 +1918 - Quarry Stoneworks Doors - 0x02010,0x275FF,0x17CE8,0x0368A +1921 - Quarry Boathouse Doors - 0x17C50,0x3865F,0x2769B,0x27163 +1924 - Shadows Laser Room Doors - 0x194B2,0x19665 +1927 - Shadows Lower Doors - 0x19865,0x0A2DF,0x1855B,0x19ADE,0x19B24 1930 - Keep Hedge Maze Doors - 0x01954,0x018CE,0x019D8,0x019B5,0x019E6,0x0199A,0x01A0E 1933 - Keep Pressure Plates Doors - 0x01BEC,0x01BEA,0x01CD5,0x01D40 1936 - Keep Shortcuts - 0x09E3D,0x04F8F -1939 - Monastery Entry - 0x0C128,0x0C153 -1942 - Monastery Shortcuts - 0x0364E,0x03750 -1945 - Town Doors - 0x0A0C9,0x034F5,0x28A61,0x03BB0,0x28AA2,0x1845B,0x2897B +1939 - Monastery Entry Doors - 0x0C128,0x0C153 +1942 - Monastery Shortcuts - 0x0364E,0x03750,0x0CF2A +1945 - Town Doors - 0x0A0C9,0x034F5,0x28A61,0x03BB0,0x28AA2,0x2897B 1948 - Town Tower Doors - 0x27798,0x27799,0x2779A,0x2779C -1951 - Theater Exit - 0x0A16D,0x3CCDF -1954 - Jungle & River Shortcuts - 0x3873B,0x0CF2A +1951 - Windmill & Theater Doors - 0x0A16D,0x3CCDF,0x1845B,0x17F88 +1954 - Jungle Doors - 0x3873B,0x1475B 1957 - Bunker Doors - 0x0C2A4,0x17C79,0x0C2A3,0x0A08D -1960 - Swamp Doors - 0x00C1C,0x184B7,0x38AE6,0x18507 +1960 - Swamp Doors - 0x00C1C,0x184B7,0x18507 +1961 - Swamp Shortcuts - 0x38AE6,0x2D880 1963 - Swamp Water Pumps - 0x04B7F,0x183F2,0x305D5,0x18482,0x0A1D6 1966 - Treehouse Entry Doors - 0x0C309,0x0C310,0x0A181 -1975 - Mountain Floor 2 Stairs & Doors - 0x09FFB,0x09EDD,0x09E07 -1978 - Mountain Bottom Floor Doors to Caves - 0x17F33,0x2D77D -1981 - Caves Doors to Challenge - 0x019A5,0x0A19A -1984 - Caves Exits to Main Island - 0x2D859,0x2D73F -1987 - Tunnels Doors - 0x27739,0x27263,0x09E87 +1969 - Treehouse Upper Doors - 0x0C323,0x0C32D +1975 - Mountain Floor 1 & 2 Doors - 0x09E54,0x09FFB,0x09EDD,0x09E07 +1978 - Mountain Bottom Floor Doors - 0x0C141,0x17F33,0x09F89 +1981 - Caves Doors - 0x019A5,0x0A19A,0x2D77D +1984 - Caves Shortcuts - 0x2D859,0x2D73F +1987 - Tunnels Doors - 0x27739,0x27263,0x09E87,0x0348A + +2000 - Desert Control Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B +2005 - Quarry Stoneworks Control Panels - 0x03678,0x03676,0x03679,0x03675 +2010 - Quarry Boathouse Control Panels - 0x03852,0x03858,0x275FA +2015 - Town Control Panels - 0x2896A,0x334D8 +2020 - Windmill & Theater Control Panels - 0x17D02,0x00815 +2025 - Bunker Control Panels - 0x34BC5,0x34BC6,0x0A079 +2030 - Swamp Control Panels - 0x00609,0x18488,0x181F5,0x17E2B,0x17C0A,0x17E07 +2035 - Mountain & Caves Control Panels - 0x09ED8,0x09E86,0x09E39,0x09EEB,0x335AB,0x335AC,0x3369D + +2100 - Symmetry Island Panels - 0x1C349,0x000B0 +2101 - Tutorial Outpost Panels - 0x0A171,0x04CA4 +2105 - Desert Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B,0x0C339,0x0A249 +2110 - Quarry Outside Panels - 0x17C09,0x09E57,0x17CC4 +2115 - Quarry Stoneworks Panels - 0x01E5A,0x01E59,0x03678,0x03676,0x03679,0x03675 +2120 - Quarry Boathouse Panels - 0x03852,0x03858,0x275FA +2122 - Keep Hedge Maze Panels - 0x00139,0x019DC,0x019E7,0x01A0F +2125 - Monastery Panels - 0x09D9B,0x00C92,0x00B10 +2130 - Town Church & RGB House Panels - 0x28998,0x28A0D,0x334D8 +2135 - Town Maze Panels - 0x2896A,0x28A79 +2140 - Windmill & Theater Panels - 0x17D02,0x00815,0x17F5F,0x17F89,0x0A168,0x33AB2 +2145 - Treehouse Panels - 0x0A182,0x0288C,0x02886,0x2700B,0x17CBC,0x037FF +2150 - Bunker Panels - 0x34BC5,0x34BC6,0x0A079,0x0A099,0x17C2E +2155 - Swamp Panels - 0x00609,0x18488,0x181F5,0x17E2B,0x17C0A,0x17E07,0x17C0D,0x0056E +2160 - Mountain Panels - 0x09ED8,0x09E86,0x09E39,0x09EEB +2165 - Caves Panels - 0x3369D,0x00FF8,0x0A16E,0x335AB,0x335AC +2170 - Tunnels Panels - 0x09E85,0x039B4 Lasers: 1500 - Symmetry Laser - 0x00509 -1501 - Desert Laser - 0x012FB,0x01317 +1501 - Desert Laser - 0x012FB 1502 - Quarry Laser - 0x01539 1503 - Shadows Laser - 0x181B3 1504 - Keep Laser - 0x014BB diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index dffdc1a701d0..acfbe8c14eb0 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -1,3 +1,5 @@ +Menu (Menu) - Entry - True: + Entry (Entry): First Hallway (First Hallway) - Entry - True - First Hallway Room - 0x00064: @@ -21,9 +23,9 @@ Tutorial (Tutorial) - Outside Tutorial - 0x03629: 159513 - 0x33600 (Patio Flowers EP) - 0x0C373 - True 159517 - 0x3352F (Gate EP) - 0x03505 - True -Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2: -158650 - 0x033D4 (Vault) - True - Dots & Black/White Squares -158651 - 0x03481 (Vault Box) - 0x033D4 - True +Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2 - Outside Tutorial Vault - 0x033D0: +158650 - 0x033D4 (Vault Panel) - True - Dots & Black/White Squares +Door - 0x033D0 (Vault Door) - 0x033D4 158013 - 0x0005D (Shed Row 1) - True - Dots 158014 - 0x0005E (Shed Row 2) - 0x0005D - Dots 158015 - 0x0005F (Shed Row 3) - 0x0005E - Dots @@ -44,6 +46,9 @@ Door - 0x03BA2 (Outpost Path) - 0x0A3B5 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 Door - 0x0A170 (Outpost Entry) - 0x0A171 @@ -54,6 +59,7 @@ Door - 0x04CA3 (Outpost Exit) - 0x04CA4 158600 - 0x17CFB (Discard) - True - Triangles Main Island (Main Island) - Outside Tutorial - True: +159801 - 0xFFD00 (Reached Independently) - True - True 159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29: @@ -76,7 +82,7 @@ Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0 158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry Door - 0x0D7ED (Back Wall) - 0x0005C -Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8: +Inside Glass Factory Behind Back Wall (Glass Factory) - The Ocean - 0x17CC8: 158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E: @@ -112,12 +118,12 @@ Door - 0x18269 (Upper) - 0x1C349 159000 - 0x0332B (Glass Factory Black Line Reflection EP) - True - True Symmetry Island Upper (Symmetry Island): -158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots -158066 - 0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots -158067 - 0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots -158068 - 0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots -158069 - 0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots -158070 - 0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots +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 @@ -135,9 +141,9 @@ Door - 0x03313 (Second Gate) - 0x032FF Orchard End (Orchard): -Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE: -158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots -158653 - 0x0339E (Vault Box) - 0x0CC7B - True +Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE - Desert Vault - 0x03444: +158652 - 0x0CC7B (Vault Panel) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots +Door - 0x03444 (Vault Door) - 0x0CC7B 158602 - 0x17CE7 (Discard) - True - Triangles 158076 - 0x00698 (Surface 1) - True - True 158077 - 0x0048F (Surface 2) - 0x00698 - True @@ -163,6 +169,9 @@ Laser - 0x012FB (Laser) - 0x03608 159040 - 0x334B9 (Shore EP) - True - True 159041 - 0x334BC (Island EP) - True - True +Desert Vault (Desert): +158653 - 0x0339E (Vault Box) - True - True + Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3: 158087 - 0x09FAA (Light Control) - True - True 158088 - 0x00422 (Light Room 1) - 0x09FAA - True @@ -199,18 +208,19 @@ Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316: Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True -Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB: +Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x01317: 158111 - 0x17C31 (Final Transparent) - True - True 158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True 158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True 158115 - 0x0A15C (Final Bent 1) - True - True 158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True 158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True -159035 - 0x037BB (Elevator EP) - 0x012FB - True +159035 - 0x037BB (Elevator EP) - 0x01317 - True +Door - 0x01317 (Elevator) - 0x03608 Desert Lowest Level Inbetween Shortcuts (Desert): -Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - TrueOneWay: +Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - 0xFFD00 & 0xFFD01: 158118 - 0x09E57 (Entry 1 Panel) - True - Black/White Squares 158603 - 0x17CF0 (Discard) - True - Triangles 158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Shapers @@ -222,7 +232,7 @@ Door - 0x09D6F (Entry 1) - 0x09E57 159420 - 0x289CF (Rock Line EP) - True - True 159421 - 0x289D1 (Rock Line Reflection EP) - True - True -Quarry Elevator (Quarry): +Quarry Elevator (Quarry) - Outside Quarry - 0x17CC4 - Quarry - 0x17CC4: 158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser 159403 - 0x17CB9 (Railroad EP) - 0x17CC4 - True @@ -230,28 +240,31 @@ Quarry Between Entrys (Quarry) - Quarry - 0x17C07: 158119 - 0x17C09 (Entry 2 Panel) - True - Shapers Door - 0x17C07 (Entry 2) - 0x17C09 -Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010 - Quarry Elevator - 0x17CC4: +Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010: +159802 - 0xFFD01 (Inside Reached Independently) - True - True 158121 - 0x01E5A (Stoneworks Entry Left Panel) - True - Black/White Squares 158122 - 0x01E59 (Stoneworks Entry Right Panel) - True - Dots Door - 0x02010 (Stoneworks Entry) - 0x01E59 & 0x01E5A -Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8: +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 Ground Floor - 0x03675 - Quarry Stoneworks Upper Floor - 0x03679: +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 Middle Floor - 0x03676 & 0x03679 - Quarry Stoneworks Ground Floor - 0x0368A: +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 @@ -262,7 +275,7 @@ Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Flo 158139 - 0x3C12D (Upper Row 6) - 0x0146C - Colored Squares & Eraser 158140 - 0x03686 (Upper Row 7) - 0x3C12D - Colored Squares & Eraser 158141 - 0x014E9 (Upper Row 8) - 0x03686 - Colored Squares & Eraser -158142 - 0x03677 (Stair Control) - True - Colored Squares & Eraser +158142 - 0x03677 (Stairs Panel) - True - Colored Squares & Eraser Door - 0x0368A (Stairs) - 0x03677 158143 - 0x3C125 (Control Room Left) - 0x014E9 - Black/White Squares & Dots & Eraser 158144 - 0x0367C (Control Room Right) - 0x014E9 - Colored Squares & Dots & Eraser @@ -277,7 +290,7 @@ Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Fro Door - 0x2769B (Dock) - 0x17CA6 Door - 0x27163 (Dock Invis Barrier) - 0x17CA6 -Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 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 @@ -309,9 +322,9 @@ Door - 0x3865F (Second Barrier) - 0x38663 158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Stars & Eraser & Shapers 159401 - 0x005F6 (Hook EP) - 0x275FA & 0x03852 & 0x3865F - True -Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 & 0x19665: +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 +Door - 0x19B24 (Timed Door) - 0x334DB | 0x334DC 158171 - 0x0AC74 (Intro 6) - 0x0A8DC - True 158172 - 0x0AC7A (Intro 7) - 0x0AC74 - True 158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - True @@ -336,7 +349,7 @@ 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 & 0x334DC & 0x19B24 - 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 @@ -345,7 +358,7 @@ Door - 0x1855B (Ledge Barrier) - 0x0A8DC Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC Shadows Laser Room (Shadows): -158703 - 0x19650 (Laser Panel) - True - True +158703 - 0x19650 (Laser Panel) - 0x194B2 & 0x19665 - True Laser - 0x181B3 (Laser) - 0x19650 Treehouse Beach (Treehouse Beach) - Main Island - True: @@ -395,9 +408,9 @@ Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F 158205 - 0x09E49 (Shadows Shortcut Panel) - True - True Door - 0x09E3D (Shadows Shortcut) - 0x09E49 -Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: -158654 - 0x00AFB (Vault) - True - Symmetry & Sound Dots & Colored Dots -158655 - 0x03535 (Vault Box) - 0x00AFB - True +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 - Triangles 159220 - 0x03B22 (Circle Far EP) - True - True 159221 - 0x03B23 (Circle Left EP) - True - True @@ -407,6 +420,9 @@ Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: 159226 - 0x28ABE (Rope Outer EP) - True - True 159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True +Shipwreck Vault (Shipwreck): +158655 - 0x03535 (Vault Box) - True - True + Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut Panel) - True - True Door - 0x04F8F (Tower Shortcut) - 0x0361B @@ -422,8 +438,8 @@ Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159251 - 0x3348F (Hedges EP) - True - True Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: -158207 - 0x03713 (Shortcut Panel) - True - True -Door - 0x0364E (Shortcut) - 0x03713 +158207 - 0x03713 (Laser Shortcut Panel) - True - True +Door - 0x0364E (Laser Shortcut) - 0x03713 158208 - 0x00B10 (Entry Left) - True - True 158209 - 0x00C92 (Entry Right) - True - True Door - 0x0C128 (Entry Inner) - 0x00B10 @@ -454,7 +470,7 @@ Inside Monastery (Monastery): Monastery Garden (Monastery): -Town (Town) - Main Island - True - Boat - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: +Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: 158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat 158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Black/White Squares & Shapers Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 @@ -469,11 +485,11 @@ Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 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 Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1 -158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers -Door - 0x28A61 (Tinted Glass Door) - 0x28998 +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 Stair Control) - True - True +158228 - 0x28A79 (Maze Panel) - True - True Door - 0x28AA2 (Maze Stairs) - 0x28A79 158241 - 0x17F5F (Windmill Entry Panel) - True - Dots Door - 0x1845B (Windmill Entry) - 0x17F5F @@ -557,7 +573,7 @@ Door - 0x3CCDF (Exit Right) - 0x33AB2 159556 - 0x33A2A (Door EP) - 0x03553 - True 159558 - 0x33B06 (Church EP) - 0x0354E - True -Jungle (Jungle) - Main Island - True - Outside Jungle River - 0x3873B - Boat - 0x17CDF: +Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF: 158251 - 0x17CDF (Shore Boat Spawn) - True - Boat 158609 - 0x17F9B (Discard) - True - Triangles 158252 - 0x002C4 (First Row 1) - True - True @@ -588,18 +604,21 @@ Door - 0x3873B (Laser Shortcut) - 0x337FA 159350 - 0x035CB (Bamboo CCW EP) - True - True 159351 - 0x035CF (Bamboo CW EP) - True - True -Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A: -158267 - 0x17CAA (Monastery Shortcut Panel) - True - True -Door - 0x0CF2A (Monastery Shortcut) - 0x17CAA -158663 - 0x15ADD (Vault) - True - Black/White Squares & Dots -158664 - 0x03702 (Vault Box) - 0x15ADD - True +Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A - River 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 159120 - 0x03BE2 (Monastery Garden Left EP) - 0x03750 - True 159121 - 0x03BE3 (Monastery Garden Right EP) - True - True 159122 - 0x0A409 (Monastery Wall EP) - True - True +River Vault (River): +158664 - 0x03702 (Vault Box) - True - True + Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4: -158268 - 0x17C2E (Entry Panel) - True - Black/White Squares & Colored Squares +158268 - 0x17C2E (Entry Panel) - True - Black/White Squares Door - 0x0C2A4 (Entry) - 0x17C2E Bunker (Bunker) - Bunker Glass Room - 0x17C79: @@ -616,9 +635,9 @@ Bunker (Bunker) - Bunker Glass Room - 0x17C79: Door - 0x17C79 (Tinted Glass Door) - 0x0A099 Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3: -158279 - 0x0A010 (Glass Room 1) - True - Colored Squares -158280 - 0x0A01B (Glass Room 2) - 0x0A010 - Colored Squares & Black/White Squares -158281 - 0x0A01F (Glass Room 3) - 0x0A01B - Colored Squares & Black/White Squares +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: @@ -631,7 +650,7 @@ Door - 0x0A08D (Elevator Room Entry) - 0x17E67 Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay: 159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True -Bunker Elevator (Bunker) - Bunker Laser Platform - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: +Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: 158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares Bunker Green Room (Bunker) - Bunker Elevator - TrueOneWay: @@ -676,7 +695,7 @@ Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat 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) - True - Shapers +158318 - 0x17C0E (Platform Shortcut Right Panel) - 0x17C0D - Shapers Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E Door - 0x04B7F (Cyan Water Pump) - 0x00006 @@ -715,18 +734,21 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 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 Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8: +158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers 158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Rotated Shapers 158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers -158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers Door - 0x18482 (Blue Water Pump) - 0x00E3A 159332 - 0x3365F (Boat EP) - 0x09DB8 - True 159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True -Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6: +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): @@ -752,7 +774,7 @@ Laser - 0x00BF6 (Laser) - 0x03615 158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers Door - 0x2D880 (Laser Shortcut) - 0x17C02 -Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309: +Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309 - The Ocean - 0x17C95: 158343 - 0x17C95 (Boat Spawn) - True - Boat 158344 - 0x0288C (First Door Panel) - True - Stars Door - 0x0C309 (First Door) - 0x0288C @@ -778,7 +800,7 @@ Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181: 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 Control) - True - 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 @@ -802,7 +824,7 @@ Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2: 158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D: -158404 - 0x037FF (Bridge Control) - True - Stars +158404 - 0x037FF (Drawbridge Panel) - True - Stars Door - 0x0C32D (Drawbridge) - 0x037FF Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6: @@ -847,7 +869,7 @@ 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 +Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DDB | 0x17CBC Treehouse Laser Room Back Platform (Treehouse): 158611 - 0x17FA0 (Laser Discard) - True - Triangles @@ -860,19 +882,22 @@ Treehouse Laser Room (Treehouse): 158403 - 0x17CBC (Laser House Door Timer Inside) - True - True Laser - 0x028A4 (Laser) - 0x03613 -Mountainside (Mountainside) - Main Island - True - Mountaintop - True: +Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: 158612 - 0x17C42 (Discard) - True - Triangles -158665 - 0x002A6 (Vault) - True - Symmetry & Colored Dots & Black/White Squares & Dots -158666 - 0x03542 (Vault Box) - 0x002A6 - True +158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares & Dots +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 Top Layer - 0x17C34: 158405 - 0x0042D (River Shape) - True - True 158406 - 0x09F7F (Box Short) - 7 Lasers - True -158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol -158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True +158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol +158800 - 0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True 159300 - 0x001A3 (River Shape EP) - True - True 159320 - 0x3370E (Arch Black EP) - True - True 159324 - 0x336C8 (Arch White Right EP) - True - True @@ -881,7 +906,7 @@ Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: Mountain Top Layer (Mountain Floor 1) - Mountain Top Layer Bridge - 0x09E39: 158408 - 0x09E39 (Light Bridge Controller) - True - Black/White Squares & Colored Squares & Eraser -Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: +Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Top Layer At Door - TrueOneWay: 158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots 158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Dots 158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers & Dots @@ -899,6 +924,8 @@ Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: 158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Dots 158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers 158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shapers + +Mountain Top Layer 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 Blue Bridge - 0x09E86 - Mountain Pink Bridge EP - TrueOneWay: @@ -917,7 +944,7 @@ 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 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 - Dots 158433 - 0x09FCE (Far Row 2) - 0x09FCC - Black/White Squares 158434 - 0x09FCF (Far Row 3) - 0x09FCE - Stars @@ -935,29 +962,27 @@ Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Third Layer - 0x09EEB: 158439 - 0x09EEB (Elevator Control Panel) - True - Dots -Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89: +Mountain Third Layer (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 - Rotated Shapers 158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser 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 Bottom Floor Rock - 0x17FA2 - Final Room - 0x0C141 - Mountain Pink Bridge EP - TrueOneWay: +Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Path to Caves - 0x17F33 - Final Room - 0x0C141: 158614 - 0x17FA2 (Discard) - 0xFFF00 - Triangles 158445 - 0x01983 (Final Room Entry Left) - True - Shapers & Stars 158446 - 0x01987 (Final Room Entry Right) - True - Colored Squares & Dots Door - 0x0C141 (Final Room Entry) - 0x01983 & 0x01987 -159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True -159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True +Door - 0x17F33 (Rock Open) - 0x17FA2 | 0x334E1 Mountain Pink Bridge EP (Mountain Floor 2): 159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True -Mountain Bottom Floor Rock (Mountain Bottom Floor) - Mountain Bottom Floor - 0x17F33 - Mountain Path to Caves - 0x17F33: -Door - 0x17F33 (Rock Open) - True - True - -Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x334E1 - Caves - 0x2D77D: +Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D: 158447 - 0x00FF8 (Caves Entry Panel) - True - Triangles & Black/White Squares Door - 0x2D77D (Caves Entry) - 0x00FF8 158448 - 0x334E1 (Rock Control) - True - True @@ -1021,7 +1046,7 @@ Path to Challenge (Caves) - Challenge - 0x0A19A: 158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Shapers & Stars + Same Colored Symbol Door - 0x0A19A (Challenge Entry) - 0x0A16E -Challenge (Challenge) - Tunnels - 0x0348A: +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 @@ -1041,11 +1066,14 @@ Challenge (Challenge) - Tunnels - 0x0348A: 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 -158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True +Door - 0x04D75 (Vault Door) - 0x1C31A & 0x1C319 158518 - 0x039B4 (Tunnels Entry Panel) - True - Triangles Door - 0x0348A (Tunnels Entry) - 0x039B4 159530 - 0x28B30 (Water EP) - True - True +Challenge Vault (Challenge): +158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True + Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87: 158668 - 0x2FAF6 (Vault Box) - True - True 158519 - 0x27732 (Theater Shortcut Panel) - True - True @@ -1075,7 +1103,7 @@ Elevator (Mountain Final Room): 158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True 158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True -Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: +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 @@ -1093,34 +1121,38 @@ Obelisks (EPs) - Entry - True: 159702 - 0xFFE02 (Desert Obelisk Side 3) - 0x3351D - True 159703 - 0xFFE03 (Desert Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True 159704 - 0xFFE04 (Desert Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True +159709 - 0x00359 (Desert Obelisk) - True - True 159710 - 0xFFE10 (Monastery Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True 159711 - 0xFFE11 (Monastery Obelisk Side 2) - 0x03AC5 - True 159712 - 0xFFE12 (Monastery Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True 159713 - 0xFFE13 (Monastery Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True 159714 - 0xFFE14 (Monastery Obelisk Side 5) - 0x03E01 - True 159715 - 0xFFE15 (Monastery Obelisk Side 6) - 0x289F4 & 0x289F5 - True +159719 - 0x00263 (Monastery Obelisk) - True - True 159720 - 0xFFE20 (Treehouse Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True 159721 - 0xFFE21 (Treehouse Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True 159722 - 0xFFE22 (Treehouse Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True 159723 - 0xFFE23 (Treehouse Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True 159724 - 0xFFE24 (Treehouse Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True 159725 - 0xFFE25 (Treehouse Obelisk Side 6) - 0x28AE9 & 0x3348F - True +159729 - 0x00097 (Treehouse Obelisk) - True - True 159730 - 0xFFE30 (River Obelisk Side 1) - 0x001A3 & 0x335AE - True 159731 - 0xFFE31 (River Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True 159732 - 0xFFE32 (River Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True 159733 - 0xFFE33 (River Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True 159734 - 0xFFE34 (River Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True 159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True +159739 - 0x00367 (River Obelisk) - True - True 159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True 159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True 159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True 159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True 159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True +159749 - 0x22073 (Quarry Obelisk) - True - True 159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True 159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True 159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True 159753 - 0xFFE53 (Town Obelisk Side 4) - 0x28B30 & 0x035C9 - True 159754 - 0xFFE54 (Town Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True 159755 - 0xFFE55 (Town Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True - -Lasers (Lasers) - Entry - True: +159759 - 0x0A16C (Town Obelisk) - True - True diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt index 581167cc450d..b1d9b8e30e40 100644 --- a/worlds/witness/WitnessLogicExpert.txt +++ b/worlds/witness/WitnessLogicExpert.txt @@ -1,3 +1,5 @@ +Menu (Menu) - Entry - True: + Entry (Entry): First Hallway (First Hallway) - Entry - True - First Hallway Room - 0x00064: @@ -21,9 +23,9 @@ Tutorial (Tutorial) - Outside Tutorial - True: 159513 - 0x33600 (Patio Flowers EP) - 0x0C373 - True 159517 - 0x3352F (Gate EP) - 0x03505 - True -Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2: -158650 - 0x033D4 (Vault) - True - Dots & Full Dots & Squares & Black/White Squares -158651 - 0x03481 (Vault Box) - 0x033D4 - True +Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2 - Outside Tutorial Vault - 0x033D0: +158650 - 0x033D4 (Vault Panel) - True - Dots & Full Dots & Squares & Black/White Squares +Door - 0x033D0 (Vault Door) - 0x033D4 158013 - 0x0005D (Shed Row 1) - True - Dots & Full Dots 158014 - 0x0005E (Shed Row 2) - 0x0005D - Dots & Full Dots 158015 - 0x0005F (Shed Row 3) - 0x0005E - Dots & Full Dots @@ -44,6 +46,9 @@ Door - 0x03BA2 (Outpost Path) - 0x0A3B5 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 Door - 0x0A170 (Outpost Entry) - 0x0A171 @@ -54,6 +59,7 @@ Door - 0x04CA3 (Outpost Exit) - 0x04CA4 158600 - 0x17CFB (Discard) - True - Arrows Main Island (Main Island) - Outside Tutorial - True: +159801 - 0xFFD00 (Reached Independently) - True - True 159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29: @@ -76,7 +82,7 @@ Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0 158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry & Dots Door - 0x0D7ED (Back Wall) - 0x0005C -Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8: +Inside Glass Factory Behind Back Wall (Glass Factory) - The Ocean - 0x17CC8: 158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E: @@ -112,12 +118,12 @@ Door - 0x18269 (Upper) - 0x1C349 159000 - 0x0332B (Glass Factory Black Line Reflection EP) - True - True Symmetry Island Upper (Symmetry Island): -158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots -158066 - 0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots -158067 - 0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots -158068 - 0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots -158069 - 0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots -158070 - 0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots +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 @@ -135,9 +141,9 @@ Door - 0x03313 (Second Gate) - 0x032FF Orchard End (Orchard): -Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE: -158652 - 0x0CC7B (Vault) - True - Dots & Full Dots & Stars & Stars + Same Colored Symbol & Eraser & Triangles & Shapers & Negative Shapers & Colored Squares -158653 - 0x0339E (Vault Box) - 0x0CC7B - True +Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE - Desert Vault - 0x03444: +158652 - 0x0CC7B (Vault Panel) - True - Dots & Full Dots & Stars & Stars + Same Colored Symbol & Eraser & Triangles & Shapers & Negative Shapers & Colored Squares +Door - 0x03444 (Vault Door) - 0x0CC7B 158602 - 0x17CE7 (Discard) - True - Arrows 158076 - 0x00698 (Surface 1) - True - True 158077 - 0x0048F (Surface 2) - 0x00698 - True @@ -163,6 +169,9 @@ Laser - 0x012FB (Laser) - 0x03608 159040 - 0x334B9 (Shore EP) - True - True 159041 - 0x334BC (Island EP) - True - True +Desert Vault (Desert): +158653 - 0x0339E (Vault Box) - True - True + Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3: 158087 - 0x09FAA (Light Control) - True - True 158088 - 0x00422 (Light Room 1) - 0x09FAA - True @@ -199,18 +208,19 @@ Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316: Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True -Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB: +Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x01317: 158111 - 0x17C31 (Final Transparent) - True - True 158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True 158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True 158115 - 0x0A15C (Final Bent 1) - True - True 158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True 158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True -159035 - 0x037BB (Elevator EP) - 0x012FB - True +159035 - 0x037BB (Elevator EP) - 0x01317 - True +Door - 0x01317 (Elevator) - 0x03608 Desert Lowest Level Inbetween Shortcuts (Desert): -Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - TrueOneWay: +Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - 0xFFD00 & 0xFFD01: 158118 - 0x09E57 (Entry 1 Panel) - True - Squares & Black/White Squares & Triangles 158603 - 0x17CF0 (Discard) - True - Arrows 158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Triangles & Stars & Stars + Same Colored Symbol @@ -222,7 +232,7 @@ Door - 0x09D6F (Entry 1) - 0x09E57 159420 - 0x289CF (Rock Line EP) - True - True 159421 - 0x289D1 (Rock Line Reflection EP) - True - True -Quarry Elevator (Quarry): +Quarry Elevator (Quarry) - Outside Quarry - 0x17CC4 - Quarry - 0x17CC4: 158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser 159403 - 0x17CB9 (Railroad EP) - 0x17CC4 - True @@ -230,28 +240,31 @@ Quarry Between Entrys (Quarry) - Quarry - 0x17C07: 158119 - 0x17C09 (Entry 2 Panel) - True - Shapers & Triangles Door - 0x17C07 (Entry 2) - 0x17C09 -Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010 - Quarry Elevator - 0x17CC4: +Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010: +159802 - 0xFFD01 (Inside Reached Independently) - True - True 158121 - 0x01E5A (Stoneworks Entry Left Panel) - True - Squares & Black/White Squares & Stars & Stars + Same Colored Symbol 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 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 Ground Floor - 0x03675 - Quarry Stoneworks Upper Floor - 0x03679: +Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - TrueOneWay: 158125 - 0x00E0C (Lower Row 1) - True - Triangles & Eraser 158126 - 0x01489 (Lower Row 2) - 0x00E0C - Triangles & Eraser 158127 - 0x0148A (Lower Row 3) - 0x01489 - Triangles & Eraser 158128 - 0x014D9 (Lower Row 4) - 0x0148A - Triangles & Eraser 158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Triangles & Eraser 158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Triangles & 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 Middle Floor - 0x03676 & 0x03679 - Quarry Stoneworks Ground Floor - 0x0368A: +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 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol @@ -262,7 +275,7 @@ Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Flo 158139 - 0x3C12D (Upper Row 6) - 0x0146C - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol 158140 - 0x03686 (Upper Row 7) - 0x3C12D - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol 158141 - 0x014E9 (Upper Row 8) - 0x03686 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol -158142 - 0x03677 (Stair Control) - True - Squares & Colored Squares & Eraser +158142 - 0x03677 (Stairs Panel) - True - Squares & Colored Squares & Eraser Door - 0x0368A (Stairs) - 0x03677 158143 - 0x3C125 (Control Room Left) - 0x014E9 - Squares & Black/White Squares & Dots & Full Dots & Eraser 158144 - 0x0367C (Control Room Right) - 0x014E9 - Squares & Colored Squares & Triangles & Eraser & Stars & Stars + Same Colored Symbol @@ -277,7 +290,7 @@ Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Fro Door - 0x2769B (Dock) - 0x17CA6 Door - 0x27163 (Dock Invis Barrier) - 0x17CA6 -Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 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 & Negative Shapers @@ -309,9 +322,9 @@ Door - 0x3865F (Second Barrier) - 0x38663 158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Stars & Eraser & Shapers & Negative Shapers & Stars + Same Colored Symbol 159401 - 0x005F6 (Hook EP) - 0x275FA & 0x03852 & 0x3865F - True -Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 & 0x19665: +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 +Door - 0x19B24 (Timed Door) - 0x334DB | 0x334DC 158171 - 0x0AC74 (Intro 6) - 0x0A8DC - True 158172 - 0x0AC7A (Intro 7) - 0x0AC74 - True 158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - True @@ -336,7 +349,7 @@ 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 & 0x334DC & 0x19B24 - 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 @@ -345,7 +358,7 @@ Door - 0x1855B (Ledge Barrier) - 0x0A8DC Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC Shadows Laser Room (Shadows): -158703 - 0x19650 (Laser Panel) - True - True +158703 - 0x19650 (Laser Panel) - 0x194B2 & 0x19665 - True Laser - 0x181B3 (Laser) - 0x19650 Treehouse Beach (Treehouse Beach) - Main Island - True: @@ -395,9 +408,9 @@ Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F 158205 - 0x09E49 (Shadows Shortcut Panel) - True - True Door - 0x09E3D (Shadows Shortcut) - 0x09E49 -Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: -158654 - 0x00AFB (Vault) - True - Symmetry & Sound Dots & Colored Dots -158655 - 0x03535 (Vault Box) - 0x00AFB - True +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 159220 - 0x03B22 (Circle Far EP) - True - True 159221 - 0x03B23 (Circle Left EP) - True - True @@ -407,6 +420,9 @@ Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: 159226 - 0x28ABE (Rope Outer EP) - True - True 159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True +Shipwreck Vault (Shipwreck): +158655 - 0x03535 (Vault Box) - True - True + Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut Panel) - True - True Door - 0x04F8F (Tower Shortcut) - 0x0361B @@ -422,8 +438,8 @@ Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159251 - 0x3348F (Hedges EP) - True - True Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: -158207 - 0x03713 (Shortcut Panel) - True - True -Door - 0x0364E (Shortcut) - 0x03713 +158207 - 0x03713 (Laser Shortcut Panel) - True - True +Door - 0x0364E (Laser Shortcut) - 0x03713 158208 - 0x00B10 (Entry Left) - True - True 158209 - 0x00C92 (Entry Right) - True - True Door - 0x0C128 (Entry Inner) - 0x00B10 @@ -454,7 +470,7 @@ Inside Monastery (Monastery): Monastery Garden (Monastery): -Town (Town) - Main Island - True - Boat - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: +Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: 158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat 158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Squares & Black/White Squares & Shapers & Triangles Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 @@ -469,11 +485,11 @@ Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Triangles & Dots & Full Dots 158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Triangles & Dots & Full Dots Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1 -158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers & Stars + Same Colored Symbol -Door - 0x28A61 (Tinted Glass Door) - 0x28A0D +158225 - 0x28998 (RGB House Entry Panel) - True - Stars & Rotated Shapers & Stars + Same Colored Symbol +Door - 0x28A61 (RGB House Entry) - 0x28A0D 158226 - 0x28A0D (Church Entry Panel) - 0x28998 - Stars Door - 0x03BB0 (Church Entry) - 0x03C08 -158228 - 0x28A79 (Maze Stair Control) - True - True +158228 - 0x28A79 (Maze Panel) - True - True Door - 0x28AA2 (Maze Stairs) - 0x28A79 158241 - 0x17F5F (Windmill Entry Panel) - True - Dots Door - 0x1845B (Windmill Entry) - 0x17F5F @@ -484,7 +500,7 @@ Door - 0x1845B (Windmill Entry) - 0x17F5F 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 & 0x03C0C & 0x03C08 - 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 @@ -557,7 +573,7 @@ Door - 0x3CCDF (Exit Right) - 0x33AB2 159556 - 0x33A2A (Door EP) - 0x03553 - True 159558 - 0x33B06 (Church EP) - 0x0354E - True -Jungle (Jungle) - Main Island - True - Outside Jungle River - 0x3873B - Boat - 0x17CDF: +Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF: 158251 - 0x17CDF (Shore Boat Spawn) - True - Boat 158609 - 0x17F9B (Discard) - True - Arrows 158252 - 0x002C4 (First Row 1) - True - True @@ -588,18 +604,21 @@ Door - 0x3873B (Laser Shortcut) - 0x337FA 159350 - 0x035CB (Bamboo CCW EP) - True - True 159351 - 0x035CF (Bamboo CW EP) - True - True -Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A: -158267 - 0x17CAA (Monastery Shortcut Panel) - True - True -Door - 0x0CF2A (Monastery Shortcut) - 0x17CAA -158663 - 0x15ADD (Vault) - True - Black/White Squares & Dots -158664 - 0x03702 (Vault Box) - 0x15ADD - True +Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A - River 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 159120 - 0x03BE2 (Monastery Garden Left EP) - 0x03750 - True 159121 - 0x03BE3 (Monastery Garden Right EP) - True - True 159122 - 0x0A409 (Monastery Wall EP) - True - True +River Vault (River): +158664 - 0x03702 (Vault Box) - True - True + Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4: -158268 - 0x17C2E (Entry Panel) - True - Squares & Black/White Squares & Colored Squares +158268 - 0x17C2E (Entry Panel) - True - Squares & Black/White Squares Door - 0x0C2A4 (Entry) - 0x17C2E Bunker (Bunker) - Bunker Glass Room - 0x17C79: @@ -616,9 +635,9 @@ Bunker (Bunker) - Bunker Glass Room - 0x17C79: Door - 0x17C79 (Tinted Glass Door) - 0x0A099 Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3: -158279 - 0x0A010 (Glass Room 1) - True - Squares & Colored Squares -158280 - 0x0A01B (Glass Room 2) - 0x0A010 - Squares & Colored Squares & Black/White Squares -158281 - 0x0A01F (Glass Room 3) - 0x0A01B - Squares & Colored Squares & Black/White Squares +158279 - 0x0A010 (Glass Room 1) - 0x17C79 - Squares & Colored Squares +158280 - 0x0A01B (Glass Room 2) - 0x17C79 & 0x0A010 - Squares & Colored Squares & Black/White Squares +158281 - 0x0A01F (Glass Room 3) - 0x17C79 & 0x0A01B - Squares & Colored Squares & Black/White Squares Door - 0x0C2A3 (UV Room Entry) - 0x0A01F Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D: @@ -631,7 +650,7 @@ Door - 0x0A08D (Elevator Room Entry) - 0x17E67 Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay: 159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True -Bunker Elevator (Bunker) - Bunker Laser Platform - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: +Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: 158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares Bunker Green Room (Bunker) - Bunker Elevator - TrueOneWay: @@ -676,7 +695,7 @@ Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat 158316 - 0x00990 (Platform Row 4) - 0x0098F - Rotated Shapers Door - 0x184B7 (Between Bridges First Door) - 0x00990 158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Rotated Shapers -158318 - 0x17C0E (Platform Shortcut Right Panel) - True - Rotated Shapers +158318 - 0x17C0E (Platform Shortcut Right Panel) - 0x17C0D - Rotated Shapers Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E Door - 0x04B7F (Cyan Water Pump) - 0x00006 @@ -715,18 +734,21 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 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 Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8: +158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Shapers & Dots & Full Dots 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers & Shapers & Dots & Full Dots 158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Shapers & Dots & Full Dots 158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Shapers & Dots & Full Dots -158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers Door - 0x18482 (Blue Water Pump) - 0x00E3A 159332 - 0x3365F (Boat EP) - 0x09DB8 - True 159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True -Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6: +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): @@ -752,7 +774,7 @@ Laser - 0x00BF6 (Laser) - 0x03615 158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Stars & Stars + Same Colored Symbol Door - 0x2D880 (Laser Shortcut) - 0x17C02 -Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309: +Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309 - The Ocean - 0x17C95: 158343 - 0x17C95 (Boat Spawn) - True - Boat 158344 - 0x0288C (First Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles Door - 0x0C309 (First Door) - 0x0288C @@ -778,7 +800,7 @@ Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181: 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 Control) - True - 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 & Full Dots @@ -802,7 +824,7 @@ Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2: 158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars & Stars + Same Colored Symbol & Triangles Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D: -158404 - 0x037FF (Bridge Control) - True - Stars +158404 - 0x037FF (Drawbridge Panel) - True - Stars Door - 0x0C32D (Drawbridge) - 0x037FF Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6: @@ -847,7 +869,7 @@ 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 & 0x17DEC +Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DEC | 0x17CBC Treehouse Laser Room Back Platform (Treehouse): 158611 - 0x17FA0 (Laser Discard) - True - Arrows @@ -860,19 +882,22 @@ Treehouse Laser Room (Treehouse): 158403 - 0x17CBC (Laser House Door Timer Inside) - True - True Laser - 0x028A4 (Laser) - 0x03613 -Mountainside (Mountainside) - Main Island - True - Mountaintop - True: +Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: 158612 - 0x17C42 (Discard) - True - Arrows -158665 - 0x002A6 (Vault) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol -158666 - 0x03542 (Vault Box) - 0x002A6 - True +158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol +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 Top Layer - 0x17C34: 158405 - 0x0042D (River Shape) - True - True 158406 - 0x09F7F (Box Short) - 7 Lasers - True -158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol & Triangles -158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True +158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol & Triangles +158800 - 0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True 159300 - 0x001A3 (River Shape EP) - True - True 159320 - 0x3370E (Arch Black EP) - True - True 159324 - 0x336C8 (Arch White Right EP) - True - True @@ -881,7 +906,7 @@ Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: Mountain Top Layer (Mountain Floor 1) - Mountain Top Layer Bridge - 0x09E39: 158408 - 0x09E39 (Light Bridge Controller) - True - Eraser & Triangles -Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: +Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Top Layer At Door - TrueOneWay: 158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots & Stars & Stars + Same Colored Symbol 158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Triangles 158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers & Stars & Stars + Same Colored Symbol @@ -899,6 +924,8 @@ Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: 158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Stars & Shapers & Stars + Same Colored Symbol 158424 - 0x09EAD (Trash Pillar 1) - True - Rotated Shapers & Stars 158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Rotated Shapers & Triangles + +Mountain Top Layer 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 Blue Bridge - 0x09E86 - Mountain Pink Bridge EP - TrueOneWay: @@ -917,7 +944,7 @@ Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2): 158431 - 0x09E86 (Light Bridge Controller Near) - True - Shapers & Dots -Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay: +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 - Triangles 158433 - 0x09FCE (Far Row 2) - 0x09FCC - Black/White Squares & Stars & Stars + Same Colored Symbol 158434 - 0x09FCF (Far Row 3) - 0x09FCE - Stars & Triangles & Stars + Same Colored Symbol @@ -935,29 +962,27 @@ Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Third Layer - 0x09EEB: 158439 - 0x09EEB (Elevator Control Panel) - True - Dots -Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89: +Mountain Third Layer (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 & Negative Shapers 158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser & Negative Shapers 158442 - 0x09F01 (Giant Puzzle Top Right) - True - Shapers & Eraser & Negative Shapers 158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser & Negative 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 Bottom Floor Rock - 0x17FA2 - Final Room - 0x0C141 - Mountain Pink Bridge EP - TrueOneWay: +Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Path to Caves - 0x17F33 - Final Room - 0x0C141: 158614 - 0x17FA2 (Discard) - 0xFFF00 - Arrows 158445 - 0x01983 (Final Room Entry Left) - True - Shapers & Stars 158446 - 0x01987 (Final Room Entry Right) - True - Squares & Colored Squares & Dots Door - 0x0C141 (Final Room Entry) - 0x01983 & 0x01987 -159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True -159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True +Door - 0x17F33 (Rock Open) - 0x17FA2 | 0x334E1 Mountain Pink Bridge EP (Mountain Floor 2): 159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True -Mountain Bottom Floor Rock (Mountain Bottom Floor) - Mountain Bottom Floor - 0x17F33 - Mountain Path to Caves - 0x17F33: -Door - 0x17F33 (Rock Open) - True - -Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x334E1 - Caves - 0x2D77D: +Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D: 158447 - 0x00FF8 (Caves Entry Panel) - True - Arrows & Black/White Squares Door - 0x2D77D (Caves Entry) - 0x00FF8 158448 - 0x334E1 (Rock Control) - True - True @@ -1021,31 +1046,34 @@ Path to Challenge (Caves) - Challenge - 0x0A19A: 158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Arrows & Stars + Same Colored Symbol Door - 0x0A19A (Challenge Entry) - 0x0A16E -Challenge (Challenge) - Tunnels - 0x0348A: +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 - Squares & Black/White Squares +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 - Squares & Black/White Squares -158509 - 0x00CA1 (Choice Squares Middle) - 0x00CD4 - Squares & Black/White Squares -158510 - 0x00C80 (Choice Squares Left) - 0x00CD4 - Squares & Black/White Squares -158511 - 0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares -158512 - 0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares -158513 - 0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares +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 - Squares & Black/White Squares & Symmetry -158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True +158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Black/White Squares & Symmetry +Door - 0x04D75 (Vault Door) - 0x1C31A & 0x1C319 158518 - 0x039B4 (Tunnels Entry Panel) - True - Arrows Door - 0x0348A (Tunnels Entry) - 0x039B4 159530 - 0x28B30 (Water EP) - True - True +Challenge Vault (Challenge): +158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True + Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87: 158668 - 0x2FAF6 (Vault Box) - True - True 158519 - 0x27732 (Theater Shortcut Panel) - True - True @@ -1075,7 +1103,7 @@ Elevator (Mountain Final Room): 158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True 158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True -Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: +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 @@ -1093,32 +1121,38 @@ Obelisks (EPs) - Entry - True: 159702 - 0xFFE02 (Desert Obelisk Side 3) - 0x3351D - True 159703 - 0xFFE03 (Desert Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True 159704 - 0xFFE04 (Desert Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True +159709 - 0x00359 (Desert Obelisk) - True - True 159710 - 0xFFE10 (Monastery Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True 159711 - 0xFFE11 (Monastery Obelisk Side 2) - 0x03AC5 - True 159712 - 0xFFE12 (Monastery Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True 159713 - 0xFFE13 (Monastery Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True 159714 - 0xFFE14 (Monastery Obelisk Side 5) - 0x03E01 - True 159715 - 0xFFE15 (Monastery Obelisk Side 6) - 0x289F4 & 0x289F5 - True +159719 - 0x00263 (Monastery Obelisk) - True - True 159720 - 0xFFE20 (Treehouse Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True 159721 - 0xFFE21 (Treehouse Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True 159722 - 0xFFE22 (Treehouse Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True 159723 - 0xFFE23 (Treehouse Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True 159724 - 0xFFE24 (Treehouse Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True 159725 - 0xFFE25 (Treehouse Obelisk Side 6) - 0x28AE9 & 0x3348F - True +159729 - 0x00097 (Treehouse Obelisk) - True - True 159730 - 0xFFE30 (River Obelisk Side 1) - 0x001A3 & 0x335AE - True 159731 - 0xFFE31 (River Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True 159732 - 0xFFE32 (River Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True 159733 - 0xFFE33 (River Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True 159734 - 0xFFE34 (River Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True 159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True +159739 - 0x00367 (River Obelisk) - True - True 159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True 159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True 159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True 159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True 159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True +159749 - 0x22073 (Quarry Obelisk) - True - True 159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True 159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True 159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True 159753 - 0xFFE53 (Town Obelisk Side 4) - 0x28B30 & 0x035C9 - True 159754 - 0xFFE54 (Town Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True 159755 - 0xFFE55 (Town Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True +159759 - 0x0A16C (Town Obelisk) - True - True diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/WitnessLogicVanilla.txt index 84e73e68a53c..719eae6c4e56 100644 --- a/worlds/witness/WitnessLogicVanilla.txt +++ b/worlds/witness/WitnessLogicVanilla.txt @@ -1,3 +1,5 @@ +Menu (Menu) - Entry - True: + Entry (Entry): First Hallway (First Hallway) - Entry - True - First Hallway Room - 0x00064: @@ -21,9 +23,9 @@ Tutorial (Tutorial) - Outside Tutorial - 0x03629: 159513 - 0x33600 (Patio Flowers EP) - 0x0C373 - True 159517 - 0x3352F (Gate EP) - 0x03505 - True -Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2: -158650 - 0x033D4 (Vault) - True - Dots & Black/White Squares -158651 - 0x03481 (Vault Box) - 0x033D4 - True +Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2 - Outside Tutorial Vault - 0x033D0: +158650 - 0x033D4 (Vault Panel) - True - Dots & Black/White Squares +Door - 0x033D0 (Vault Door) - 0x033D4 158013 - 0x0005D (Shed Row 1) - True - Dots 158014 - 0x0005E (Shed Row 2) - 0x0005D - Dots 158015 - 0x0005F (Shed Row 3) - 0x0005E - Dots @@ -44,6 +46,9 @@ Door - 0x03BA2 (Outpost Path) - 0x0A3B5 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 Door - 0x0A170 (Outpost Entry) - 0x0A171 @@ -54,6 +59,7 @@ Door - 0x04CA3 (Outpost Exit) - 0x04CA4 158600 - 0x17CFB (Discard) - True - Triangles Main Island (Main Island) - Outside Tutorial - True: +159801 - 0xFFD00 (Reached Independently) - True - True 159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29: @@ -76,7 +82,7 @@ Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0 158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry Door - 0x0D7ED (Back Wall) - 0x0005C -Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8: +Inside Glass Factory Behind Back Wall (Glass Factory) - The Ocean - 0x17CC8: 158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E: @@ -112,12 +118,12 @@ Door - 0x18269 (Upper) - 0x1C349 159000 - 0x0332B (Glass Factory Black Line Reflection EP) - True - True Symmetry Island Upper (Symmetry Island): -158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots -158066 - 0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots -158067 - 0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots -158068 - 0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots -158069 - 0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots -158070 - 0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots +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 @@ -135,9 +141,9 @@ Door - 0x03313 (Second Gate) - 0x032FF Orchard End (Orchard): -Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE: -158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots -158653 - 0x0339E (Vault Box) - 0x0CC7B - True +Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE - Desert Vault - 0x03444: +158652 - 0x0CC7B (Vault Panel) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots +Door - 0x03444 (Vault Door) - 0x0CC7B 158602 - 0x17CE7 (Discard) - True - Triangles 158076 - 0x00698 (Surface 1) - True - True 158077 - 0x0048F (Surface 2) - 0x00698 - True @@ -163,6 +169,9 @@ Laser - 0x012FB (Laser) - 0x03608 159040 - 0x334B9 (Shore EP) - True - True 159041 - 0x334BC (Island EP) - True - True +Desert Vault (Desert): +158653 - 0x0339E (Vault Box) - True - True + Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3: 158087 - 0x09FAA (Light Control) - True - True 158088 - 0x00422 (Light Room 1) - 0x09FAA - True @@ -199,18 +208,19 @@ Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316: Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True -Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB: +Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x01317: 158111 - 0x17C31 (Final Transparent) - True - True 158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True 158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True 158115 - 0x0A15C (Final Bent 1) - True - True 158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True 158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True -159035 - 0x037BB (Elevator EP) - 0x012FB - True +159035 - 0x037BB (Elevator EP) - 0x01317 - True +Door - 0x01317 (Elevator) - 0x03608 Desert Lowest Level Inbetween Shortcuts (Desert): -Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - TrueOneWay: +Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - 0xFFD00 & 0xFFD01: 158118 - 0x09E57 (Entry 1 Panel) - True - Black/White Squares 158603 - 0x17CF0 (Discard) - True - Triangles 158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Shapers @@ -222,7 +232,7 @@ Door - 0x09D6F (Entry 1) - 0x09E57 159420 - 0x289CF (Rock Line EP) - True - True 159421 - 0x289D1 (Rock Line Reflection EP) - True - True -Quarry Elevator (Quarry): +Quarry Elevator (Quarry) - Outside Quarry - 0x17CC4 - Quarry - 0x17CC4: 158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser 159403 - 0x17CB9 (Railroad EP) - 0x17CC4 - True @@ -230,28 +240,31 @@ Quarry Between Entrys (Quarry) - Quarry - 0x17C07: 158119 - 0x17C09 (Entry 2 Panel) - True - Shapers Door - 0x17C07 (Entry 2) - 0x17C09 -Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010 - Quarry Elevator - 0x17CC4: +Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010: +159802 - 0xFFD01 (Inside Reached Independently) - True - True 158121 - 0x01E5A (Stoneworks Entry Left Panel) - True - Black/White Squares 158122 - 0x01E59 (Stoneworks Entry Right Panel) - True - Dots Door - 0x02010 (Stoneworks Entry) - 0x01E59 & 0x01E5A -Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8: +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 Ground Floor - 0x03675 - Quarry Stoneworks Upper Floor - 0x03679: +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 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 Middle Floor - 0x03676 & 0x03679 - Quarry Stoneworks Ground Floor - 0x0368A: +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 @@ -262,7 +275,7 @@ Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Flo 158139 - 0x3C12D (Upper Row 6) - 0x0146C - Colored Squares & Eraser 158140 - 0x03686 (Upper Row 7) - 0x3C12D - Colored Squares & Eraser 158141 - 0x014E9 (Upper Row 8) - 0x03686 - Colored Squares & Eraser -158142 - 0x03677 (Stair Control) - True - Colored Squares & Eraser +158142 - 0x03677 (Stairs Panel) - True - Colored Squares & Eraser Door - 0x0368A (Stairs) - 0x03677 158143 - 0x3C125 (Control Room Left) - 0x014E9 - Black/White Squares & Dots & Eraser 158144 - 0x0367C (Control Room Right) - 0x014E9 - Colored Squares & Dots & Eraser @@ -277,7 +290,7 @@ Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Fro Door - 0x2769B (Dock) - 0x17CA6 Door - 0x27163 (Dock Invis Barrier) - 0x17CA6 -Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 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 @@ -309,9 +322,9 @@ Door - 0x3865F (Second Barrier) - 0x38663 158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Stars & Eraser & Shapers 159401 - 0x005F6 (Hook EP) - 0x275FA & 0x03852 & 0x3865F - True -Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 & 0x19665: +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 +Door - 0x19B24 (Timed Door) - 0x334DB | 0x334DC 158171 - 0x0AC74 (Intro 6) - 0x0A8DC - True 158172 - 0x0AC7A (Intro 7) - 0x0AC74 - True 158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - True @@ -336,7 +349,7 @@ 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 & 0x334DC & 0x19B24 - 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 @@ -345,7 +358,7 @@ Door - 0x1855B (Ledge Barrier) - 0x0A8DC Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC Shadows Laser Room (Shadows): -158703 - 0x19650 (Laser Panel) - True - True +158703 - 0x19650 (Laser Panel) - 0x194B2 & 0x19665 - True Laser - 0x181B3 (Laser) - 0x19650 Treehouse Beach (Treehouse Beach) - Main Island - True: @@ -395,9 +408,9 @@ Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F 158205 - 0x09E49 (Shadows Shortcut Panel) - True - True Door - 0x09E3D (Shadows Shortcut) - 0x09E49 -Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: -158654 - 0x00AFB (Vault) - True - Symmetry & Sound Dots & Colored Dots -158655 - 0x03535 (Vault Box) - 0x00AFB - True +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 - Triangles 159220 - 0x03B22 (Circle Far EP) - True - True 159221 - 0x03B23 (Circle Left EP) - True - True @@ -407,6 +420,9 @@ Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: 159226 - 0x28ABE (Rope Outer EP) - True - True 159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True +Shipwreck Vault (Shipwreck): +158655 - 0x03535 (Vault Box) - True - True + Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut Panel) - True - True Door - 0x04F8F (Tower Shortcut) - 0x0361B @@ -422,8 +438,8 @@ Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159251 - 0x3348F (Hedges EP) - True - True Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: -158207 - 0x03713 (Shortcut Panel) - True - True -Door - 0x0364E (Shortcut) - 0x03713 +158207 - 0x03713 (Laser Shortcut Panel) - True - True +Door - 0x0364E (Laser Shortcut) - 0x03713 158208 - 0x00B10 (Entry Left) - True - True 158209 - 0x00C92 (Entry Right) - True - True Door - 0x0C128 (Entry Inner) - 0x00B10 @@ -454,7 +470,7 @@ Inside Monastery (Monastery): Monastery Garden (Monastery): -Town (Town) - Main Island - True - Boat - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: +Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: 158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat 158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Black/White Squares & Shapers Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 @@ -469,11 +485,11 @@ Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 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 Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1 -158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers -Door - 0x28A61 (Tinted Glass Door) - 0x28998 +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 Stair Control) - True - True +158228 - 0x28A79 (Maze Panel) - True - True Door - 0x28AA2 (Maze Stairs) - 0x28A79 158241 - 0x17F5F (Windmill Entry Panel) - True - Dots Door - 0x1845B (Windmill Entry) - 0x17F5F @@ -557,7 +573,7 @@ Door - 0x3CCDF (Exit Right) - 0x33AB2 159556 - 0x33A2A (Door EP) - 0x03553 - True 159558 - 0x33B06 (Church EP) - 0x0354E - True -Jungle (Jungle) - Main Island - True - Outside Jungle River - 0x3873B - Boat - 0x17CDF: +Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF: 158251 - 0x17CDF (Shore Boat Spawn) - True - Boat 158609 - 0x17F9B (Discard) - True - Triangles 158252 - 0x002C4 (First Row 1) - True - True @@ -588,18 +604,21 @@ Door - 0x3873B (Laser Shortcut) - 0x337FA 159350 - 0x035CB (Bamboo CCW EP) - True - True 159351 - 0x035CF (Bamboo CW EP) - True - True -Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A: -158267 - 0x17CAA (Monastery Shortcut Panel) - True - True -Door - 0x0CF2A (Monastery Shortcut) - 0x17CAA -158663 - 0x15ADD (Vault) - True - Black/White Squares & Dots -158664 - 0x03702 (Vault Box) - 0x15ADD - True +Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A - River 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 159120 - 0x03BE2 (Monastery Garden Left EP) - 0x03750 - True 159121 - 0x03BE3 (Monastery Garden Right EP) - True - True 159122 - 0x0A409 (Monastery Wall EP) - True - True +River Vault (River): +158664 - 0x03702 (Vault Box) - True - True + Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4: -158268 - 0x17C2E (Entry Panel) - True - Black/White Squares & Colored Squares +158268 - 0x17C2E (Entry Panel) - True - Black/White Squares Door - 0x0C2A4 (Entry) - 0x17C2E Bunker (Bunker) - Bunker Glass Room - 0x17C79: @@ -616,9 +635,9 @@ Bunker (Bunker) - Bunker Glass Room - 0x17C79: Door - 0x17C79 (Tinted Glass Door) - 0x0A099 Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3: -158279 - 0x0A010 (Glass Room 1) - True - Colored Squares -158280 - 0x0A01B (Glass Room 2) - 0x0A010 - Colored Squares & Black/White Squares -158281 - 0x0A01F (Glass Room 3) - 0x0A01B - Colored Squares & Black/White Squares +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: @@ -631,7 +650,7 @@ Door - 0x0A08D (Elevator Room Entry) - 0x17E67 Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay: 159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True -Bunker Elevator (Bunker) - Bunker Laser Platform - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: +Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: 158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares Bunker Green Room (Bunker) - Bunker Elevator - TrueOneWay: @@ -676,7 +695,7 @@ Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat 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) - True - Shapers +158318 - 0x17C0E (Platform Shortcut Right Panel) - 0x17C0D - Shapers Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E Door - 0x04B7F (Cyan Water Pump) - 0x00006 @@ -715,18 +734,21 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 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 Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8: +158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers 158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Rotated Shapers & Shapers 158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers -158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers Door - 0x18482 (Blue Water Pump) - 0x00E3A 159332 - 0x3365F (Boat EP) - 0x09DB8 - True 159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True -Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6: +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): @@ -752,7 +774,7 @@ Laser - 0x00BF6 (Laser) - 0x03615 158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers Door - 0x2D880 (Laser Shortcut) - 0x17C02 -Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309: +Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309 - The Ocean - 0x17C95: 158343 - 0x17C95 (Boat Spawn) - True - Boat 158344 - 0x0288C (First Door Panel) - True - Stars Door - 0x0C309 (First Door) - 0x0288C @@ -778,7 +800,7 @@ Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181: 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 Control) - True - 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 @@ -802,7 +824,7 @@ Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2: 158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D: -158404 - 0x037FF (Bridge Control) - True - Stars +158404 - 0x037FF (Drawbridge Panel) - True - Stars Door - 0x0C32D (Drawbridge) - 0x037FF Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6: @@ -847,7 +869,7 @@ 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 +Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DDB | 0x17CBC Treehouse Laser Room Back Platform (Treehouse): 158611 - 0x17FA0 (Laser Discard) - True - Triangles @@ -860,19 +882,22 @@ Treehouse Laser Room (Treehouse): 158403 - 0x17CBC (Laser House Door Timer Inside) - True - True Laser - 0x028A4 (Laser) - 0x03613 -Mountainside (Mountainside) - Main Island - True - Mountaintop - True: +Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: 158612 - 0x17C42 (Discard) - True - Triangles -158665 - 0x002A6 (Vault) - True - Symmetry & Colored Dots & Black/White Squares -158666 - 0x03542 (Vault Box) - 0x002A6 - True +158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares +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 Top Layer - 0x17C34: 158405 - 0x0042D (River Shape) - True - True 158406 - 0x09F7F (Box Short) - 7 Lasers - True -158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Black/White Squares -158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True +158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Black/White Squares +158800 - 0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True 159300 - 0x001A3 (River Shape EP) - True - True 159320 - 0x3370E (Arch Black EP) - True - True 159324 - 0x336C8 (Arch White Right EP) - True - True @@ -881,7 +906,7 @@ Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: Mountain Top Layer (Mountain Floor 1) - Mountain Top Layer Bridge - 0x09E39: 158408 - 0x09E39 (Light Bridge Controller) - True - Black/White Squares & Rotated Shapers -Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: +Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Top Layer At Door - TrueOneWay: 158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots 158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Dots 158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers @@ -899,6 +924,8 @@ Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: 158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Dots 158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers 158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shapers + +Mountain Top Layer 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 Blue Bridge - 0x09E86 - Mountain Pink Bridge EP - TrueOneWay: @@ -917,7 +944,7 @@ 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 & Black/White Squares -Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay: +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 - Dots 158433 - 0x09FCE (Far Row 2) - 0x09FCC - Black/White Squares 158434 - 0x09FCF (Far Row 3) - 0x09FCE - Shapers @@ -935,29 +962,27 @@ Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Third Layer - 0x09EEB: 158439 - 0x09EEB (Elevator Control Panel) - True - Dots -Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89: +Mountain Third Layer (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 - Rotated Shapers & Eraser 158442 - 0x09F01 (Giant Puzzle Top Right) - True - Shapers & Eraser 158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser 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 Bottom Floor Rock - 0x17FA2 - Final Room - 0x0C141 - Mountain Pink Bridge EP - TrueOneWay: +Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Path to Caves - 0x17F33 - Final Room - 0x0C141: 158614 - 0x17FA2 (Discard) - 0xFFF00 - Triangles 158445 - 0x01983 (Final Room Entry Left) - True - Shapers & Stars 158446 - 0x01987 (Final Room Entry Right) - True - Colored Squares & Dots Door - 0x0C141 (Final Room Entry) - 0x01983 & 0x01987 -159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True -159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True +Door - 0x17F33 (Rock Open) - 0x17FA2 | 0x334E1 Mountain Pink Bridge EP (Mountain Floor 2): 159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True -Mountain Bottom Floor Rock (Mountain Bottom Floor) - Mountain Bottom Floor - 0x17F33 - Mountain Path to Caves - 0x17F33: -Door - 0x17F33 (Rock Open) - True - True - -Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x334E1 - Caves - 0x2D77D: +Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D: 158447 - 0x00FF8 (Caves Entry Panel) - True - Black/White Squares Door - 0x2D77D (Caves Entry) - 0x00FF8 158448 - 0x334E1 (Rock Control) - True - True @@ -1021,7 +1046,7 @@ Path to Challenge (Caves) - Challenge - 0x0A19A: 158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Shapers & Stars + Same Colored Symbol Door - 0x0A19A (Challenge Entry) - 0x0A16E -Challenge (Challenge) - Tunnels - 0x0348A: +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 @@ -1041,11 +1066,14 @@ Challenge (Challenge) - Tunnels - 0x0348A: 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 -158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True +Door - 0x04D75 (Vault Door) - 0x1C31A & 0x1C319 158518 - 0x039B4 (Tunnels Entry Panel) - True - Triangles Door - 0x0348A (Tunnels Entry) - 0x039B4 159530 - 0x28B30 (Water EP) - True - True +Challenge Vault (Challenge): +158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True + Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87: 158668 - 0x2FAF6 (Vault Box) - True - True 158519 - 0x27732 (Theater Shortcut Panel) - True - True @@ -1075,7 +1103,7 @@ Elevator (Mountain Final Room): 158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True 158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True -Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: +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 @@ -1093,32 +1121,38 @@ Obelisks (EPs) - Entry - True: 159702 - 0xFFE02 (Desert Obelisk Side 3) - 0x3351D - True 159703 - 0xFFE03 (Desert Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True 159704 - 0xFFE04 (Desert Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True +159709 - 0x00359 (Desert Obelisk) - True - True 159710 - 0xFFE10 (Monastery Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True 159711 - 0xFFE11 (Monastery Obelisk Side 2) - 0x03AC5 - True 159712 - 0xFFE12 (Monastery Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True 159713 - 0xFFE13 (Monastery Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True 159714 - 0xFFE14 (Monastery Obelisk Side 5) - 0x03E01 - True 159715 - 0xFFE15 (Monastery Obelisk Side 6) - 0x289F4 & 0x289F5 - True +159719 - 0x00263 (Monastery Obelisk) - True - True 159720 - 0xFFE20 (Treehouse Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True 159721 - 0xFFE21 (Treehouse Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True 159722 - 0xFFE22 (Treehouse Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True 159723 - 0xFFE23 (Treehouse Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True 159724 - 0xFFE24 (Treehouse Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True 159725 - 0xFFE25 (Treehouse Obelisk Side 6) - 0x28AE9 & 0x3348F - True +159729 - 0x00097 (Treehouse Obelisk) - True - True 159730 - 0xFFE30 (River Obelisk Side 1) - 0x001A3 & 0x335AE - True 159731 - 0xFFE31 (River Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True 159732 - 0xFFE32 (River Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True 159733 - 0xFFE33 (River Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True 159734 - 0xFFE34 (River Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True 159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True +159739 - 0x00367 (River Obelisk) - True - True 159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True 159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True 159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True 159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True 159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True +159749 - 0x22073 (Quarry Obelisk) - True - True 159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True 159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True 159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True 159753 - 0xFFE53 (Town Obelisk Side 4) - 0x28B30 & 0x035C9 - True 159754 - 0xFFE54 (Town Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True 159755 - 0xFFE55 (Town Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True +159759 - 0x0A16C (Town Obelisk) - True - True diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 28eaba6404b6..c2d2311c1537 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -1,9 +1,11 @@ """ Archipelago init file for The Witness """ +import dataclasses from typing import Dict, Optional -from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial +from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial, CollectionState +from Options import PerGameCommonOptions, Toggle from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \ get_priority_hint_items, make_hints, generate_joke_hints from worlds.AutoWorld import World, WebWorld @@ -11,9 +13,9 @@ from .static_logic import StaticWitnessLogic from .locations import WitnessPlayerLocations, StaticWitnessLocations from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems, ItemData -from .rules import set_rules from .regions import WitnessRegions -from .Options import is_option_enabled, the_witness_options, get_option_value +from .rules import set_rules +from .Options import TheWitnessOptions from .utils import get_audio_logs from logging import warning, error @@ -38,13 +40,15 @@ class WitnessWorld(World): """ game = "The Witness" topology_present = False - data_version = 13 + data_version = 14 StaticWitnessLogic() StaticWitnessLocations() StaticWitnessItems() web = WitnessWebWorld() - option_definitions = the_witness_options + + options_dataclass = TheWitnessOptions + options: TheWitnessOptions item_name_to_id = { name: data.ap_code for name, data in StaticWitnessItems.item_data.items() @@ -52,7 +56,7 @@ class WitnessWorld(World): location_name_to_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID item_name_groups = StaticWitnessItems.item_groups - required_client_version = (0, 3, 9) + required_client_version = (0, 4, 4) def __init__(self, multiworld: "MultiWorld", player: int): super().__init__(multiworld, player) @@ -64,6 +68,9 @@ def __init__(self, multiworld: "MultiWorld", player: int): self.log_ids_to_hints = None + self.items_placed_early = [] + self.own_itempool = [] + def _get_slot_data(self): return { 'seed': self.random.randrange(0, 1000000), @@ -72,12 +79,11 @@ def _get_slot_data(self): 'item_id_to_door_hexes': StaticWitnessItems.get_item_to_door_mappings(), 'door_hexes_in_the_pool': self.items.get_door_ids_in_pool(), 'symbols_not_in_the_game': self.items.get_symbol_ids_not_in_pool(), - 'disabled_panels': list(self.player_logic.COMPLETELY_DISABLED_CHECKS), + 'disabled_entities': [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES], 'log_ids_to_hints': self.log_ids_to_hints, 'progressive_item_lists': self.items.get_progressive_item_ids_in_pool(), 'obelisk_side_id_to_EPs': StaticWitnessLogic.OBELISK_SIDE_ID_TO_EP_HEXES, - 'precompleted_puzzles': [int(h, 16) for h in - self.player_logic.EXCLUDED_LOCATIONS | self.player_logic.PRECOMPLETED_LOCATIONS], + 'precompleted_puzzles': [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS], 'entity_to_name': StaticWitnessLogic.ENTITY_ID_TO_NAME, } @@ -85,36 +91,125 @@ def generate_early(self): disabled_locations = self.multiworld.exclude_locations[self.player].value self.player_logic = WitnessPlayerLogic( - self.multiworld, self.player, disabled_locations, self.multiworld.start_inventory[self.player].value + self, disabled_locations, self.multiworld.start_inventory[self.player].value ) - self.locat: WitnessPlayerLocations = WitnessPlayerLocations(self.multiworld, self.player, self.player_logic) - self.items: WitnessPlayerItems = WitnessPlayerItems(self.multiworld, self.player, self.player_logic, self.locat) - self.regio: WitnessRegions = WitnessRegions(self.locat) + self.locat: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic) + self.items: WitnessPlayerItems = WitnessPlayerItems( + self, self.player_logic, self.locat + ) + self.regio: WitnessRegions = WitnessRegions(self.locat, self) self.log_ids_to_hints = dict() - if not (is_option_enabled(self.multiworld, self.player, "shuffle_symbols") - or get_option_value(self.multiworld, self.player, "shuffle_doors") - or is_option_enabled(self.multiworld, self.player, "shuffle_lasers")): + if not (self.options.shuffle_symbols or self.options.shuffle_doors or self.options.shuffle_lasers): if self.multiworld.players == 1: - warning("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle, Door" - " Shuffle or Laser Shuffle if that doesn't seem right.") + warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression" + f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't" + f" seem right.") else: - raise Exception("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle," - " Door Shuffle or Laser Shuffle.") + raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any" + f" progression items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle.") def create_regions(self): - self.regio.create_regions(self.multiworld, self.player, self.player_logic) + self.regio.create_regions(self, self.player_logic) - def create_items(self): + # Set rules early so extra locations can be created based on the results of exploring collection states + + set_rules(self) + + # Add event items and tie them to event locations (e.g. laser activations). + + event_locations = [] + + for event_location in self.locat.EVENT_LOCATION_TABLE: + item_obj = self.create_item( + self.player_logic.EVENT_ITEM_PAIRS[event_location] + ) + location_obj = self.multiworld.get_location(event_location, self.player) + location_obj.place_locked_item(item_obj) + self.own_itempool.append(item_obj) + + event_locations.append(location_obj) + + # Place other locked items + dog_puzzle_skip = self.create_item("Puzzle Skip") + self.multiworld.get_location("Town Pet the Dog", self.player).place_locked_item(dog_puzzle_skip) + + self.own_itempool.append(dog_puzzle_skip) + + self.items_placed_early.append("Puzzle Skip") + + # Pick an early item to place on the tutorial gate. + early_items = [item for item in self.items.get_early_items() if item in self.items.get_mandatory_items()] + if early_items: + random_early_item = self.multiworld.random.choice(early_items) + if self.options.puzzle_randomization == 1: + # In Expert, only tag the item as early, rather than forcing it onto the gate. + 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. + gate_item = self.create_item(random_early_item) + self.multiworld.get_location("Tutorial Gate Open", self.player).place_locked_item(gate_item) + self.own_itempool.append(gate_item) + self.items_placed_early.append(random_early_item) + + # There are some really restrictive settings in The Witness. + # They are rarely played, but when they are, we add some extra sphere 1 locations. + # This is done both to prevent generation failures, but also to make the early game less linear. + # Only sweeps for events because having this behavior be random based on Tutorial Gate would be strange. + + state = CollectionState(self.multiworld) + state.sweep_for_events(locations=event_locations) + + num_early_locs = sum(1 for loc in self.multiworld.get_reachable_locations(state, self.player) if loc.address) + + # Adjust the needed size for sphere 1 based on how restrictive the settings are in terms of items - # Determine pool size. Note that the dog location is included in the location list, so this needs to be -1. - pool_size: int = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) - 1 + needed_size = 3 + needed_size += self.options.puzzle_randomization == 1 + needed_size += self.options.shuffle_symbols + needed_size += self.options.shuffle_doors > 0 + + # Then, add checks in order until the required amount of sphere 1 checks is met. + + extra_checks = [ + ("First Hallway Room", "First Hallway Bend"), + ("First Hallway", "First Hallway Straight"), + ("Desert Outside", "Desert Surface 3"), + ] + + for i in range(num_early_locs, needed_size): + if not extra_checks: + break + + region, loc = extra_checks.pop(0) + self.locat.add_location_late(loc) + self.multiworld.get_region(region, self.player).add_locations({loc: self.location_name_to_id[loc]}) + + player = self.multiworld.get_player_name(self.player) + + warning(f"""Location "{loc}" had to be added to {player}'s world due to insufficient sphere 1 size.""") + + def create_items(self): + # Determine pool size. + pool_size: int = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) # Fill mandatory items and remove precollected and/or starting items from the pool. item_pool: Dict[str, int] = self.items.get_mandatory_items() + # Remove one copy of each item that was placed early + for already_placed in self.items_placed_early: + pool_size -= 1 + + if already_placed not in item_pool: + continue + + if item_pool[already_placed] == 1: + item_pool.pop(already_placed) + else: + item_pool[already_placed] -= 1 + for precollected_item_name in [item.name for item in self.multiworld.precollected_items[self.player]]: if precollected_item_name in item_pool: if item_pool[precollected_item_name] == 1: @@ -131,17 +226,18 @@ def create_items(self): self.multiworld.push_precollected(self.create_item(inventory_item_name)) if len(item_pool) > pool_size: - error_string = "The Witness world has too few locations ({num_loc}) to place its necessary items " \ - "({num_item})." - error(error_string.format(num_loc=pool_size, num_item=len(item_pool))) + error(f"{self.multiworld.get_player_name(self.player)}'s Witness world has too few locations ({pool_size})" + f" to place its necessary items ({len(item_pool)}).") return remaining_item_slots = pool_size - sum(item_pool.values()) # Add puzzle skips. - num_puzzle_skips = get_option_value(self.multiworld, self.player, "puzzle_skip_amount") + num_puzzle_skips = self.options.puzzle_skip_amount + if num_puzzle_skips > remaining_item_slots: - warning(f"The Witness world has insufficient locations to place all requested puzzle skips.") + warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations" + f" to place all requested puzzle skips.") num_puzzle_skips = remaining_item_slots item_pool["Puzzle Skip"] = num_puzzle_skips remaining_item_slots -= num_puzzle_skips @@ -150,45 +246,17 @@ def create_items(self): if remaining_item_slots > 0: item_pool.update(self.items.get_filler_items(remaining_item_slots)) - # Add event items and tie them to event locations (e.g. laser activations). - for event_location in self.locat.EVENT_LOCATION_TABLE: - item_obj = self.create_item( - self.player_logic.EVENT_ITEM_PAIRS[event_location] - ) - location_obj = self.multiworld.get_location(event_location, self.player) - location_obj.place_locked_item(item_obj) - - # BAD DOG GET BACK HERE WITH THAT PUZZLE SKIP YOU'RE POLLUTING THE ITEM POOL - self.multiworld.get_location("Town Pet the Dog", self.player)\ - .place_locked_item(self.create_item("Puzzle Skip")) - - # Pick an early item to place on the tutorial gate. - early_items = [item for item in self.items.get_early_items() if item in item_pool] - if early_items: - random_early_item = self.multiworld.random.choice(early_items) - if get_option_value(self.multiworld, self.player, "puzzle_randomization") == 1: - # In Expert, only tag the item as early, rather than forcing it onto the gate. - 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. - self.multiworld.get_location("Tutorial Gate Open", self.player)\ - .place_locked_item(self.create_item(random_early_item)) - if item_pool[random_early_item] == 1: - item_pool.pop(random_early_item) - else: - item_pool[random_early_item] -= 1 - # Generate the actual items. for item_name, quantity in sorted(item_pool.items()): - self.multiworld.itempool += [self.create_item(item_name) for _ in range(0, quantity)] + new_items = [self.create_item(item_name) for _ in range(0, quantity)] + + self.own_itempool += new_items + self.multiworld.itempool += new_items if self.items.item_data[item_name].local_only: self.multiworld.local_items[self.player].value.add(item_name) - def set_rules(self): - set_rules(self.multiworld, self.player, self.player_logic, self.locat) - def fill_slot_data(self) -> dict: - hint_amount = get_option_value(self.multiworld, self.player, "hint_amount") + hint_amount = self.options.hint_amount.value credits_hint = ( "This Randomizer is brought to you by", @@ -199,9 +267,9 @@ def fill_slot_data(self) -> dict: audio_logs = get_audio_logs().copy() if hint_amount != 0: - generated_hints = make_hints(self.multiworld, self.player, hint_amount) + generated_hints = make_hints(self, hint_amount, self.own_itempool) - self.multiworld.per_slot_randoms[self.player].shuffle(audio_logs) + self.random.shuffle(audio_logs) duplicates = min(3, len(audio_logs) // hint_amount) @@ -216,7 +284,7 @@ def fill_slot_data(self) -> dict: audio_log = audio_logs.pop() self.log_ids_to_hints[int(audio_log, 16)] = credits_hint - joke_hints = generate_joke_hints(self.multiworld, self.player, len(audio_logs)) + joke_hints = generate_joke_hints(self, len(audio_logs)) while audio_logs: audio_log = audio_logs.pop() @@ -226,10 +294,10 @@ def fill_slot_data(self) -> dict: slot_data = self._get_slot_data() - for option_name in the_witness_options: - slot_data[option_name] = get_option_value( - self.multiworld, self.player, option_name - ) + for option_name in (attr.name for attr in dataclasses.fields(TheWitnessOptions) + if attr not in dataclasses.fields(PerGameCommonOptions)): + option = getattr(self.options, option_name) + slot_data[option_name] = bool(option.value) if isinstance(option, Toggle) else option.value return slot_data @@ -257,36 +325,35 @@ class WitnessLocation(Location): Archipelago Location for The Witness """ game: str = "The Witness" - check_hex: int = -1 + entity_hex: int = -1 def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1): super().__init__(player, name, address, parent) - self.check_hex = ch_hex + self.entity_hex = ch_hex -def create_region(world: MultiWorld, player: int, name: str, - locat: WitnessPlayerLocations, region_locations=None, exits=None): +def create_region(world: WitnessWorld, name: str, locat: WitnessPlayerLocations, region_locations=None, exits=None): """ Create an Archipelago Region for The Witness """ - ret = Region(name, player, world) + ret = Region(name, world.player, world.multiworld) if region_locations: for location in region_locations: loc_id = locat.CHECK_LOCATION_TABLE[location] - check_hex = -1 - if location in StaticWitnessLogic.CHECKS_BY_NAME: - check_hex = int( - StaticWitnessLogic.CHECKS_BY_NAME[location]["checkHex"], 0 + entity_hex = -1 + if location in StaticWitnessLogic.ENTITIES_BY_NAME: + entity_hex = int( + StaticWitnessLogic.ENTITIES_BY_NAME[location]["entity_hex"], 0 ) location = WitnessLocation( - player, location, loc_id, ret, check_hex + world.player, location, loc_id, ret, entity_hex ) ret.locations.append(location) if exits: for single_exit in exits: - ret.exits.append(Entrance(player, single_exit, ret)) + ret.exits.append(Entrance(world.player, single_exit, ret)) return ret diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 8a9dab54bc18..24302f0c6724 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -1,5 +1,9 @@ -from BaseClasses import MultiWorld -from .Options import is_option_enabled, get_option_value +from typing import Tuple, List, TYPE_CHECKING + +from BaseClasses import Item + +if TYPE_CHECKING: + from . import WitnessWorld joke_hints = [ "Quaternions break my brain", @@ -113,16 +117,16 @@ ] -def get_always_hint_items(multiworld: MultiWorld, player: int): +def get_always_hint_items(world: "WitnessWorld"): always = [ "Boat", - "Caves Exits to Main Island", + "Caves Shortcuts", "Progressive Dots", ] - difficulty = get_option_value(multiworld, player, "puzzle_randomization") - discards = is_option_enabled(multiworld, player, "shuffle_discarded_panels") - wincon = get_option_value(multiworld, player, "victory_condition") + difficulty = world.options.puzzle_randomization + discards = world.options.shuffle_discarded_panels + wincon = world.options.victory_condition if discards: if difficulty == 1: @@ -131,12 +135,15 @@ def get_always_hint_items(multiworld: MultiWorld, player: int): always.append("Triangles") if wincon == 0: - always.append("Mountain Bottom Floor Final Room Entry (Door)") + always += ["Mountain Bottom Floor Final Room Entry (Door)", "Mountain Bottom Floor Doors"] + + if wincon == 1: + always += ["Challenge Entry (Panel)", "Caves Panels"] return always -def get_always_hint_locations(multiworld: MultiWorld, player: int): +def get_always_hint_locations(_: "WitnessWorld"): return { "Challenge Vault Box", "Mountain Bottom Floor Discard", @@ -146,19 +153,34 @@ def get_always_hint_locations(multiworld: MultiWorld, player: int): } -def get_priority_hint_items(multiworld: MultiWorld, player: int): +def get_priority_hint_items(world: "WitnessWorld"): priority = { "Caves Mountain Shortcut (Door)", "Caves Swamp Shortcut (Door)", - "Negative Shapers", - "Sound Dots", - "Colored Dots", - "Stars + Same Colored Symbol", "Swamp Entry (Panel)", "Swamp Laser Shortcut (Door)", } - if is_option_enabled(multiworld, player, "shuffle_lasers"): + if world.options.shuffle_symbols: + symbols = [ + "Progressive Dots", + "Progressive Stars", + "Shapers", + "Rotated Shapers", + "Negative Shapers", + "Arrows", + "Triangles", + "Eraser", + "Black/White Squares", + "Colored Squares", + "Colored Dots", + "Sound Dots", + "Symmetry" + ] + + priority.update(world.random.sample(symbols, 5)) + + if world.options.shuffle_lasers: lasers = [ "Symmetry Laser", "Town Laser", @@ -172,18 +194,18 @@ def get_priority_hint_items(multiworld: MultiWorld, player: int): "Shadows Laser", ] - if get_option_value(multiworld, player, "shuffle_doors") >= 2: + if world.options.shuffle_doors >= 2: priority.add("Desert Laser") - priority.update(multiworld.per_slot_randoms[player].sample(lasers, 5)) + priority.update(world.random.sample(lasers, 5)) else: lasers.append("Desert Laser") - priority.update(multiworld.per_slot_randoms[player].sample(lasers, 6)) + priority.update(world.random.sample(lasers, 6)) return priority -def get_priority_hint_locations(multiworld: MultiWorld, player: int): +def get_priority_hint_locations(_: "WitnessWorld"): return { "Swamp Purple Underwater", "Shipwreck Vault Box", @@ -201,89 +223,100 @@ def get_priority_hint_locations(multiworld: MultiWorld, player: int): } -def make_hint_from_item(multiworld: MultiWorld, player: int, item: str): - location_obj = multiworld.find_item(item, player).item.location +def make_hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]): + locations = [item.location for item in own_itempool if item.name == item_name and item.location] + + if not locations: + return None + + location_obj = world.random.choice(locations) location_name = location_obj.name - if location_obj.player != player: - location_name += " (" + multiworld.get_player_name(location_obj.player) + ")" - return location_name, item, location_obj.address if (location_obj.player == player) else -1 + if location_obj.player != world.player: + location_name += " (" + world.multiworld.get_player_name(location_obj.player) + ")" + + return location_name, item_name, location_obj.address if (location_obj.player == world.player) else -1 -def make_hint_from_location(multiworld: MultiWorld, player: int, location: str): - location_obj = multiworld.get_location(location, player) - item_obj = multiworld.get_location(location, player).item +def make_hint_from_location(world: "WitnessWorld", location: str): + location_obj = world.multiworld.get_location(location, world.player) + item_obj = world.multiworld.get_location(location, world.player).item item_name = item_obj.name - if item_obj.player != player: - item_name += " (" + multiworld.get_player_name(item_obj.player) + ")" + if item_obj.player != world.player: + item_name += " (" + world.multiworld.get_player_name(item_obj.player) + ")" - return location, item_name, location_obj.address if (location_obj.player == player) else -1 + return location, item_name, location_obj.address if (location_obj.player == world.player) else -1 -def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): +def make_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List[Item]): hints = list() prog_items_in_this_world = { - item.name for item in multiworld.get_items() - if item.player == player and item.code and item.advancement + item.name for item in own_itempool if item.advancement and item.code and item.location } loc_in_this_world = { - location.name for location in multiworld.get_locations(player) - if location.address + location.name for location in world.multiworld.get_locations(world.player) if location.address } always_locations = [ - location for location in get_always_hint_locations(multiworld, player) + location for location in get_always_hint_locations(world) if location in loc_in_this_world ] always_items = [ - item for item in get_always_hint_items(multiworld, player) + item for item in get_always_hint_items(world) if item in prog_items_in_this_world ] priority_locations = [ - location for location in get_priority_hint_locations(multiworld, player) + location for location in get_priority_hint_locations(world) if location in loc_in_this_world ] priority_items = [ - item for item in get_priority_hint_items(multiworld, player) + item for item in get_priority_hint_items(world) if item in prog_items_in_this_world ] always_hint_pairs = dict() for item in always_items: - hint_pair = make_hint_from_item(multiworld, player, item) + hint_pair = make_hint_from_item(world, item, own_itempool) - if hint_pair[2] == 158007: # Tutorial Gate Open + if not hint_pair or hint_pair[2] == 158007: # Tutorial Gate Open continue always_hint_pairs[hint_pair[0]] = (hint_pair[1], True, hint_pair[2]) for location in always_locations: - hint_pair = make_hint_from_location(multiworld, player, location) + hint_pair = make_hint_from_location(world, location) always_hint_pairs[hint_pair[0]] = (hint_pair[1], False, hint_pair[2]) priority_hint_pairs = dict() for item in priority_items: - hint_pair = make_hint_from_item(multiworld, player, item) + hint_pair = make_hint_from_item(world, item, own_itempool) - if hint_pair[2] == 158007: # Tutorial Gate Open + if not hint_pair or hint_pair[2] == 158007: # Tutorial Gate Open continue priority_hint_pairs[hint_pair[0]] = (hint_pair[1], True, hint_pair[2]) for location in priority_locations: - hint_pair = make_hint_from_location(multiworld, player, location) + hint_pair = make_hint_from_location(world, location) priority_hint_pairs[hint_pair[0]] = (hint_pair[1], False, hint_pair[2]) + already_hinted_locations = set() + for loc, item in always_hint_pairs.items(): + if loc in already_hinted_locations: + continue + if item[1]: hints.append((f"{item[0]} can be found at {loc}.", item[2])) else: hints.append((f"{loc} contains {item[0]}.", item[2])) - multiworld.per_slot_randoms[player].shuffle(hints) # shuffle always hint order in case of low hint amount + already_hinted_locations.add(loc) + + world.random.shuffle(hints) # shuffle always hint order in case of low hint amount remaining_hints = hint_amount - len(hints) priority_hint_amount = int(max(0.0, min(len(priority_hint_pairs) / 2, remaining_hints / 2))) @@ -291,22 +324,27 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): prog_items_in_this_world = sorted(list(prog_items_in_this_world)) locations_in_this_world = sorted(list(loc_in_this_world)) - multiworld.per_slot_randoms[player].shuffle(prog_items_in_this_world) - multiworld.per_slot_randoms[player].shuffle(locations_in_this_world) + world.random.shuffle(prog_items_in_this_world) + world.random.shuffle(locations_in_this_world) priority_hint_list = list(priority_hint_pairs.items()) - multiworld.per_slot_randoms[player].shuffle(priority_hint_list) + world.random.shuffle(priority_hint_list) for _ in range(0, priority_hint_amount): next_priority_hint = priority_hint_list.pop() loc = next_priority_hint[0] item = next_priority_hint[1] + if loc in already_hinted_locations: + continue + if item[1]: hints.append((f"{item[0]} can be found at {loc}.", item[2])) else: hints.append((f"{loc} contains {item[0]}.", item[2])) - next_random_hint_is_item = multiworld.per_slot_randoms[player].randrange(0, 2) # Moving this to the new system is in the bigger refactoring PR + already_hinted_locations.add(loc) + + next_random_hint_is_item = world.random.randrange(0, 2) while len(hints) < hint_amount: if next_random_hint_is_item: @@ -314,16 +352,28 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): next_random_hint_is_item = not next_random_hint_is_item continue - hint = make_hint_from_item(multiworld, player, prog_items_in_this_world.pop()) + hint = make_hint_from_item(world, prog_items_in_this_world.pop(), own_itempool) + + if not hint or hint[0] in already_hinted_locations: + continue + hints.append((f"{hint[1]} can be found at {hint[0]}.", hint[2])) + + already_hinted_locations.add(hint[0]) else: - hint = make_hint_from_location(multiworld, player, locations_in_this_world.pop()) + hint = make_hint_from_location(world, locations_in_this_world.pop()) + + if hint[0] in already_hinted_locations: + continue + hints.append((f"{hint[0]} contains {hint[1]}.", hint[2])) + already_hinted_locations.add(hint[0]) + next_random_hint_is_item = not next_random_hint_is_item return hints -def generate_joke_hints(multiworld: MultiWorld, player: int, amount: int): - return [(x, -1) for x in multiworld.per_slot_randoms[player].sample(joke_hints, amount)] +def generate_joke_hints(world: "WitnessWorld", amount: int) -> List[Tuple[str, int]]: + return [(x, -1) for x in world.random.sample(joke_hints, amount)] diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 82c79047f3fb..15c693b25dd4 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -2,18 +2,20 @@ Defines progression, junk and event items for The Witness """ import copy + from dataclasses import dataclass -from typing import Optional, Dict, List, Set +from typing import Optional, Dict, List, Set, TYPE_CHECKING from BaseClasses import Item, MultiWorld, ItemClassification -from .Options import get_option_value, is_option_enabled, the_witness_options - from .locations import ID_START, WitnessPlayerLocations from .player_logic import WitnessPlayerLogic from .static_logic import ItemDefinition, DoorItemDefinition, ProgressiveItemDefinition, ItemCategory, \ StaticWitnessLogic, WeightedItemDefinition from .utils import build_weighted_int_list +if TYPE_CHECKING: + from . import WitnessWorld + NUM_ENERGY_UPGRADES = 4 @@ -59,7 +61,7 @@ def __init__(self): classification = ItemClassification.progression StaticWitnessItems.item_groups.setdefault("Doors", []).append(item_name) elif definition.category is ItemCategory.LASER: - classification = ItemClassification.progression + classification = ItemClassification.progression_skip_balancing StaticWitnessItems.item_groups.setdefault("Lasers", []).append(item_name) elif definition.category is ItemCategory.USEFUL: classification = ItemClassification.useful @@ -90,11 +92,12 @@ class WitnessPlayerItems: Class that defines Items for a single world """ - def __init__(self, multiworld: MultiWorld, player: int, logic: WitnessPlayerLogic, locat: WitnessPlayerLocations): + def __init__(self, world: "WitnessWorld", logic: WitnessPlayerLogic, locat: WitnessPlayerLocations): """Adds event items after logic changes due to options""" - self._world: MultiWorld = multiworld - self._player_id: int = player + self._world: "WitnessWorld" = world + self._multiworld: MultiWorld = world.multiworld + self._player_id: int = world.player self._logic: WitnessPlayerLogic = logic self._locations: WitnessPlayerLocations = locat @@ -102,19 +105,33 @@ def __init__(self, multiworld: MultiWorld, player: int, logic: WitnessPlayerLogi self.item_data: Dict[str, ItemData] = copy.deepcopy(StaticWitnessItems.item_data) # Remove all progression items that aren't actually in the game. - self.item_data = {name: data for (name, data) in self.item_data.items() - if data.classification is not ItemClassification.progression or - name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME} + self.item_data = { + name: data for (name, data) in self.item_data.items() + if data.classification not in + {ItemClassification.progression, ItemClassification.progression_skip_balancing} + or name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME + } # Adjust item classifications based on game settings. - eps_shuffled = get_option_value(self._world, self._player_id, "shuffle_EPs") != 0 + eps_shuffled = self._world.options.shuffle_EPs + come_to_you = self._world.options.elevators_come_to_you for item_name, item_data in self.item_data.items(): - if not eps_shuffled and item_name in ["Monastery Garden Entry (Door)", "Monastery Shortcuts"]: + if not eps_shuffled and item_name in {"Monastery Garden Entry (Door)", + "Monastery Shortcuts", + "Quarry Boathouse Hook Control (Panel)", + "Windmill Turn Control (Panel)"}: # Downgrade doors that only gate progress in EP shuffle. item_data.classification = ItemClassification.useful - elif item_name in ["River Monastery Shortcut (Door)", "Jungle & River Shortcuts", - "Monastery Shortcut (Door)", - "Orchard Second Gate (Door)"]: + elif not come_to_you and not eps_shuffled and item_name in {"Quarry Elevator Control (Panel)", + "Swamp Long Bridge (Panel)"}: + # These Bridges/Elevators are not logical access because they may leave you stuck. + item_data.classification = ItemClassification.useful + elif item_name in {"River Monastery Garden Shortcut (Door)", + "Monastery Laser Shortcut (Door)", + "Orchard Second Gate (Door)", + "Jungle Bamboo Laser Shortcut (Door)", + "Keep Pressure Plates 2 Exit (Door)", + "Caves Elevator Controls (Panel)"}: # Downgrade doors that don't gate progress. item_data.classification = ItemClassification.useful @@ -122,8 +139,11 @@ def __init__(self, multiworld: MultiWorld, player: int, logic: WitnessPlayerLogi self._mandatory_items: Dict[str, int] = {} # Add progression items to the mandatory item list. - for item_name, item_data in {name: data for (name, data) in self.item_data.items() - if data.classification == ItemClassification.progression}.items(): + progression_dict = { + name: data for (name, data) in self.item_data.items() + if data.classification in {ItemClassification.progression, ItemClassification.progression_skip_balancing} + } + for item_name, item_data in progression_dict.items(): if isinstance(item_data.definition, ProgressiveItemDefinition): num_progression = len(self._logic.MULTI_LISTS[item_name]) self._mandatory_items[item_name] = num_progression @@ -170,7 +190,7 @@ def get_filler_items(self, quantity: int) -> Dict[str, int]: remaining_quantity -= len(output) # Read trap configuration data. - trap_weight = get_option_value(self._world, self._player_id, "trap_percentage") / 100 + trap_weight = self._world.options.trap_percentage / 100 filler_weight = 1 - trap_weight # Add filler items to the list. @@ -198,15 +218,14 @@ def get_early_items(self) -> List[str]: Returns items that are ideal for placing on extremely early checks, like the tutorial gate. """ output: Set[str] = set() - if "shuffle_symbols" not in the_witness_options.keys() \ - or is_option_enabled(self._world, self._player_id, "shuffle_symbols"): - if get_option_value(self._world, self._player_id, "shuffle_doors") > 0: + if self._world.options.shuffle_symbols: + if self._world.options.shuffle_doors: output = {"Dots", "Black/White Squares", "Symmetry"} else: output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"} - if is_option_enabled(self._world, self._player_id, "shuffle_discarded_panels"): - if get_option_value(self._world, self._player_id, "puzzle_randomization") == 1: + if self._world.options.shuffle_discarded_panels: + if self._world.options.puzzle_randomization == 1: output.add("Arrows") else: output.add("Triangles") @@ -217,7 +236,7 @@ def get_early_items(self) -> List[str]: # 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 # regardless of whether or not they actually wind up being manually placed. - for plando_setting in self._world.plando_items[self._player_id]: + for plando_setting in self._multiworld.plando_items[self._player_id]: if plando_setting.get("from_pool", True): for item_setting_key in [key for key in ["item", "items"] if key in plando_setting]: if type(plando_setting[item_setting_key]) is str: @@ -243,6 +262,7 @@ def get_door_ids_in_pool(self) -> List[int]: for item_name, item_data in {name: data for name, data in self.item_data.items() if isinstance(data.definition, DoorItemDefinition)}.items(): output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes] + return output def get_symbol_ids_not_in_pool(self) -> List[int]: diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index b33e276e3ad8..d20be2794056 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -1,11 +1,14 @@ """ Defines constants for different types of locations in the game """ +from typing import TYPE_CHECKING -from .Options import is_option_enabled, get_option_value from .player_logic import WitnessPlayerLogic from .static_logic import StaticWitnessLogic +if TYPE_CHECKING: + from . import WitnessWorld + ID_START = 158000 @@ -19,22 +22,29 @@ class StaticWitnessLocations: "Tutorial Front Left", "Tutorial Back Left", "Tutorial Back Right", + "Tutorial Patio Floor", "Tutorial Gate Open", "Outside Tutorial Vault Box", "Outside Tutorial Discard", "Outside Tutorial Shed Row 5", "Outside Tutorial Tree Row 9", + "Outside Tutorial Outpost Entry Panel", + "Outside Tutorial Outpost Exit Panel", "Glass Factory Discard", "Glass Factory Back Wall 5", "Glass Factory Front 3", "Glass Factory Melting 3", + "Symmetry Island Lower Panel", "Symmetry Island Right 5", "Symmetry Island Back 6", "Symmetry Island Left 7", + "Symmetry Island Upper Panel", "Symmetry Island Scenery Outlines 5", + "Symmetry Island Laser Yellow 3", + "Symmetry Island Laser Blue 3", "Symmetry Island Laser Panel", "Orchard Apple Tree 5", @@ -49,9 +59,15 @@ class StaticWitnessLocations: "Desert Final Bent 3", "Desert Laser Panel", + "Quarry Entry 1 Panel", + "Quarry Entry 2 Panel", + "Quarry Stoneworks Entry Left Panel", + "Quarry Stoneworks Entry Right Panel", "Quarry Stoneworks Lower Row 6", "Quarry Stoneworks Upper Row 8", + "Quarry Stoneworks Control Room Left", "Quarry Stoneworks Control Room Right", + "Quarry Stoneworks Stairs Panel", "Quarry Boathouse Intro Right", "Quarry Boathouse Intro Left", "Quarry Boathouse Front Row 5", @@ -84,15 +100,32 @@ class StaticWitnessLocations: "Monastery Inside 4", "Monastery Laser Panel", + "Town Cargo Box Entry Panel", "Town Cargo Box Discard", "Town Tall Hexagonal", + "Town Church Entry Panel", "Town Church Lattice", + "Town Maze Panel", "Town Rooftop Discard", "Town Red Rooftop 5", "Town Wooden Roof Lower Row 5", "Town Wooden Rooftop", + "Town Windmill Entry Panel", + "Town RGB House Entry Panel", "Town Laser Panel", + "Town RGB Room Left", + "Town RGB Room Right", + "Town Sound Room Right", + + "Windmill Theater Entry Panel", + "Theater Exit Left Panel", + "Theater Exit Right Panel", + "Theater Tutorial Video", + "Theater Desert Video", + "Theater Jungle Video", + "Theater Shipwreck Video", + "Theater Mountain Video", "Theater Discard", "Jungle Discard", @@ -102,24 +135,33 @@ class StaticWitnessLocations: "Jungle Laser Panel", "River Vault Box", + "River Monastery Garden Shortcut Panel", + "Bunker Entry Panel", "Bunker Intro Left 5", "Bunker Intro Back 4", "Bunker Glass Room 3", "Bunker UV Room 2", "Bunker Laser Panel", + "Swamp Entry Panel", "Swamp Intro Front 6", "Swamp Intro Back 8", "Swamp Between Bridges Near Row 4", "Swamp Cyan Underwater 5", "Swamp Platform Row 4", + "Swamp Platform Shortcut Right Panel", "Swamp Between Bridges Far Row 4", "Swamp Red Underwater 4", + "Swamp Purple Underwater", "Swamp Beyond Rotating Bridge 4", "Swamp Blue Underwater 5", "Swamp Laser Panel", + "Swamp Laser Shortcut Right Panel", + "Treehouse First Door Panel", + "Treehouse Second Door Panel", + "Treehouse Third Door Panel", "Treehouse Yellow Bridge 9", "Treehouse First Purple Bridge 5", "Treehouse Second Purple Bridge 7", @@ -129,22 +171,11 @@ class StaticWitnessLocations: "Treehouse Laser Discard", "Treehouse Right Orange Bridge 12", "Treehouse Laser Panel", + "Treehouse Drawbridge Panel", "Mountainside Discard", "Mountainside Vault Box", - "Mountaintop River Shape", - "Tutorial Patio Floor", - "Quarry Stoneworks Control Room Left", - "Theater Tutorial Video", - "Theater Desert Video", - "Theater Jungle Video", - "Theater Shipwreck Video", - "Theater Mountain Video", - "Town RGB Room Left", - "Town RGB Room Right", - "Town Sound Room Right", - "Swamp Purple Underwater", "First Hallway EP", "Tutorial Cloud EP", @@ -316,46 +347,10 @@ class StaticWitnessLocations: "Town Obelisk Side 4", "Town Obelisk Side 5", "Town Obelisk Side 6", - } - OBELISK_SIDES = { - "Desert Obelisk Side 1", - "Desert Obelisk Side 2", - "Desert Obelisk Side 3", - "Desert Obelisk Side 4", - "Desert Obelisk Side 5", - "Monastery Obelisk Side 1", - "Monastery Obelisk Side 2", - "Monastery Obelisk Side 3", - "Monastery Obelisk Side 4", - "Monastery Obelisk Side 5", - "Monastery Obelisk Side 6", - "Treehouse Obelisk Side 1", - "Treehouse Obelisk Side 2", - "Treehouse Obelisk Side 3", - "Treehouse Obelisk Side 4", - "Treehouse Obelisk Side 5", - "Treehouse Obelisk Side 6", - "River Obelisk Side 1", - "River Obelisk Side 2", - "River Obelisk Side 3", - "River Obelisk Side 4", - "River Obelisk Side 5", - "River Obelisk Side 6", - "Quarry Obelisk Side 1", - "Quarry Obelisk Side 2", - "Quarry Obelisk Side 3", - "Quarry Obelisk Side 4", - "Quarry Obelisk Side 5", - "Town Obelisk Side 1", - "Town Obelisk Side 2", - "Town Obelisk Side 3", - "Town Obelisk Side 4", - "Town Obelisk Side 5", - "Town Obelisk Side 6", - } + "Caves Mountain Shortcut Panel", + "Caves Swamp Shortcut Panel", - CAVES_LOCATIONS = { "Caves Blue Tunnel Right First 4", "Caves Blue Tunnel Left First 1", "Caves Blue Tunnel Left Second 5", @@ -378,17 +373,22 @@ class StaticWitnessLocations: "Caves Left Upstairs Single", "Caves Left Upstairs Left Row 5", + "Caves Challenge Entry Panel", + "Challenge Tunnels Entry Panel", + "Tunnels Vault Box", "Theater Challenge Video", + "Tunnels Town Shortcut Panel", + "Caves Skylight EP", "Challenge Water EP", "Tunnels Theater Flowers EP", "Tutorial Gate EP", - } - MOUNTAIN_UNREACHABLE_FROM_BEHIND = { - "Mountaintop Trap Door Triple Exit", + "Mountaintop Mountain Entry Panel", + + "Mountain Floor 1 Light Bridge Controller", "Mountain Floor 1 Right Row 5", "Mountain Floor 1 Left Row 7", @@ -403,46 +403,84 @@ class StaticWitnessLocations: "Mountain Bottom Floor Yellow Bridge EP", "Mountain Bottom Floor Blue Bridge EP", "Mountain Floor 2 Pink Bridge EP", - } - MOUNTAIN_REACHABLE_FROM_BEHIND = { "Mountain Floor 2 Elevator Discard", "Mountain Bottom Floor Giant Puzzle", + "Mountain Bottom Floor Final Room Entry Left", + "Mountain Bottom Floor Final Room Entry Right", + + "Mountain Bottom Floor Caves Entry Panel", + "Mountain Final Room Left Pillar 4", "Mountain Final Room Right Pillar 4", - } - MOUNTAIN_EXTRAS = { "Challenge Vault Box", "Theater Challenge Video", - "Mountain Bottom Floor Discard" + "Mountain Bottom Floor Discard", + } + + OBELISK_SIDES = { + "Desert Obelisk Side 1", + "Desert Obelisk Side 2", + "Desert Obelisk Side 3", + "Desert Obelisk Side 4", + "Desert Obelisk Side 5", + "Monastery Obelisk Side 1", + "Monastery Obelisk Side 2", + "Monastery Obelisk Side 3", + "Monastery Obelisk Side 4", + "Monastery Obelisk Side 5", + "Monastery Obelisk Side 6", + "Treehouse Obelisk Side 1", + "Treehouse Obelisk Side 2", + "Treehouse Obelisk Side 3", + "Treehouse Obelisk Side 4", + "Treehouse Obelisk Side 5", + "Treehouse Obelisk Side 6", + "River Obelisk Side 1", + "River Obelisk Side 2", + "River Obelisk Side 3", + "River Obelisk Side 4", + "River Obelisk Side 5", + "River Obelisk Side 6", + "Quarry Obelisk Side 1", + "Quarry Obelisk Side 2", + "Quarry Obelisk Side 3", + "Quarry Obelisk Side 4", + "Quarry Obelisk Side 5", + "Town Obelisk Side 1", + "Town Obelisk Side 2", + "Town Obelisk Side 3", + "Town Obelisk Side 4", + "Town Obelisk Side 5", + "Town Obelisk Side 6", } ALL_LOCATIONS_TO_ID = dict() @staticmethod - def get_id(chex): + def get_id(chex: str): """ Calculates the location ID for any given location """ - return StaticWitnessLogic.CHECKS_BY_HEX[chex]["id"] + return StaticWitnessLogic.ENTITIES_BY_HEX[chex]["id"] @staticmethod - def get_event_name(panel_hex): + def get_event_name(panel_hex: str): """ Returns the event name of any given panel. """ - action = " Opened" if StaticWitnessLogic.CHECKS_BY_HEX[panel_hex]["panelType"] == "Door" else " Solved" + action = " Opened" if StaticWitnessLogic.ENTITIES_BY_HEX[panel_hex]["entityType"] == "Door" else " Solved" - return StaticWitnessLogic.CHECKS_BY_HEX[panel_hex]["checkName"] + action + return StaticWitnessLogic.ENTITIES_BY_HEX[panel_hex]["checkName"] + action def __init__(self): all_loc_to_id = { panel_obj["checkName"]: self.get_id(chex) - for chex, panel_obj in StaticWitnessLogic.CHECKS_BY_HEX.items() + for chex, panel_obj in StaticWitnessLogic.ENTITIES_BY_HEX.items() if panel_obj["id"] } @@ -459,84 +497,44 @@ class WitnessPlayerLocations: Class that defines locations for a single player """ - def __init__(self, world, player, player_logic: WitnessPlayerLogic): + def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic): """Defines locations AFTER logic changes due to options""" self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"} self.CHECK_LOCATIONS = StaticWitnessLocations.GENERAL_LOCATIONS.copy() - doors = get_option_value(world, player, "shuffle_doors") >= 2 - earlyutm = is_option_enabled(world, player, "early_secret_area") - victory = get_option_value(world, player, "victory_condition") - mount_lasers = get_option_value(world, player, "mountain_lasers") - chal_lasers = get_option_value(world, player, "challenge_lasers") - # laser_shuffle = get_option_value(world, player, "shuffle_lasers") - - postgame = set() - postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS - postgame = postgame | StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND - postgame = postgame | StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND - postgame = postgame | StaticWitnessLocations.MOUNTAIN_EXTRAS - - self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | postgame - - mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mount_lasers) - - if earlyutm or doors: # in non-doors, there is no way to get symbol-locked by the final pillars (currently) - postgame -= StaticWitnessLocations.CAVES_LOCATIONS - - if (doors or earlyutm) and (victory == 0 or (victory == 2 and mount_lasers > chal_lasers)): - postgame -= {"Challenge Vault Box", "Theater Challenge Video"} - - if doors or mountain_enterable_from_top: - postgame -= StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND - - if mountain_enterable_from_top: - postgame -= StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND - - if (victory == 0 and doors) or victory == 1 or (victory == 2 and mount_lasers > chal_lasers and doors): - postgame -= {"Mountain Bottom Floor Discard"} - - if is_option_enabled(world, player, "shuffle_discarded_panels"): + if world.options.shuffle_discarded_panels: self.PANEL_TYPES_TO_SHUFFLE.add("Discard") - if is_option_enabled(world, player, "shuffle_vault_boxes"): + if world.options.shuffle_vault_boxes: self.PANEL_TYPES_TO_SHUFFLE.add("Vault") - if get_option_value(world, player, "shuffle_EPs") == 1: + if world.options.shuffle_EPs == 1: self.PANEL_TYPES_TO_SHUFFLE.add("EP") - elif get_option_value(world, player, "shuffle_EPs") == 2: + elif world.options.shuffle_EPs == 2: self.PANEL_TYPES_TO_SHUFFLE.add("Obelisk Side") for obelisk_loc in StaticWitnessLocations.OBELISK_SIDES: - obelisk_loc_hex = StaticWitnessLogic.CHECKS_BY_NAME[obelisk_loc]["checkHex"] + obelisk_loc_hex = StaticWitnessLogic.ENTITIES_BY_NAME[obelisk_loc]["entity_hex"] if player_logic.REQUIREMENTS_BY_HEX[obelisk_loc_hex] == frozenset({frozenset()}): self.CHECK_LOCATIONS.discard(obelisk_loc) self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | player_logic.ADDED_CHECKS - if not is_option_enabled(world, player, "shuffle_postgame"): - self.CHECK_LOCATIONS -= postgame - - self.CHECK_LOCATIONS -= { - StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] - for panel in player_logic.PRECOMPLETED_LOCATIONS - } - - self.CHECK_LOCATIONS.discard(StaticWitnessLogic.CHECKS_BY_HEX[player_logic.VICTORY_LOCATION]["checkName"]) + self.CHECK_LOCATIONS.discard(StaticWitnessLogic.ENTITIES_BY_HEX[player_logic.VICTORY_LOCATION]["checkName"]) self.CHECK_LOCATIONS = self.CHECK_LOCATIONS - { - StaticWitnessLogic.CHECKS_BY_HEX[check_hex]["checkName"] - for check_hex in player_logic.COMPLETELY_DISABLED_CHECKS + StaticWitnessLogic.ENTITIES_BY_HEX[entity_hex]["checkName"] + for entity_hex in player_logic.COMPLETELY_DISABLED_ENTITIES | player_logic.PRECOMPLETED_LOCATIONS } self.CHECK_PANELHEX_TO_ID = { - StaticWitnessLogic.CHECKS_BY_NAME[ch]["checkHex"]: StaticWitnessLocations.ALL_LOCATIONS_TO_ID[ch] + StaticWitnessLogic.ENTITIES_BY_NAME[ch]["entity_hex"]: StaticWitnessLocations.ALL_LOCATIONS_TO_ID[ch] for ch in self.CHECK_LOCATIONS - if StaticWitnessLogic.CHECKS_BY_NAME[ch]["panelType"] in self.PANEL_TYPES_TO_SHUFFLE + if StaticWitnessLogic.ENTITIES_BY_NAME[ch]["entityType"] in self.PANEL_TYPES_TO_SHUFFLE } - dog_hex = StaticWitnessLogic.CHECKS_BY_NAME["Town Pet the Dog"]["checkHex"] + dog_hex = StaticWitnessLogic.ENTITIES_BY_NAME["Town Pet the Dog"]["entity_hex"] dog_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID["Town Pet the Dog"] self.CHECK_PANELHEX_TO_ID[dog_hex] = dog_id @@ -554,9 +552,14 @@ def __init__(self, world, player, player_logic: WitnessPlayerLogic): } check_dict = { - StaticWitnessLogic.CHECKS_BY_HEX[location]["checkName"]: - StaticWitnessLocations.get_id(StaticWitnessLogic.CHECKS_BY_HEX[location]["checkHex"]) + StaticWitnessLogic.ENTITIES_BY_HEX[location]["checkName"]: + StaticWitnessLocations.get_id(StaticWitnessLogic.ENTITIES_BY_HEX[location]["entity_hex"]) for location in self.CHECK_PANELHEX_TO_ID } self.CHECK_LOCATION_TABLE = {**self.EVENT_LOCATION_TABLE, **check_dict} + + def add_location_late(self, entity_name: str): + entity_hex = StaticWitnessLogic.ENTITIES_BY_NAME[entity_name]["entity_hex"] + self.CHECK_LOCATION_TABLE[entity_hex] = entity_name + self.CHECK_PANELHEX_TO_ID[entity_hex] = StaticWitnessLocations.get_id(entity_hex) diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index be1a34aedfcf..cfd36c09be24 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -16,22 +16,22 @@ """ import copy -from typing import Set, Dict, cast, List +from collections import defaultdict +from typing import cast, TYPE_CHECKING from logging import warning -from BaseClasses import MultiWorld from .static_logic import StaticWitnessLogic, DoorItemDefinition, ItemCategory, ProgressiveItemDefinition -from .utils import define_new_region, get_disable_unrandomized_list, parse_lambda, get_early_utm_list, \ - get_symbol_shuffle_list, get_door_panel_shuffle_list, get_doors_complex_list, get_doors_max_list, \ - get_doors_simple_list, get_laser_shuffle, get_ep_all_individual, get_ep_obelisks, get_ep_easy, get_ep_no_eclipse, \ - get_ep_no_caves, get_ep_no_mountain, get_ep_no_videos -from .Options import is_option_enabled, get_option_value, the_witness_options +from .utils import * + +if TYPE_CHECKING: + from . import WitnessWorld class WitnessPlayerLogic: """WITNESS LOGIC CLASS""" - def reduce_req_within_region(self, panel_hex): + @lru_cache(maxsize=None) + def reduce_req_within_region(self, panel_hex: str) -> FrozenSet[FrozenSet[str]]: """ Panels in this game often only turn on when other panels are solved. Those other panels may have different item requirements. @@ -40,14 +40,14 @@ def reduce_req_within_region(self, panel_hex): Panels outside of the same region will still be checked manually. """ - if panel_hex in self.COMPLETELY_DISABLED_CHECKS or panel_hex in self.PRECOMPLETED_LOCATIONS: + if panel_hex in self.COMPLETELY_DISABLED_ENTITIES or panel_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: return frozenset() - check_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel_hex] + entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel_hex] these_items = frozenset({frozenset()}) - if check_obj["id"]: + if entity_obj["id"]: these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"] these_items = frozenset({ @@ -58,6 +58,8 @@ def reduce_req_within_region(self, panel_hex): for subset in these_items: self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset) + these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"] + if panel_hex in self.DOOR_ITEMS_BY_ID: door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[panel_hex]}) @@ -68,14 +70,21 @@ def reduce_req_within_region(self, panel_hex): for items_option in these_items: all_options.add(items_option.union(dependentItem)) + # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved... if panel_hex != "0x28A0D": return frozenset(all_options) - else: # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved - these_items = all_options + # ...except in Expert, where that dependency doesn't exist, but now there *is* a power dependency. + # In the future, it would be wise to make a distinction between "power dependencies" and other dependencies. + if any("0x28998" in option for option in these_panels): + return frozenset(all_options) - these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"] + these_items = all_options - these_panels = frozenset({panels - self.PRECOMPLETED_LOCATIONS for panels in these_panels}) + disabled_eps = {eHex for eHex in self.COMPLETELY_DISABLED_ENTITIES + if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[eHex]["entityType"] == "EP"} + + these_panels = frozenset({panels - disabled_eps + for panels in these_panels}) if these_panels == frozenset({frozenset()}): return these_items @@ -85,40 +94,30 @@ def reduce_req_within_region(self, panel_hex): for option in these_panels: dependent_items_for_option = frozenset({frozenset()}) - for option_panel in option: - dep_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX.get(option_panel) - - if option_panel in self.COMPLETELY_DISABLED_CHECKS: - new_items = frozenset() - elif option_panel in {"7 Lasers", "11 Lasers", "PP2 Weirdness", "Theater to Tunnels"}: - new_items = frozenset({frozenset([option_panel])}) - # If a panel turns on when a panel in a different region turns on, - # the latter panel will be an "event panel", unless it ends up being - # a location itself. This prevents generation failures. - elif dep_obj["region"]["name"] != check_obj["region"]["name"]: - new_items = frozenset({frozenset([option_panel])}) - self.EVENT_PANELS_FROM_PANELS.add(option_panel) - elif option_panel in self.ALWAYS_EVENT_NAMES_BY_HEX.keys(): - new_items = frozenset({frozenset([option_panel])}) - self.EVENT_PANELS_FROM_PANELS.add(option_panel) - else: - new_items = self.reduce_req_within_region(option_panel) - - updated_items = set() + for option_entity in option: + dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity) - for items_option in dependent_items_for_option: - for items_option2 in new_items: - updated_items.add(items_option.union(items_option2)) + if option_entity in self.EVENT_NAMES_BY_HEX: + new_items = frozenset({frozenset([option_entity])}) + elif option_entity in {"7 Lasers", "11 Lasers", "PP2 Weirdness", "Theater to Tunnels"}: + new_items = frozenset({frozenset([option_entity])}) + else: + new_items = self.reduce_req_within_region(option_entity) + if dep_obj["region"] and entity_obj["region"] != dep_obj["region"]: + new_items = frozenset( + frozenset(possibility | {dep_obj["region"]["name"]}) + for possibility in new_items + ) - dependent_items_for_option = updated_items + dependent_items_for_option = dnf_and([dependent_items_for_option, new_items]) for items_option in these_items: for dependentItem in dependent_items_for_option: all_options.add(items_option.union(dependentItem)) - return frozenset(all_options) + return dnf_remove_redundancies(frozenset(all_options)) - def make_single_adjustment(self, adj_type, line): + def make_single_adjustment(self, adj_type: str, line: str): from . import StaticWitnessItems """Makes a single logic adjustment based on additional logic file""" @@ -148,9 +147,9 @@ def make_single_adjustment(self, adj_type, line): self.THEORETICAL_ITEMS.discard(item_name) if isinstance(StaticWitnessLogic.all_items[item_name], ProgressiveItemDefinition): - self.THEORETICAL_ITEMS_NO_MULTI\ - .difference_update(cast(ProgressiveItemDefinition, - StaticWitnessLogic.all_items[item_name]).child_item_names) + self.THEORETICAL_ITEMS_NO_MULTI.difference_update( + cast(ProgressiveItemDefinition, StaticWitnessLogic.all_items[item_name]).child_item_names + ) else: self.THEORETICAL_ITEMS_NO_MULTI.discard(item_name) @@ -165,25 +164,15 @@ def make_single_adjustment(self, adj_type, line): if adj_type == "Event Items": line_split = line.split(" - ") + new_event_name = line_split[0] hex_set = line_split[1].split(",") - for hex_code in hex_set: - self.ALWAYS_EVENT_NAMES_BY_HEX[hex_code] = line_split[0] - - """ - Should probably do this differently... - Events right now depend on a panel. - That seems bad. - """ - - to_remove = set() - - for hex_code, event_name in self.ALWAYS_EVENT_NAMES_BY_HEX.items(): - if hex_code not in hex_set and event_name == line_split[0]: - to_remove.add(hex_code) + for entity, event_name in self.EVENT_NAMES_BY_HEX.items(): + if event_name == new_event_name: + self.DONT_MAKE_EVENTS.add(entity) - for remove in to_remove: - del self.ALWAYS_EVENT_NAMES_BY_HEX[remove] + for hex_code in hex_set: + self.EVENT_NAMES_BY_HEX[hex_code] = new_event_name return @@ -196,9 +185,10 @@ def make_single_adjustment(self, adj_type, line): if len(line_split) > 2: required_items = parse_lambda(line_split[2]) - items_actually_in_the_game = [item_name for item_name, item_definition - in StaticWitnessLogic.all_items.items() - if item_definition.category is ItemCategory.SYMBOL] + items_actually_in_the_game = [ + item_name for item_name, item_definition in StaticWitnessLogic.all_items.items() + if item_definition.category is ItemCategory.SYMBOL + ] required_items = frozenset( subset.intersection(items_actually_in_the_game) for subset in required_items @@ -213,116 +203,204 @@ def make_single_adjustment(self, adj_type, line): if adj_type == "Disabled Locations": panel_hex = line[:7] - self.COMPLETELY_DISABLED_CHECKS.add(panel_hex) + self.COMPLETELY_DISABLED_ENTITIES.add(panel_hex) + + return + + if adj_type == "Irrelevant Locations": + panel_hex = line[:7] + + self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES.add(panel_hex) return if adj_type == "Region Changes": new_region_and_options = define_new_region(line + ":") - + self.CONNECTIONS_BY_REGION_NAME[new_region_and_options[0]["name"]] = new_region_and_options[1] return + if adj_type == "New Connections": + line_split = line.split(" - ") + source_region = line_split[0] + target_region = line_split[1] + panel_set_string = line_split[2] + + for connection in self.CONNECTIONS_BY_REGION_NAME[source_region]: + if connection[0] == target_region: + self.CONNECTIONS_BY_REGION_NAME[source_region].remove(connection) + + if panel_set_string == "TrueOneWay": + self.CONNECTIONS_BY_REGION_NAME[source_region].add( + (target_region, frozenset({frozenset(["TrueOneWay"])})) + ) + else: + new_lambda = connection[1] | parse_lambda(panel_set_string) + self.CONNECTIONS_BY_REGION_NAME[source_region].add((target_region, new_lambda)) + break + else: # Execute if loop did not break. TIL this is a thing you can do! + new_conn = (target_region, parse_lambda(panel_set_string)) + self.CONNECTIONS_BY_REGION_NAME[source_region].add(new_conn) + if adj_type == "Added Locations": if "0x" in line: - line = StaticWitnessLogic.CHECKS_BY_HEX[line]["checkName"] + line = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[line]["checkName"] self.ADDED_CHECKS.add(line) - if adj_type == "Precompleted Locations": - self.PRECOMPLETED_LOCATIONS.add(line) - - def make_options_adjustments(self, world, player): + def make_options_adjustments(self, world: "WitnessWorld"): """Makes logic adjustments based on options""" adjustment_linesets_in_order = [] - if get_option_value(world, player, "victory_condition") == 0: + # Postgame + + doors = world.options.shuffle_doors >= 2 + lasers = world.options.shuffle_lasers + early_caves = world.options.early_caves > 0 + victory = world.options.victory_condition + mnt_lasers = world.options.mountain_lasers + chal_lasers = world.options.challenge_lasers + + mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mnt_lasers) + + if not world.options.shuffle_postgame: + if not (early_caves or doors): + adjustment_linesets_in_order.append(get_caves_exclusion_list()) + if not victory == 1: + adjustment_linesets_in_order.append(get_path_to_challenge_exclusion_list()) + adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list()) + adjustment_linesets_in_order.append(get_beyond_challenge_exclusion_list()) + + if not ((doors or early_caves) and (victory == 0 or (victory == 2 and mnt_lasers > chal_lasers))): + adjustment_linesets_in_order.append(get_beyond_challenge_exclusion_list()) + if not victory == 1: + adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list()) + + if not (doors or mountain_enterable_from_top): + adjustment_linesets_in_order.append(get_mountain_lower_exclusion_list()) + + if not mountain_enterable_from_top: + adjustment_linesets_in_order.append(get_mountain_upper_exclusion_list()) + + if not ((victory == 0 and doors) or victory == 1 or (victory == 2 and mnt_lasers > chal_lasers and doors)): + if doors: + adjustment_linesets_in_order.append(get_bottom_floor_discard_exclusion_list()) + else: + adjustment_linesets_in_order.append(get_bottom_floor_discard_nondoors_exclusion_list()) + + if victory == 2 and chal_lasers >= mnt_lasers: + adjustment_linesets_in_order.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"]) + + # Exclude Discards / Vaults + + if not world.options.shuffle_discarded_panels: + # In disable_non_randomized, the discards are needed for alternate activation triggers, UNLESS both + # (remote) doors and lasers are shuffled. + if not world.options.disable_non_randomized_puzzles or (doors and lasers): + adjustment_linesets_in_order.append(get_discard_exclusion_list()) + + if doors: + adjustment_linesets_in_order.append(get_bottom_floor_discard_exclusion_list()) + + if not world.options.shuffle_vault_boxes: + adjustment_linesets_in_order.append(get_vault_exclusion_list()) + if not victory == 1: + adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list()) + + # Victory Condition + + if victory == 0: self.VICTORY_LOCATION = "0x3D9A9" - elif get_option_value(world, player, "victory_condition") == 1: + elif victory == 1: self.VICTORY_LOCATION = "0x0356B" - elif get_option_value(world, player, "victory_condition") == 2: + elif victory == 2: self.VICTORY_LOCATION = "0x09F7F" - elif get_option_value(world, player, "victory_condition") == 3: + elif victory == 3: self.VICTORY_LOCATION = "0xFFF00" - if get_option_value(world, player, "challenge_lasers") <= 7: + if chal_lasers <= 7: adjustment_linesets_in_order.append([ "Requirement Changes:", "0xFFF00 - 11 Lasers - True", ]) - if is_option_enabled(world, player, "disable_non_randomized_puzzles"): + if world.options.disable_non_randomized_puzzles: adjustment_linesets_in_order.append(get_disable_unrandomized_list()) - if is_option_enabled(world, player, "shuffle_symbols") or "shuffle_symbols" not in the_witness_options.keys(): + if world.options.shuffle_symbols: adjustment_linesets_in_order.append(get_symbol_shuffle_list()) - if get_option_value(world, player, "EP_difficulty") == 0: + if world.options.EP_difficulty == 0: adjustment_linesets_in_order.append(get_ep_easy()) - elif get_option_value(world, player, "EP_difficulty") == 1: + elif world.options.EP_difficulty == 1: adjustment_linesets_in_order.append(get_ep_no_eclipse()) - if not is_option_enabled(world, player, "shuffle_vault_boxes"): - adjustment_linesets_in_order.append(get_ep_no_videos()) - - doors = get_option_value(world, player, "shuffle_doors") >= 2 - earlyutm = is_option_enabled(world, player, "early_secret_area") - victory = get_option_value(world, player, "victory_condition") - mount_lasers = get_option_value(world, player, "mountain_lasers") - chal_lasers = get_option_value(world, player, "challenge_lasers") - - excluse_postgame = not is_option_enabled(world, player, "shuffle_postgame") - - if excluse_postgame and not (earlyutm or doors): - adjustment_linesets_in_order.append(get_ep_no_caves()) - - mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mount_lasers) - if excluse_postgame and not mountain_enterable_from_top: - adjustment_linesets_in_order.append(get_ep_no_mountain()) - - if get_option_value(world, player, "shuffle_doors") == 1: - adjustment_linesets_in_order.append(get_door_panel_shuffle_list()) - - if get_option_value(world, player, "shuffle_doors") == 2: - adjustment_linesets_in_order.append(get_doors_simple_list()) - - if get_option_value(world, player, "shuffle_doors") == 3: - adjustment_linesets_in_order.append(get_doors_complex_list()) - - if get_option_value(world, player, "shuffle_doors") == 4: - adjustment_linesets_in_order.append(get_doors_max_list()) - - if is_option_enabled(world, player, "early_secret_area"): - adjustment_linesets_in_order.append(get_early_utm_list()) + if world.options.door_groupings == 1: + if world.options.shuffle_doors == 1: + adjustment_linesets_in_order.append(get_simple_panels()) + elif world.options.shuffle_doors == 2: + adjustment_linesets_in_order.append(get_simple_doors()) + elif world.options.shuffle_doors == 3: + adjustment_linesets_in_order.append(get_simple_doors()) + adjustment_linesets_in_order.append(get_simple_additional_panels()) + else: + if world.options.shuffle_doors == 1: + adjustment_linesets_in_order.append(get_complex_door_panels()) + adjustment_linesets_in_order.append(get_complex_additional_panels()) + elif world.options.shuffle_doors == 2: + adjustment_linesets_in_order.append(get_complex_doors()) + elif world.options.shuffle_doors == 3: + adjustment_linesets_in_order.append(get_complex_doors()) + adjustment_linesets_in_order.append(get_complex_additional_panels()) + + if world.options.shuffle_boat: + adjustment_linesets_in_order.append(get_boat()) + + if world.options.early_caves == 2: + adjustment_linesets_in_order.append(get_early_caves_start_list()) + + if world.options.early_caves == 1 and not doors: + adjustment_linesets_in_order.append(get_early_caves_list()) + + if world.options.elevators_come_to_you: + adjustment_linesets_in_order.append(get_elevators_come_to_you()) for item in self.YAML_ADDED_ITEMS: adjustment_linesets_in_order.append(["Items:", item]) - if is_option_enabled(world, player, "shuffle_lasers"): + if lasers: adjustment_linesets_in_order.append(get_laser_shuffle()) - if get_option_value(world, player, "shuffle_EPs") == 0: # No EP Shuffle - adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_all_individual()[1:]) - adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:]) + if world.options.shuffle_EPs: + ep_gen = ((ep_hex, ep_obj) for (ep_hex, ep_obj) in self.REFERENCE_LOGIC.ENTITIES_BY_HEX.items() + if ep_obj["entityType"] == "EP") - elif get_option_value(world, player, "shuffle_EPs") == 1: # Individual EPs + for ep_hex, ep_obj in ep_gen: + obelisk = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[self.REFERENCE_LOGIC.EP_TO_OBELISK_SIDE[ep_hex]] + obelisk_name = obelisk["checkName"] + ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"] + self.EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}" + else: adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:]) + if world.options.shuffle_EPs == 0: + adjustment_linesets_in_order.append(["Irrelevant Locations:"] + get_ep_all_individual()[1:]) + yaml_disabled_eps = [] for yaml_disabled_location in self.YAML_DISABLED_LOCATIONS: - if yaml_disabled_location not in StaticWitnessLogic.CHECKS_BY_NAME: + if yaml_disabled_location not in self.REFERENCE_LOGIC.ENTITIES_BY_NAME: continue - loc_obj = StaticWitnessLogic.CHECKS_BY_NAME[yaml_disabled_location] + loc_obj = self.REFERENCE_LOGIC.ENTITIES_BY_NAME[yaml_disabled_location] - if loc_obj["panelType"] == "EP" and get_option_value(world, player, "shuffle_EPs") == 2: - yaml_disabled_eps.append(loc_obj["checkHex"]) + if loc_obj["entityType"] == "EP" and world.options.shuffle_EPs != 0: + yaml_disabled_eps.append(loc_obj["entity_hex"]) - if loc_obj["panelType"] in {"EP", "General"}: - self.EXCLUDED_LOCATIONS.add(loc_obj["checkHex"]) + if loc_obj["entityType"] in {"EP", "General", "Vault", "Discard"}: + self.EXCLUDED_LOCATIONS.add(loc_obj["entity_hex"]) - adjustment_linesets_in_order.append(["Precompleted Locations:"] + yaml_disabled_eps) + adjustment_linesets_in_order.append(["Disabled Locations:"] + yaml_disabled_eps) for adjustment_lineset in adjustment_linesets_in_order: current_adjustment_type = None @@ -337,15 +415,19 @@ def make_options_adjustments(self, world, player): self.make_single_adjustment(current_adjustment_type, line) + for entity_id in self.COMPLETELY_DISABLED_ENTITIES: + if entity_id in self.DOOR_ITEMS_BY_ID: + del self.DOOR_ITEMS_BY_ID[entity_id] + def make_dependency_reduced_checklist(self): """ Turns dependent check set into semi-independent check set """ - for check_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys(): - indep_requirement = self.reduce_req_within_region(check_hex) + for entity_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys(): + indep_requirement = self.reduce_req_within_region(entity_hex) - self.REQUIREMENTS_BY_HEX[check_hex] = indep_requirement + self.REQUIREMENTS_BY_HEX[entity_hex] = indep_requirement for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: if item not in self.THEORETICAL_ITEMS: @@ -360,71 +442,76 @@ def make_dependency_reduced_checklist(self): else: self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item) - def make_event_item_pair(self, panel): - """ - Makes a pair of an event panel and its event item - """ - action = " Opened" if StaticWitnessLogic.CHECKS_BY_HEX[panel]["panelType"] == "Door" else " Solved" + for region, connections in self.CONNECTIONS_BY_REGION_NAME.items(): + new_connections = [] - name = StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + action - if panel not in self.EVENT_ITEM_NAMES: - if StaticWitnessLogic.CHECKS_BY_HEX[panel]["panelType"] == "EP": - obelisk = StaticWitnessLogic.CHECKS_BY_HEX[StaticWitnessLogic.EP_TO_OBELISK_SIDE[panel]]["checkName"] + for connection in connections: + overall_requirement = frozenset() - self.EVENT_ITEM_NAMES[panel] = obelisk + " - " + StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + for option in connection[1]: + individual_entity_requirements = [] + for entity in option: + if entity in self.EVENT_NAMES_BY_HEX or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX: + individual_entity_requirements.append(frozenset({frozenset({entity})})) + else: + entity_req = self.reduce_req_within_region(entity) - else: - warning("Panel \"" + name + "\" does not have an associated event name.") - self.EVENT_ITEM_NAMES[panel] = name + " Event" - pair = (name, self.EVENT_ITEM_NAMES[panel]) - return pair + if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]: + region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"] + entity_req = dnf_and([entity_req, frozenset({frozenset({region_name})})]) - def make_event_panel_lists(self): - """ - Special event panel data structures - """ + individual_entity_requirements.append(entity_req) - self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" + overall_requirement |= dnf_and(individual_entity_requirements) - for region_name, connections in self.CONNECTIONS_BY_REGION_NAME.items(): - for connection in connections: - for panel_req in connection[1]: - for panel in panel_req: - if panel == "TrueOneWay": - continue + new_connections.append((connection[0], overall_requirement)) - if self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel]["region"]["name"] != region_name: - self.EVENT_PANELS_FROM_REGIONS.add(panel) + self.CONNECTIONS_BY_REGION_NAME[region] = new_connections - self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_PANELS) - self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_REGIONS) + def make_event_item_pair(self, panel: str): + """ + Makes a pair of an event panel and its event item + """ + action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["entityType"] == "Door" else " Solved" + + name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["checkName"] + action + if panel not in self.EVENT_NAMES_BY_HEX: + warning("Panel \"" + name + "\" does not have an associated event name.") + self.EVENT_NAMES_BY_HEX[panel] = name + " Event" + pair = (name, self.EVENT_NAMES_BY_HEX[panel]) + return pair + + def make_event_panel_lists(self): + self.EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" - for always_hex, always_item in self.ALWAYS_EVENT_NAMES_BY_HEX.items(): - self.ALWAYS_EVENT_HEX_CODES.add(always_hex) - self.EVENT_PANELS.add(always_hex) - self.EVENT_ITEM_NAMES[always_hex] = always_item + for event_hex, event_name in self.EVENT_NAMES_BY_HEX.items(): + if event_hex in self.COMPLETELY_DISABLED_ENTITIES: + continue + self.EVENT_PANELS.add(event_hex) for panel in self.EVENT_PANELS: pair = self.make_event_item_pair(panel) self.EVENT_ITEM_PAIRS[pair[0]] = pair[1] - def __init__(self, world: MultiWorld, player: int, disabled_locations: Set[str], start_inv: Dict[str, int]): + def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]): self.YAML_DISABLED_LOCATIONS = disabled_locations self.YAML_ADDED_ITEMS = start_inv self.EVENT_PANELS_FROM_PANELS = set() self.EVENT_PANELS_FROM_REGIONS = set() + self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES = set() + self.THEORETICAL_ITEMS = set() self.THEORETICAL_ITEMS_NO_MULTI = set() - self.MULTI_AMOUNTS = dict() + self.MULTI_AMOUNTS = defaultdict(lambda: 1) self.MULTI_LISTS = dict() self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set() - self.DOOR_ITEMS_BY_ID: Dict[str, List[int]] = {} + self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {} self.STARTING_INVENTORY = set() - self.DIFFICULTY = get_option_value(world, player, "puzzle_randomization") + self.DIFFICULTY = world.options.puzzle_randomization.value if self.DIFFICULTY == 0: self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_normal @@ -441,106 +528,30 @@ def __init__(self, world: MultiWorld, player: int, disabled_locations: Set[str], # At the end, we will have EVENT_ITEM_PAIRS for all the necessary ones. self.EVENT_PANELS = set() self.EVENT_ITEM_PAIRS = dict() - self.ALWAYS_EVENT_HEX_CODES = set() - self.COMPLETELY_DISABLED_CHECKS = set() + self.DONT_MAKE_EVENTS = set() + self.COMPLETELY_DISABLED_ENTITIES = set() self.PRECOMPLETED_LOCATIONS = set() self.EXCLUDED_LOCATIONS = set() self.ADDED_CHECKS = set() self.VICTORY_LOCATION = "0x0356B" - self.EVENT_ITEM_NAMES = { - "0x09D9B": "Monastery Shutters Open", - "0x193A6": "Monastery Laser Panel Activates", - "0x00037": "Monastery Branch Panels Activate", - "0x0A079": "Access to Bunker Laser", - "0x0A3B5": "Door to Tutorial Discard Opens", - "0x00139": "Keep Hedges 1 Knowledge", - "0x019DC": "Keep Hedges 2 Knowledge", - "0x019E7": "Keep Hedges 3 Knowledge", - "0x01A0F": "Keep Hedges 4 Knowledge", - "0x033EA": "Pressure Plates 1 Knowledge", - "0x01BE9": "Pressure Plates 2 Knowledge", - "0x01CD3": "Pressure Plates 3 Knowledge", - "0x01D3F": "Pressure Plates 4 Knowledge", - "0x09F7F": "Mountain Access", - "0x0367C": "Quarry Laser Stoneworks Requirement Met", - "0x009A1": "Swamp Between Bridges Far 1 Activates", - "0x00006": "Swamp Cyan Water Drains", - "0x00990": "Swamp Between Bridges Near Row 1 Activates", - "0x0A8DC": "Intro 6 Activates", - "0x0000A": "Swamp Beyond Rotating Bridge 1 Access", - "0x09E86": "Mountain Floor 2 Blue Bridge Access", - "0x09ED8": "Mountain Floor 2 Yellow Bridge Access", - "0x0A3D0": "Quarry Laser Boathouse Requirement Met", - "0x00596": "Swamp Red Water Drains", - "0x00E3A": "Swamp Purple Water Drains", - "0x0343A": "Door to Symmetry Island Powers On", - "0xFFF00": "Mountain Bottom Floor Discard Turns On", - "0x17CA6": "All Boat Panels Turn On", - "0x17CDF": "All Boat Panels Turn On", - "0x09DB8": "All Boat Panels Turn On", - "0x17C95": "All Boat Panels Turn On", - "0x0A054": "Couch EP solvable", - "0x03BB0": "Town Church Lattice Vision From Outside", - "0x28AC1": "Town Wooden Rooftop Turns On", - "0x28A69": "Town Tower 1st Door Opens", - "0x28ACC": "Town Tower 2nd Door Opens", - "0x28AD9": "Town Tower 3rd Door Opens", - "0x28B39": "Town Tower 4th Door Opens", - "0x03675": "Quarry Stoneworks Ramp Activation From Above", - "0x03679": "Quarry Stoneworks Lift Lowering While Standing On It", - "0x2FAF6": "Tutorial Gate Secret Solution Knowledge", - "0x079DF": "Town Tall Hexagonal Turns On", - "0x17DA2": "Right Orange Bridge Fully Extended", - "0x19B24": "Shadows Intro Patterns Visible", - "0x2700B": "Open Door to Treehouse Laser House", - "0x00055": "Orchard Apple Trees 4 Turns On", - "0x17DDB": "Left Orange Bridge Fully Extended", - "0x03535": "Shipwreck Video Pattern Knowledge", - "0x03542": "Mountain Video Pattern Knowledge", - "0x0339E": "Desert Video Pattern Knowledge", - "0x03481": "Tutorial Video Pattern Knowledge", - "0x03702": "Jungle Video Pattern Knowledge", - "0x0356B": "Challenge Video Pattern Knowledge", - "0x0A15F": "Desert Laser Panel Shutters Open (1)", - "0x012D7": "Desert Laser Panel Shutters Open (2)", - "0x03613": "Treehouse Orange Bridge 13 Turns On", - "0x17DEC": "Treehouse Laser House Access Requirement", - "0x03C08": "Town Church Entry Opens", - "0x17D02": "Windmill Blades Spinning", - "0x0A0C9": "Cargo Box EP completable", - "0x09E39": "Pink Light Bridge Extended", - "0x17CC4": "Rails EP available", - "0x2896A": "Bridge Underside EP available", - "0x00064": "First Tunnel EP visible", - "0x03553": "Tutorial Video EPs availble", - "0x17C79": "Bunker Door EP available", - "0x275FF": "Stoneworks Light EPs available", - "0x17E2B": "Remaining Purple Sand EPs available", - "0x03852": "Ramp EPs requirement", - "0x334D8": "RGB panels & EPs solvable", - "0x03750": "Left Garden EP available", - "0x03C0C": "RGB Flowers EP requirement", - "0x01CD5": "Pressure Plates 3 EP requirement", - "0x3865F": "Ramp EPs access requirement", - } - self.ALWAYS_EVENT_NAMES_BY_HEX = { - "0x00509": "Symmetry Laser Activation", - "0x012FB": "Desert Laser Activation", + self.EVENT_NAMES_BY_HEX = { + "0x00509": "+1 Laser (Symmetry Laser)", + "0x012FB": "+1 Laser (Desert Laser)", "0x09F98": "Desert Laser Redirection", - "0x01539": "Quarry Laser Activation", - "0x181B3": "Shadows Laser Activation", - "0x014BB": "Keep Laser Activation", - "0x17C65": "Monastery Laser Activation", - "0x032F9": "Town Laser Activation", - "0x00274": "Jungle Laser Activation", - "0x0C2B2": "Bunker Laser Activation", - "0x00BF6": "Swamp Laser Activation", - "0x028A4": "Treehouse Laser Activation", - "0x09F7F": "Mountaintop Trap Door Turns On", - "0x17C34": "Mountain Access", + "0x01539": "+1 Laser (Quarry Laser)", + "0x181B3": "+1 Laser (Shadows Laser)", + "0x014BB": "+1 Laser (Keep Laser)", + "0x17C65": "+1 Laser (Monastery Laser)", + "0x032F9": "+1 Laser (Town Laser)", + "0x00274": "+1 Laser (Jungle Laser)", + "0x0C2B2": "+1 Laser (Bunker Laser)", + "0x00BF6": "+1 Laser (Swamp Laser)", + "0x028A4": "+1 Laser (Treehouse Laser)", + "0x09F7F": "Mountain Entry", + "0xFFF00": "Bottom Floor Discard Turns On", } - self.make_options_adjustments(world, player) + self.make_options_adjustments(world) self.make_dependency_reduced_checklist() self.make_event_panel_lists() diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 0e15cafe10fd..2187010bac07 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -2,13 +2,17 @@ Defines Region for The Witness, assigns locations to them, and connects them with the proper requirements """ +from typing import FrozenSet, TYPE_CHECKING, Dict, Tuple, List -from BaseClasses import MultiWorld, Entrance +from BaseClasses import Entrance, Region +from Utils import KeyedDefaultDict from .static_logic import StaticWitnessLogic -from .Options import get_option_value from .locations import WitnessPlayerLocations, StaticWitnessLocations from .player_logic import WitnessPlayerLogic +if TYPE_CHECKING: + from . import WitnessWorld + class WitnessRegions: """Class that defines Witness Regions""" @@ -16,62 +20,81 @@ class WitnessRegions: locat = None logic = None - def make_lambda(self, panel_hex_to_solve_set, world, player, player_logic): + @staticmethod + def make_lambda(item_requirement: FrozenSet[FrozenSet[str]], world: "WitnessWorld"): + from .rules import _meets_item_requirements + """ Lambdas are made in a for loop, so the values have to be captured This function is for that purpose """ - return lambda state: state._witness_can_solve_panels( - panel_hex_to_solve_set, world, player, player_logic, self.locat - ) + return _meets_item_requirements(item_requirement, world) - def connect(self, world: MultiWorld, player: int, source: str, target: str, player_logic: WitnessPlayerLogic, - panel_hex_to_solve_set=frozenset({frozenset()}), backwards: bool = False): + def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: FrozenSet[FrozenSet[str]], + regions_by_name: Dict[str, Region], backwards: bool = False): """ connect two regions and set the corresponding requirement """ - source_region = world.get_region(source, player) - target_region = world.get_region(target, player) + + # Remove any possibilities where being in the target region would be required anyway. + real_requirement = frozenset({option for option in req if target not in option}) + + # There are some connections that should only be done one way. If this is a backwards connection, check for that + if backwards: + real_requirement = frozenset({option for option in real_requirement if "TrueOneWay" not in option}) + + # Dissolve any "True" or "TrueOneWay" + real_requirement = frozenset({option - {"True", "TrueOneWay"} for option in real_requirement}) + + # If there is no way to actually use this connection, don't even bother making it. + if not real_requirement: + return + + # We don't need to check for the accessibility of the source region. + final_requirement = frozenset({option - frozenset({source}) for option in real_requirement}) + + source_region = regions_by_name[source] + target_region = regions_by_name[target] backwards = " Backwards" if backwards else "" + connection_name = source + " to " + target + backwards connection = Entrance( - player, - source + " to " + target + backwards, + world.player, + connection_name, source_region ) - connection.access_rule = self.make_lambda(panel_hex_to_solve_set, world, player, player_logic) + connection.access_rule = self.make_lambda(final_requirement, world) source_region.exits.append(connection) connection.connect(target_region) - def create_regions(self, world, player: int, player_logic: WitnessPlayerLogic): + self.created_entrances[(source, target)].append(connection) + + # Register any necessary indirect connections + mentioned_regions = { + single_unlock for option in final_requirement for single_unlock in option + if single_unlock in self.reference_logic.ALL_REGIONS_BY_NAME + } + + for dependent_region in mentioned_regions: + world.multiworld.register_indirect_condition(regions_by_name[dependent_region], connection) + + def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic): """ Creates all the regions for The Witness """ from . import create_region - world.regions += [ - create_region(world, player, 'Menu', self.locat, None, ["The Splashscreen?"]), - ] - - difficulty = get_option_value(world, player, "puzzle_randomization") - - if difficulty == 1: - reference_logic = StaticWitnessLogic.sigma_expert - elif difficulty == 0: - reference_logic = StaticWitnessLogic.sigma_normal - else: - reference_logic = StaticWitnessLogic.vanilla - all_locations = set() + regions_by_name = dict() - for region_name, region in reference_logic.ALL_REGIONS_BY_NAME.items(): + for region_name, region in self.reference_logic.ALL_REGIONS_BY_NAME.items(): locations_for_this_region = [ - reference_logic.CHECKS_BY_HEX[panel]["checkName"] for panel in region["panels"] - if reference_logic.CHECKS_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE + self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["panels"] + if self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE ] locations_for_this_region += [ StaticWitnessLocations.get_event_name(panel) for panel in region["panels"] @@ -80,34 +103,45 @@ def create_regions(self, world, player: int, player_logic: WitnessPlayerLogic): all_locations = all_locations | set(locations_for_this_region) - world.regions += [ - create_region(world, player, region_name, self.locat, locations_for_this_region) - ] + new_region = create_region(world, region_name, self.locat, locations_for_this_region) + + regions_by_name[region_name] = new_region - for region_name, region in reference_logic.ALL_REGIONS_BY_NAME.items(): + for region_name, region in self.reference_logic.ALL_REGIONS_BY_NAME.items(): for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]: - if connection[1] == frozenset({frozenset(["TrueOneWay"])}): - self.connect(world, player, region_name, connection[0], player_logic, frozenset({frozenset()})) + self.connect_if_possible(world, region_name, connection[0], connection[1], regions_by_name) + self.connect_if_possible(world, connection[0], region_name, connection[1], regions_by_name, True) + + # find regions that are completely disconnected from the start node and remove them + regions_to_check = {"Menu"} + reachable_regions = {"Menu"} + + while regions_to_check: + next_region = regions_to_check.pop() + region_obj = regions_by_name[next_region] + + for exit in region_obj.exits: + target = exit.connected_region + + if target.name in reachable_regions: continue - backwards_connections = set() + regions_to_check.add(target.name) + reachable_regions.add(target.name) - for subset in connection[1]: - if all({panel in player_logic.DOOR_ITEMS_BY_ID for panel in subset}): - if all({reference_logic.CHECKS_BY_HEX[panel]["id"] is None for panel in subset}): - backwards_connections.add(subset) + final_regions_list = [v for k, v in regions_by_name.items() if k in reachable_regions] - if backwards_connections: - self.connect( - world, player, connection[0], region_name, player_logic, - frozenset(backwards_connections), True - ) + world.multiworld.regions += final_regions_list - self.connect(world, player, region_name, connection[0], player_logic, connection[1]) + def __init__(self, locat: WitnessPlayerLocations, world: "WitnessWorld"): + difficulty = world.options.puzzle_randomization.value - world.get_entrance("The Splashscreen?", player).connect( - world.get_region('First Hallway', player) - ) + if difficulty == 0: + self.reference_logic = StaticWitnessLogic.sigma_normal + elif difficulty == 1: + self.reference_logic = StaticWitnessLogic.sigma_expert + elif difficulty == 2: + self.reference_logic = StaticWitnessLogic.vanilla - def __init__(self, locat: WitnessPlayerLocations): self.locat = locat + self.created_entrances: Dict[Tuple[str, str], List[Entrance]] = KeyedDefaultDict(lambda _: []) diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 4cf3054af6fd..07fea23b14ba 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -3,223 +3,218 @@ depending on the items received """ -# pylint: disable=E1101 +from typing import TYPE_CHECKING, Callable, FrozenSet -from BaseClasses import MultiWorld +from BaseClasses import CollectionState from .player_logic import WitnessPlayerLogic -from .Options import is_option_enabled, get_option_value from .locations import WitnessPlayerLocations -from . import StaticWitnessLogic -from worlds.AutoWorld import LogicMixin +from . import StaticWitnessLogic, WitnessRegions from worlds.generic.Rules import set_rule +if TYPE_CHECKING: + from . import WitnessWorld -class WitnessLogic(LogicMixin): +laser_hexes = [ + "0x028A4", + "0x00274", + "0x032F9", + "0x01539", + "0x181B3", + "0x0C2B2", + "0x00509", + "0x00BF6", + "0x014BB", + "0x012FB", + "0x17C65", +] + + +def _has_laser(laser_hex: str, world: "WitnessWorld", player: int) -> Callable[[CollectionState], bool]: + if laser_hex == "0x012FB": + return lambda state: ( + _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat)(state) + and state.has("Desert Laser Redirection", player) + ) + else: + return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat) + + +def _has_lasers(amount: int, world: "WitnessWorld") -> Callable[[CollectionState], bool]: + laser_lambdas = [] + + for laser_hex in laser_hexes: + has_laser_lambda = _has_laser(laser_hex, world, world.player) + + laser_lambdas.append(has_laser_lambda) + + return lambda state: sum(laser_lambda(state) for laser_lambda in laser_lambdas) >= amount + + +def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic, + locat: WitnessPlayerLocations) -> Callable[[CollectionState], bool]: + """ + Determines whether a panel can be solved + """ + + panel_obj = player_logic.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel] + entity_name = panel_obj["checkName"] + + if entity_name + " Solved" in locat.EVENT_LOCATION_TABLE: + return lambda state: state.has(player_logic.EVENT_ITEM_PAIRS[entity_name + " Solved"], player) + else: + return make_lambda(panel, world) + + +def _can_move_either_direction(state: CollectionState, source: str, target: str, regio: WitnessRegions) -> bool: + entrance_forward = regio.created_entrances[(source, target)] + entrance_backward = regio.created_entrances[(source, target)] + + return ( + any(entrance.can_reach(state) for entrance in entrance_forward) + or + any(entrance.can_reach(state) for entrance in entrance_backward) + ) + + +def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: + player = world.player + + hedge_2_access = ( + _can_move_either_direction(state, "Keep 2nd Maze", "Keep", world.regio) + ) + + hedge_3_access = ( + _can_move_either_direction(state, "Keep 3rd Maze", "Keep", world.regio) + or _can_move_either_direction(state, "Keep 3rd Maze", "Keep 2nd Maze", world.regio) + and hedge_2_access + ) + + hedge_4_access = ( + _can_move_either_direction(state, "Keep 4th Maze", "Keep", world.regio) + or _can_move_either_direction(state, "Keep 4th Maze", "Keep 3rd Maze", world.regio) + and hedge_3_access + ) + + hedge_access = ( + _can_move_either_direction(state, "Keep 4th Maze", "Keep Tower", world.regio) + and state.can_reach("Keep", "Region", player) + and hedge_4_access + ) + + backwards_to_fourth = ( + state.can_reach("Keep", "Region", player) + and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Keep Tower", world.regio) + and ( + _can_move_either_direction(state, "Keep", "Keep Tower", world.regio) + or hedge_access + ) + ) + + shadows_shortcut = ( + state.can_reach("Main Island", "Region", player) + and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Shadows", world.regio) + ) + + backwards_access = ( + _can_move_either_direction(state, "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate", world.regio) + and (backwards_to_fourth or shadows_shortcut) + ) + + front_access = ( + _can_move_either_direction(state, "Keep 2nd Pressure Plate", "Keep", world.regio) + and state.can_reach("Keep", "Region", player) + ) + + return front_access and backwards_access + + +def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> bool: + direct_access = ( + _can_move_either_direction(state, "Tunnels", "Windmill Interior", world.regio) + and _can_move_either_direction(state, "Theater", "Windmill Interior", world.regio) + ) + + theater_from_town = ( + _can_move_either_direction(state, "Town", "Windmill Interior", world.regio) + and _can_move_either_direction(state, "Theater", "Windmill Interior", world.regio) + or _can_move_either_direction(state, "Town", "Theater", world.regio) + ) + + tunnels_from_town = ( + _can_move_either_direction(state, "Tunnels", "Windmill Interior", world.regio) + and _can_move_either_direction(state, "Town", "Windmill Interior", world.regio) + or _can_move_either_direction(state, "Tunnels", "Town", world.regio) + ) + + return direct_access or theater_from_town and tunnels_from_town + + +def _has_item(item: str, world: "WitnessWorld", player: int, + player_logic: WitnessPlayerLogic, locat: WitnessPlayerLocations) -> Callable[[CollectionState], bool]: + if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME: + return lambda state: state.can_reach(item, "Region", player) + if item == "7 Lasers": + laser_req = world.options.mountain_lasers.value + return _has_lasers(laser_req, world) + if item == "11 Lasers": + laser_req = world.options.challenge_lasers.value + return _has_lasers(laser_req, world) + elif item == "PP2 Weirdness": + return lambda state: _can_do_expert_pp2(state, world) + elif item == "Theater to Tunnels": + return lambda state: _can_do_theater_to_tunnels(state, world) + if item in player_logic.EVENT_PANELS: + return _can_solve_panel(item, world, player, player_logic, locat) + + prog_item = StaticWitnessLogic.get_parent_progressive_item(item) + return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item]) + + +def _meets_item_requirements(requirements: FrozenSet[FrozenSet[str]], + world: "WitnessWorld") -> Callable[[CollectionState], bool]: """ - Logic macros that get reused + Checks whether item and panel requirements are met for + a panel """ - def _witness_has_lasers(self, world, player: int, amount: int) -> bool: - regular_lasers = not is_option_enabled(world, player, "shuffle_lasers") - - lasers = 0 - - place_names = [ - "Symmetry", "Desert", "Town", "Monastery", "Keep", - "Quarry", "Treehouse", "Jungle", "Bunker", "Swamp", "Shadows" - ] - - for place in place_names: - has_laser = self.has(place + " Laser", player) - - has_laser = has_laser or (regular_lasers and self.has(place + " Laser Activation", player)) - - if place == "Desert": - has_laser = has_laser and self.has("Desert Laser Redirection", player) - - lasers += int(has_laser) - - return lasers >= amount - - def _witness_can_solve_panel(self, panel, world, player, player_logic: WitnessPlayerLogic, locat): - """ - Determines whether a panel can be solved - """ - - panel_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel] - check_name = panel_obj["checkName"] - - if (check_name + " Solved" in locat.EVENT_LOCATION_TABLE - and not self.has(player_logic.EVENT_ITEM_PAIRS[check_name + " Solved"], player)): - return False - if (check_name + " Solved" not in locat.EVENT_LOCATION_TABLE - and not self._witness_meets_item_requirements(panel, world, player, player_logic, locat)): - return False - return True - - def _witness_meets_item_requirements(self, panel, world, player, player_logic: WitnessPlayerLogic, locat): - """ - Checks whether item and panel requirements are met for - a panel - """ - - panel_req = player_logic.REQUIREMENTS_BY_HEX[panel] - - for option in panel_req: - if len(option) == 0: - return True - - valid_option = True - - for item in option: - if item == "7 Lasers": - laser_req = get_option_value(world, player, "mountain_lasers") - - if not self._witness_has_lasers(world, player, laser_req): - valid_option = False - break - elif item == "11 Lasers": - laser_req = get_option_value(world, player, "challenge_lasers") - - if not self._witness_has_lasers(world, player, laser_req): - valid_option = False - break - elif item == "PP2 Weirdness": - hedge_2_access = ( - self.can_reach("Keep 2nd Maze to Keep", "Entrance", player) - or self.can_reach("Keep to Keep 2nd Maze", "Entrance", player) - ) - - hedge_3_access = ( - self.can_reach("Keep 3rd Maze to Keep", "Entrance", player) - or self.can_reach("Keep 2nd Maze to Keep 3rd Maze", "Entrance", player) - and hedge_2_access - ) - - hedge_4_access = ( - self.can_reach("Keep 4th Maze to Keep", "Entrance", player) - or self.can_reach("Keep 3rd Maze to Keep 4th Maze", "Entrance", player) - and hedge_3_access - ) - - hedge_access = ( - self.can_reach("Keep 4th Maze to Keep Tower", "Entrance", player) - and self.can_reach("Keep", "Region", player) - and hedge_4_access - ) - - backwards_to_fourth = ( - self.can_reach("Keep", "Region", player) - and self.can_reach("Keep 4th Pressure Plate to Keep Tower", "Entrance", player) - and ( - self.can_reach("Keep Tower to Keep", "Entrance", player) - or hedge_access - ) - ) - - shadows_shortcut = ( - self.can_reach("Main Island", "Region", player) - and self.can_reach("Keep 4th Pressure Plate to Shadows", "Entrance", player) - ) - - backwards_access = ( - self.can_reach("Keep 3rd Pressure Plate to Keep 4th Pressure Plate", "Entrance", player) - and (backwards_to_fourth or shadows_shortcut) - ) - - front_access = ( - self.can_reach("Keep to Keep 2nd Pressure Plate", 'Entrance', player) - and self.can_reach("Keep", "Region", player) - ) - - if not (front_access and backwards_access): - valid_option = False - break - elif item == "Theater to Tunnels": - direct_access = ( - self.can_reach("Tunnels to Windmill Interior", "Entrance", player) - and self.can_reach("Windmill Interior to Theater", "Entrance", player) - ) - - theater_from_town = ( - self.can_reach("Town to Windmill Interior", "Entrance", player) - and self.can_reach("Windmill Interior to Theater", "Entrance", player) - or self.can_reach("Theater to Town", "Entrance", player) - ) - - tunnels_from_town = ( - self.can_reach("Tunnels to Windmill Interior", "Entrance", player) - and self.can_reach("Town to Windmill Interior", "Entrance", player) - or self.can_reach("Tunnels to Town", "Entrance", player) - ) - - if not (direct_access or theater_from_town and tunnels_from_town): - valid_option = False - break - elif item in player_logic.EVENT_PANELS: - if not self._witness_can_solve_panel(item, world, player, player_logic, locat): - valid_option = False - break - elif not self.has(item, player): - # The player doesn't have the item. Check to see if it's part of a progressive item and, if so, the - # player has enough of that. - prog_item = StaticWitnessLogic.get_parent_progressive_item(item) - if prog_item is item or not self.has(prog_item, player, player_logic.MULTI_AMOUNTS[item]): - valid_option = False - break - - if valid_option: - return True - - return False - - def _witness_can_solve_panels(self, panel_hex_to_solve_set, world, player, player_logic: WitnessPlayerLogic, locat): - """ - Checks whether a set of panels can be solved. - """ - - for option in panel_hex_to_solve_set: - if len(option) == 0: - return True - - valid_option = True - - for panel in option: - if not self._witness_can_solve_panel(panel, world, player, player_logic, locat): - valid_option = False - break - - if valid_option: - return True - return False - - -def make_lambda(check_hex, world, player, player_logic, locat): + lambda_conversion = [ + [_has_item(item, world, world.player, world.player_logic, world.locat) for item in subset] + for subset in requirements + ] + + return lambda state: any( + all(condition(state) for condition in sub_requirement) + for sub_requirement in lambda_conversion + ) + + +def make_lambda(entity_hex: str, world: "WitnessWorld") -> Callable[[CollectionState], bool]: """ Lambdas are created in a for loop so values need to be captured """ - return lambda state: state._witness_meets_item_requirements( - check_hex, world, player, player_logic, locat - ) + entity_req = world.player_logic.REQUIREMENTS_BY_HEX[entity_hex] + + return _meets_item_requirements(entity_req, world) -def set_rules(world: MultiWorld, player: int, player_logic: WitnessPlayerLogic, locat: WitnessPlayerLocations): +def set_rules(world: "WitnessWorld"): """ Sets all rules for all locations """ - for location in locat.CHECK_LOCATION_TABLE: + for location in world.locat.CHECK_LOCATION_TABLE: real_location = location - if location in locat.EVENT_LOCATION_TABLE: + if location in world.locat.EVENT_LOCATION_TABLE: real_location = location[:-7] - panel = StaticWitnessLogic.CHECKS_BY_NAME[real_location] - check_hex = panel["checkHex"] + associated_entity = world.player_logic.REFERENCE_LOGIC.ENTITIES_BY_NAME[real_location] + entity_hex = associated_entity["entity_hex"] + + rule = make_lambda(entity_hex, world) - rule = make_lambda(check_hex, world, player, player_logic, locat) + location = world.multiworld.get_location(location, world.player) - set_rule(world.get_location(location, player), rule) + set_rule(location, rule) - world.completion_condition[player] = \ - lambda state: state.has('Victory', player) + world.multiworld.completion_condition[world.player] = lambda state: state.has('Victory', world.player) diff --git a/worlds/witness/settings/Door_Panel_Shuffle.txt b/worlds/witness/settings/Door_Panel_Shuffle.txt deleted file mode 100644 index 80195cfb9968..000000000000 --- a/worlds/witness/settings/Door_Panel_Shuffle.txt +++ /dev/null @@ -1,31 +0,0 @@ -Items: -Glass Factory Entry (Panel) -Symmetry Island Lower (Panel) -Symmetry Island Upper (Panel) -Desert Light Room Entry (Panel) -Desert Flood Controls (Panel) -Quarry Stoneworks Entry (Panel) -Quarry Stoneworks Ramp Controls (Panel) -Quarry Stoneworks Lift Controls (Panel) -Quarry Boathouse Ramp Height Control (Panel) -Quarry Boathouse Ramp Horizontal Control (Panel) -Shadows Door Timer (Panel) -Monastery Entry Left (Panel) -Monastery Entry Right (Panel) -Town Tinted Glass Door (Panel) -Town Church Entry (Panel) -Town Maze Panel (Drop-Down Staircase) (Panel) -Windmill Entry (Panel) -Treehouse First & Second Doors (Panel) -Treehouse Third Door (Panel) -Treehouse Laser House Door Timer (Panel) -Treehouse Drawbridge (Panel) -Jungle Popup Wall (Panel) -Bunker Entry (Panel) -Bunker Tinted Glass Door (Panel) -Bunker Elevator Control (Panel) -Swamp Entry (Panel) -Swamp Sliding Bridge (Panel) -Swamp Rotating Bridge (Panel) -Swamp Maze Control (Panel) -Boat \ No newline at end of file diff --git a/worlds/witness/settings/Door_Shuffle/Boat.txt b/worlds/witness/settings/Door_Shuffle/Boat.txt new file mode 100644 index 000000000000..6494b455cf79 --- /dev/null +++ b/worlds/witness/settings/Door_Shuffle/Boat.txt @@ -0,0 +1,2 @@ +Items: +Boat \ No newline at end of file diff --git a/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt b/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt new file mode 100644 index 000000000000..79bda7ea2281 --- /dev/null +++ b/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt @@ -0,0 +1,25 @@ +Items: +Desert Flood Controls (Panel) +Desert Light Control (Panel) +Quarry Elevator Control (Panel) +Quarry Stoneworks Ramp Controls (Panel) +Quarry Stoneworks Lift Controls (Panel) +Quarry Boathouse Ramp Height Control (Panel) +Quarry Boathouse Ramp Horizontal Control (Panel) +Quarry Boathouse Hook Control (Panel) +Monastery Shutters Control (Panel) +Town Maze Rooftop Bridge (Panel) +Town RGB Control (Panel) +Windmill Turn Control (Panel) +Theater Video Input (Panel) +Bunker Drop-Down Door Controls (Panel) +Bunker Elevator Control (Panel) +Swamp Sliding Bridge (Panel) +Swamp Rotating Bridge (Panel) +Swamp Long Bridge (Panel) +Swamp Maze Controls (Panel) +Mountain Floor 1 Light Bridge (Panel) +Mountain Floor 2 Light Bridge Near (Panel) +Mountain Floor 2 Light Bridge Far (Panel) +Mountain Floor 2 Elevator Control (Panel) +Caves Elevator Controls (Panel) \ No newline at end of file diff --git a/worlds/witness/settings/Door_Shuffle/Complex_Door_Panels.txt b/worlds/witness/settings/Door_Shuffle/Complex_Door_Panels.txt new file mode 100644 index 000000000000..472403962065 --- /dev/null +++ b/worlds/witness/settings/Door_Shuffle/Complex_Door_Panels.txt @@ -0,0 +1,38 @@ +Items: +Glass Factory Entry (Panel) +Tutorial Outpost Entry (Panel) +Tutorial Outpost Exit (Panel) +Symmetry Island Lower (Panel) +Symmetry Island Upper (Panel) +Desert Light Room Entry (Panel) +Desert Flood Room Entry (Panel) +Quarry Entry 1 (Panel) +Quarry Entry 2 (Panel) +Quarry Stoneworks Entry (Panel) +Shadows Door Timer (Panel) +Keep Hedge Maze 1 (Panel) +Keep Hedge Maze 2 (Panel) +Keep Hedge Maze 3 (Panel) +Keep Hedge Maze 4 (Panel) +Monastery Entry Left (Panel) +Monastery Entry Right (Panel) +Town RGB House Entry (Panel) +Town Church Entry (Panel) +Town Maze Stairs (Panel) +Town Windmill Entry (Panel) +Town Cargo Box Entry (Panel) +Theater Entry (Panel) +Theater Exit (Panel) +Treehouse First & Second Doors (Panel) +Treehouse Third Door (Panel) +Treehouse Laser House Door Timer (Panel) +Treehouse Drawbridge (Panel) +Jungle Popup Wall (Panel) +Bunker Entry (Panel) +Bunker Tinted Glass Door (Panel) +Swamp Entry (Panel) +Swamp Platform Shortcut (Panel) +Caves Entry (Panel) +Challenge Entry (Panel) +Tunnels Entry (Panel) +Tunnels Town Shortcut (Panel) \ No newline at end of file diff --git a/worlds/witness/settings/Doors_Complex.txt b/worlds/witness/settings/Door_Shuffle/Complex_Doors.txt similarity index 94% rename from worlds/witness/settings/Doors_Complex.txt rename to worlds/witness/settings/Door_Shuffle/Complex_Doors.txt index d8da6783b0c5..2f2b32171079 100644 --- a/worlds/witness/settings/Doors_Complex.txt +++ b/worlds/witness/settings/Door_Shuffle/Complex_Doors.txt @@ -12,6 +12,7 @@ Desert Light Room Entry (Door) Desert Pond Room Entry (Door) Desert Flood Room Entry (Door) Desert Elevator Room Entry (Door) +Desert Elevator (Door) Quarry Entry 1 (Door) Quarry Entry 2 (Door) Quarry Stoneworks Entry (Door) @@ -39,13 +40,13 @@ Keep Pressure Plates 3 Exit (Door) Keep Pressure Plates 4 Exit (Door) Keep Shadows Shortcut (Door) Keep Tower Shortcut (Door) -Monastery Shortcut (Door) +Monastery Laser Shortcut (Door) Monastery Entry Inner (Door) Monastery Entry Outer (Door) Monastery Garden Entry (Door) Town Cargo Box Entry (Door) Town Wooden Roof Stairs (Door) -Town Tinted Glass Door +Town RGB House Entry (Door) Town Church Entry (Door) Town Maze Stairs (Door) Town Windmill Entry (Door) @@ -59,7 +60,7 @@ Theater Exit Left (Door) Theater Exit Right (Door) Jungle Bamboo Laser Shortcut (Door) Jungle Popup Wall (Door) -River Monastery Shortcut (Door) +River Monastery Garden Shortcut (Door) Bunker Entry (Door) Bunker Tinted Glass Door Bunker UV Room Entry (Door) @@ -115,7 +116,7 @@ Quarry Stoneworks Entry Right Panel Quarry Stoneworks Entry Left Panel Quarry Stoneworks Side Exit Panel Quarry Stoneworks Roof Exit Panel -Quarry Stoneworks Stair Control +Quarry Stoneworks Stairs Panel Quarry Boathouse Second Barrier Panel Shadows Door Timer Inside Shadows Door Timer Outside @@ -133,15 +134,15 @@ Keep Pressure Plates 3 Keep Pressure Plates 4 Keep Shadows Shortcut Panel Keep Tower Shortcut Panel -Monastery Shortcut Panel +Monastery Laser Shortcut Panel Monastery Entry Left Monastery Entry Right Monastery Outside 3 Town Cargo Box Entry Panel Town Wooden Roof Lower Row 5 -Town Tinted Glass Door Panel +Town RGB House Entry Panel Town Church Entry Panel -Town Maze Stair Control +Town Maze Panel Town Windmill Entry Panel Town Sound Room Right Town Red Rooftop 5 @@ -153,7 +154,7 @@ Theater Exit Left Panel Theater Exit Right Panel Jungle Laser Shortcut Panel Jungle Popup Wall Control -River Monastery Shortcut Panel +River Monastery Garden Shortcut Panel Bunker Entry Panel Bunker Tinted Glass Door Panel Bunker Glass Room 3 @@ -171,10 +172,10 @@ Swamp Laser Shortcut Right Panel Treehouse First Door Panel Treehouse Second Door Panel Treehouse Third Door Panel -Treehouse Bridge Control +Treehouse Drawbridge Panel Treehouse Left Orange Bridge 15 Treehouse Right Orange Bridge 12 -Treehouse Laser House Door Timer Outside Control +Treehouse Laser House Door Timer Outside Treehouse Laser House Door Timer Inside Mountain Floor 1 Left Row 7 Mountain Floor 1 Right Row 5 diff --git a/worlds/witness/settings/Door_Shuffle/Elevators_Come_To_You.txt b/worlds/witness/settings/Door_Shuffle/Elevators_Come_To_You.txt new file mode 100644 index 000000000000..78d245f9f0b5 --- /dev/null +++ b/worlds/witness/settings/Door_Shuffle/Elevators_Come_To_You.txt @@ -0,0 +1,11 @@ +New Connections: +Quarry - Quarry Elevator - TrueOneWay +Outside Quarry - Quarry Elevator - TrueOneWay +Outside Bunker - Bunker Elevator - TrueOneWay +Outside Swamp - Swamp Long Bridge - TrueOneWay +Swamp Near Boat - Swamp Long Bridge - TrueOneWay +Town Red Rooftop - Town Maze Rooftop - TrueOneWay + + +Requirement Changes: +0x035DE - 0x17E2B - True \ No newline at end of file diff --git a/worlds/witness/settings/Door_Shuffle/Simple_Additional_Panels.txt b/worlds/witness/settings/Door_Shuffle/Simple_Additional_Panels.txt new file mode 100644 index 000000000000..c16ce737629a --- /dev/null +++ b/worlds/witness/settings/Door_Shuffle/Simple_Additional_Panels.txt @@ -0,0 +1,11 @@ +Items: +Desert Control Panels +Quarry Elevator Control (Panel) +Quarry Stoneworks Control Panels +Quarry Boathouse Control Panels +Monastery Shutters Control (Panel) +Town Control Panels +Windmill & Theater Control Panels +Bunker Control Panels +Swamp Control Panels +Mountain & Caves Control Panels \ No newline at end of file diff --git a/worlds/witness/settings/Doors_Simple.txt b/worlds/witness/settings/Door_Shuffle/Simple_Doors.txt similarity index 74% rename from worlds/witness/settings/Doors_Simple.txt rename to worlds/witness/settings/Door_Shuffle/Simple_Doors.txt index 309874b131b9..91a7132ec113 100644 --- a/worlds/witness/settings/Doors_Simple.txt +++ b/worlds/witness/settings/Door_Shuffle/Simple_Doors.txt @@ -1,44 +1,33 @@ Items: -Glass Factory Back Wall (Door) -Quarry Boathouse Dock (Door) Outside Tutorial Outpost Doors -Glass Factory Entry (Door) +Glass Factory Doors Symmetry Island Doors Orchard Gates -Desert Doors -Quarry Main Entry -Quarry Stoneworks Entry (Door) -Quarry Stoneworks Shortcuts -Quarry Boathouse Barriers -Shadows Timed Door -Shadows Laser Room Door -Shadows Barriers +Desert Doors & Elevator +Quarry Entry Doors +Quarry Stoneworks Doors +Quarry Boathouse Doors +Shadows Laser Room Doors +Shadows Lower Doors Keep Hedge Maze Doors Keep Pressure Plates Doors Keep Shortcuts -Monastery Entry +Monastery Entry Doors Monastery Shortcuts Town Doors Town Tower Doors -Theater Entry (Door) -Theater Exit -Jungle & River Shortcuts -Jungle Popup Wall (Door) +Windmill & Theater Doors +Jungle Doors Bunker Doors Swamp Doors -Swamp Laser Shortcut (Door) +Swamp Shortcuts Swamp Water Pumps Treehouse Entry Doors -Treehouse Drawbridge (Door) -Treehouse Laser House Entry (Door) -Mountain Floor 1 Exit (Door) -Mountain Floor 2 Stairs & Doors -Mountain Bottom Floor Giant Puzzle Exit (Door) -Mountain Bottom Floor Final Room Entry (Door) -Mountain Bottom Floor Doors to Caves -Caves Doors to Challenge -Caves Exits to Main Island -Challenge Tunnels Entry (Door) +Treehouse Upper Doors +Mountain Floor 1 & 2 Doors +Mountain Bottom Floor Doors +Caves Doors +Caves Shortcuts Tunnels Doors Added Locations: @@ -60,7 +49,7 @@ Quarry Stoneworks Entry Right Panel Quarry Stoneworks Entry Left Panel Quarry Stoneworks Side Exit Panel Quarry Stoneworks Roof Exit Panel -Quarry Stoneworks Stair Control +Quarry Stoneworks Stairs Panel Quarry Boathouse Second Barrier Panel Shadows Door Timer Inside Shadows Door Timer Outside @@ -78,15 +67,15 @@ Keep Pressure Plates 3 Keep Pressure Plates 4 Keep Shadows Shortcut Panel Keep Tower Shortcut Panel -Monastery Shortcut Panel +Monastery Laser Shortcut Panel Monastery Entry Left Monastery Entry Right Monastery Outside 3 Town Cargo Box Entry Panel Town Wooden Roof Lower Row 5 -Town Tinted Glass Door Panel +Town RGB House Entry Panel Town Church Entry Panel -Town Maze Stair Control +Town Maze Panel Town Windmill Entry Panel Town Sound Room Right Town Red Rooftop 5 @@ -98,7 +87,7 @@ Theater Exit Left Panel Theater Exit Right Panel Jungle Laser Shortcut Panel Jungle Popup Wall Control -River Monastery Shortcut Panel +River Monastery Garden Shortcut Panel Bunker Entry Panel Bunker Tinted Glass Door Panel Bunker Glass Room 3 @@ -116,10 +105,10 @@ Swamp Laser Shortcut Right Panel Treehouse First Door Panel Treehouse Second Door Panel Treehouse Third Door Panel -Treehouse Bridge Control +Treehouse Drawbridge Panel Treehouse Left Orange Bridge 15 Treehouse Right Orange Bridge 12 -Treehouse Laser House Door Timer Outside Control +Treehouse Laser House Door Timer Outside Treehouse Laser House Door Timer Inside Mountain Floor 1 Left Row 7 Mountain Floor 1 Right Row 5 diff --git a/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt b/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt new file mode 100644 index 000000000000..79da154491b7 --- /dev/null +++ b/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt @@ -0,0 +1,22 @@ +Items: +Symmetry Island Panels +Tutorial Outpost Panels +Desert Panels +Quarry Outside Panels +Quarry Stoneworks Panels +Quarry Boathouse Panels +Keep Hedge Maze Panels +Monastery Panels +Town Church & RGB House Panels +Town Maze Panels +Windmill & Theater Panels +Town Cargo Box Entry (Panel) +Treehouse Panels +Bunker Panels +Swamp Panels +Mountain Panels +Caves Panels +Tunnels Panels +Glass Factory Entry (Panel) +Shadows Door Timer (Panel) +Jungle Popup Wall (Panel) \ No newline at end of file diff --git a/worlds/witness/settings/Doors_Max.txt b/worlds/witness/settings/Doors_Max.txt deleted file mode 100644 index e722b61ca0c0..000000000000 --- a/worlds/witness/settings/Doors_Max.txt +++ /dev/null @@ -1,211 +0,0 @@ -Items: -Outside Tutorial Outpost Path (Door) -Outside Tutorial Outpost Entry (Door) -Outside Tutorial Outpost Exit (Door) -Glass Factory Entry (Door) -Glass Factory Back Wall (Door) -Symmetry Island Lower (Door) -Symmetry Island Upper (Door) -Orchard First Gate (Door) -Orchard Second Gate (Door) -Desert Light Room Entry (Door) -Desert Pond Room Entry (Door) -Desert Flood Room Entry (Door) -Desert Elevator Room Entry (Door) -Quarry Entry 1 (Door) -Quarry Entry 2 (Door) -Quarry Stoneworks Entry (Door) -Quarry Stoneworks Side Exit (Door) -Quarry Stoneworks Roof Exit (Door) -Quarry Stoneworks Stairs (Door) -Quarry Boathouse Dock (Door) -Quarry Boathouse First Barrier (Door) -Quarry Boathouse Second Barrier (Door) -Shadows Timed Door -Shadows Laser Entry Right (Door) -Shadows Laser Entry Left (Door) -Shadows Quarry Barrier (Door) -Shadows Ledge Barrier (Door) -Keep Hedge Maze 1 Exit (Door) -Keep Pressure Plates 1 Exit (Door) -Keep Hedge Maze 2 Shortcut (Door) -Keep Hedge Maze 2 Exit (Door) -Keep Hedge Maze 3 Shortcut (Door) -Keep Hedge Maze 3 Exit (Door) -Keep Hedge Maze 4 Shortcut (Door) -Keep Hedge Maze 4 Exit (Door) -Keep Pressure Plates 2 Exit (Door) -Keep Pressure Plates 3 Exit (Door) -Keep Pressure Plates 4 Exit (Door) -Keep Shadows Shortcut (Door) -Keep Tower Shortcut (Door) -Monastery Shortcut (Door) -Monastery Entry Inner (Door) -Monastery Entry Outer (Door) -Monastery Garden Entry (Door) -Town Cargo Box Entry (Door) -Town Wooden Roof Stairs (Door) -Town Tinted Glass Door -Town Church Entry (Door) -Town Maze Stairs (Door) -Town Windmill Entry (Door) -Town RGB House Stairs (Door) -Town Tower Second (Door) -Town Tower First (Door) -Town Tower Fourth (Door) -Town Tower Third (Door) -Theater Entry (Door) -Theater Exit Left (Door) -Theater Exit Right (Door) -Jungle Bamboo Laser Shortcut (Door) -Jungle Popup Wall (Door) -River Monastery Shortcut (Door) -Bunker Entry (Door) -Bunker Tinted Glass Door -Bunker UV Room Entry (Door) -Bunker Elevator Room Entry (Door) -Swamp Entry (Door) -Swamp Between Bridges First Door -Swamp Platform Shortcut Door -Swamp Cyan Water Pump (Door) -Swamp Between Bridges Second Door -Swamp Red Water Pump (Door) -Swamp Red Underwater Exit (Door) -Swamp Blue Water Pump (Door) -Swamp Purple Water Pump (Door) -Swamp Laser Shortcut (Door) -Treehouse First (Door) -Treehouse Second (Door) -Treehouse Third (Door) -Treehouse Drawbridge (Door) -Treehouse Laser House Entry (Door) -Mountain Floor 1 Exit (Door) -Mountain Floor 2 Staircase Near (Door) -Mountain Floor 2 Exit (Door) -Mountain Floor 2 Staircase Far (Door) -Mountain Bottom Floor Giant Puzzle Exit (Door) -Mountain Bottom Floor Final Room Entry (Door) -Mountain Bottom Floor Rock (Door) -Caves Entry (Door) -Caves Pillar Door -Caves Mountain Shortcut (Door) -Caves Swamp Shortcut (Door) -Challenge Entry (Door) -Challenge Tunnels Entry (Door) -Tunnels Theater Shortcut (Door) -Tunnels Desert Shortcut (Door) -Tunnels Town Shortcut (Door) - -Desert Flood Controls (Panel) -Quarry Stoneworks Ramp Controls (Panel) -Quarry Stoneworks Lift Controls (Panel) -Quarry Boathouse Ramp Height Control (Panel) -Quarry Boathouse Ramp Horizontal Control (Panel) -Bunker Elevator Control (Panel) -Swamp Sliding Bridge (Panel) -Swamp Rotating Bridge (Panel) -Swamp Maze Control (Panel) -Boat - -Added Locations: -Outside Tutorial Outpost Entry Panel -Outside Tutorial Outpost Exit Panel -Glass Factory Entry Panel -Glass Factory Back Wall 5 -Symmetry Island Lower Panel -Symmetry Island Upper Panel -Orchard Apple Tree 3 -Orchard Apple Tree 5 -Desert Light Room Entry Panel -Desert Light Room 3 -Desert Flood Room Entry Panel -Desert Flood Room 6 -Quarry Entry 1 Panel -Quarry Entry 2 Panel -Quarry Stoneworks Entry Right Panel -Quarry Stoneworks Entry Left Panel -Quarry Stoneworks Side Exit Panel -Quarry Stoneworks Roof Exit Panel -Quarry Stoneworks Stair Control -Quarry Boathouse Second Barrier Panel -Shadows Door Timer Inside -Shadows Door Timer Outside -Shadows Far 8 -Shadows Near 5 -Shadows Intro 3 -Shadows Intro 5 -Keep Hedge Maze 1 -Keep Pressure Plates 1 -Keep Hedge Maze 2 -Keep Hedge Maze 3 -Keep Hedge Maze 4 -Keep Pressure Plates 2 -Keep Pressure Plates 3 -Keep Pressure Plates 4 -Keep Shadows Shortcut Panel -Keep Tower Shortcut Panel -Monastery Shortcut Panel -Monastery Entry Left -Monastery Entry Right -Monastery Outside 3 -Town Cargo Box Entry Panel -Town Wooden Roof Lower Row 5 -Town Tinted Glass Door Panel -Town Church Entry Panel -Town Maze Stair Control -Town Windmill Entry Panel -Town Sound Room Right -Town Red Rooftop 5 -Town Church Lattice -Town Tall Hexagonal -Town Wooden Rooftop -Windmill Theater Entry Panel -Theater Exit Left Panel -Theater Exit Right Panel -Jungle Laser Shortcut Panel -Jungle Popup Wall Control -River Monastery Shortcut Panel -Bunker Entry Panel -Bunker Tinted Glass Door Panel -Bunker Glass Room 3 -Bunker UV Room 2 -Swamp Entry Panel -Swamp Platform Row 4 -Swamp Platform Shortcut Right Panel -Swamp Blue Underwater 5 -Swamp Between Bridges Near Row 4 -Swamp Cyan Underwater 5 -Swamp Red Underwater 4 -Swamp Beyond Rotating Bridge 4 -Swamp Beyond Rotating Bridge 4 -Swamp Laser Shortcut Right Panel -Treehouse First Door Panel -Treehouse Second Door Panel -Treehouse Third Door Panel -Treehouse Bridge Control -Treehouse Left Orange Bridge 15 -Treehouse Right Orange Bridge 12 -Treehouse Laser House Door Timer Outside Control -Treehouse Laser House Door Timer Inside -Mountain Floor 1 Left Row 7 -Mountain Floor 1 Right Row 5 -Mountain Floor 1 Back Row 3 -Mountain Floor 1 Trash Pillar 2 -Mountain Floor 2 Near Row 5 -Mountain Floor 2 Light Bridge Controller Near -Mountain Floor 2 Light Bridge Controller Far -Mountain Floor 2 Far Row 6 -Mountain Bottom Floor Giant Puzzle -Mountain Bottom Floor Final Room Entry Left -Mountain Bottom Floor Final Room Entry Right -Mountain Bottom Floor Discard -Mountain Bottom Floor Rock Control -Mountain Bottom Floor Caves Entry Panel -Caves Lone Pillar -Caves Mountain Shortcut Panel -Caves Swamp Shortcut Panel -Caves Challenge Entry Panel -Challenge Tunnels Entry Panel -Tunnels Theater Shortcut Panel -Tunnels Desert Shortcut Panel -Tunnels Town Shortcut Panel \ No newline at end of file diff --git a/worlds/witness/settings/EP_Shuffle/EP_All.txt b/worlds/witness/settings/EP_Shuffle/EP_All.txt index 51af5e38502e..939adc36e814 100644 --- a/worlds/witness/settings/EP_Shuffle/EP_All.txt +++ b/worlds/witness/settings/EP_Shuffle/EP_All.txt @@ -1,136 +1,136 @@ Added Locations: -0x0332B -0x03367 -0x28B8A -0x037B6 -0x037B2 -0x000F7 -0x3351D -0x0053C -0x00771 -0x335C8 -0x335C9 -0x337F8 -0x037BB -0x220E4 -0x220E5 -0x334B9 -0x334BC -0x22106 -0x0A14C -0x0A14D -0x03ABC -0x03ABE -0x03AC0 -0x03AC4 -0x03AC5 -0x03BE2 -0x03BE3 -0x0A409 -0x006E5 -0x006E6 -0x006E7 -0x034A7 -0x034AD -0x034AF -0x03DAB -0x03DAC -0x03DAD -0x03E01 -0x289F4 -0x289F5 -0x0053D -0x0053E -0x00769 -0x33721 -0x220A7 -0x220BD -0x03B22 -0x03B23 -0x03B24 -0x03B25 -0x03A79 -0x28ABD -0x28ABE -0x3388F -0x28B29 -0x28B2A -0x018B6 -0x033BE -0x033BF -0x033DD -0x033E5 -0x28AE9 -0x3348F -0x001A3 -0x335AE -0x000D3 -0x035F5 -0x09D5D -0x09D5E -0x09D63 -0x3370E -0x035DE -0x03601 -0x03603 -0x03D0D -0x3369A -0x336C8 -0x33505 -0x03A9E -0x016B2 -0x3365F -0x03731 -0x036CE -0x03C07 -0x03A93 -0x03AA6 -0x3397C -0x0105D -0x0A304 -0x035CB -0x035CF -0x28A7B -0x005F6 -0x00859 -0x17CB9 -0x28A4A -0x334B6 -0x0069D -0x00614 -0x28A4C -0x289CF -0x289D1 -0x33692 -0x03E77 -0x03E7C -0x035C7 -0x01848 -0x03D06 -0x33530 -0x33600 -0x28A2F -0x28A37 -0x334A3 -0x3352F -0x33857 -0x33879 -0x03C19 -0x28B30 -0x035C9 -0x03335 -0x03412 -0x038A6 -0x038AA -0x03E3F -0x03E40 -0x28B8E -0x28B91 -0x03BCE -0x03BCF -0x03BD1 -0x339B6 -0x33A20 -0x33A29 -0x33A2A -0x33B06 \ No newline at end of file +0x0332B (Glass Factory Black Line Reflection EP) +0x03367 (Glass Factory Black Line EP) +0x28B8A (Vase EP) +0x037B6 (Windmill First Blade EP) +0x037B2 (Windmill Second Blade EP) +0x000F7 (Windmill Third Blade EP) +0x3351D (Sand Snake EP) +0x0053C (Facade Right EP) +0x00771 (Facade Left EP) +0x335C8 (Stairs Left EP) +0x335C9 (Stairs Right EP) +0x337F8 (Flood Room EP) +0x037BB (Elevator EP) +0x220E4 (Broken Wall Straight EP) +0x220E5 (Broken Wall Bend EP) +0x334B9 (Shore EP) +0x334BC (Island EP) +0x22106 (Desert EP) +0x0A14C (Pond Room Near Reflection EP) +0x0A14D (Pond Room Far Reflection EP) +0x03ABC (Long Arch Moss EP) +0x03ABE (Straight Left Moss EP) +0x03AC0 (Pop-up Wall Moss EP) +0x03AC4 (Short Arch Moss EP) +0x03AC5 (Green Leaf Moss EP) +0x03BE2 (Monastery Garden Left EP) +0x03BE3 (Monastery Garden Right EP) +0x0A409 (Monastery Wall EP) +0x006E5 (Facade Left Near EP) +0x006E6 (Facade Left Far Short EP) +0x006E7 (Facade Left Far Long EP) +0x034A7 (Left Shutter EP) +0x034AD (Middle Shutter EP) +0x034AF (Right Shutter EP) +0x03DAB (Facade Right Near EP) +0x03DAC (Facade Left Stairs EP) +0x03DAD (Facade Right Stairs EP) +0x03E01 (Grass Stairs EP) +0x289F4 (Entrance EP) +0x289F5 (Tree Halo EP) +0x0053D (Rock Shadow EP) +0x0053E (Sand Shadow EP) +0x00769 (Burned House Beach EP) +0x33721 (Buoy EP) +0x220A7 (Right Orange Bridge EP) +0x220BD (Both Orange Bridges EP) +0x03B22 (Circle Far EP) +0x03B23 (Circle Left EP) +0x03B24 (Circle Near EP) +0x03B25 (Shipwreck CCW Underside EP) +0x03A79 (Stern EP) +0x28ABD (Rope Inner EP) +0x28ABE (Rope Outer EP) +0x3388F (Couch EP) +0x28B29 (Shipwreck Green EP) +0x28B2A (Shipwreck CW Underside EP) +0x018B6 (Pressure Plates 4 Right Exit EP) +0x033BE (Pressure Plates 1 EP) +0x033BF (Pressure Plates 2 EP) +0x033DD (Pressure Plates 3 EP) +0x033E5 (Pressure Plates 4 Left Exit EP) +0x28AE9 (Path EP) +0x3348F (Hedges EP) +0x001A3 (River Shape EP) +0x335AE (Cloud Cycle EP) +0x000D3 (Green Room Flowers EP) +0x035F5 (Tinted Door EP) +0x09D5D (Yellow Bridge EP) +0x09D5E (Blue Bridge EP) +0x09D63 (Pink Bridge EP) +0x3370E (Arch Black EP) +0x035DE (Purple Sand Bottom EP) +0x03601 (Purple Sand Top EP) +0x03603 (Purple Sand Middle EP) +0x03D0D (Bunker Yellow Line EP) +0x3369A (Arch White Left EP) +0x336C8 (Arch White Right EP) +0x33505 (Bush EP) +0x03A9E (Purple Underwater Right EP) +0x016B2 (Rotating Bridge CCW EP) +0x3365F (Boat EP) +0x03731 (Long Bridge Side EP) +0x036CE (Rotating Bridge CW EP) +0x03C07 (Apparent River EP) +0x03A93 (Purple Underwater Left EP) +0x03AA6 (Cyan Underwater Sliding Bridge EP) +0x3397C (Skylight EP) +0x0105D (Sliding Bridge Left EP) +0x0A304 (Sliding Bridge Right EP) +0x035CB (Bamboo CCW EP) +0x035CF (Bamboo CW EP) +0x28A7B (Quarry Stoneworks Rooftop Vent EP) +0x005F6 (Hook EP) +0x00859 (Moving Ramp EP) +0x17CB9 (Railroad EP) +0x28A4A (Shore EP) +0x334B6 (Entrance Pipe EP) +0x0069D (Ramp EP) +0x00614 (Lift EP) +0x28A4C (Sand Pile EP) +0x289CF (Rock Line EP) +0x289D1 (Rock Line Reflection EP) +0x33692 (Brown Bridge EP) +0x03E77 (Red Flowers EP) +0x03E7C (Purple Flowers EP) +0x035C7 (Tractor EP) +0x01848 (EP) +0x03D06 (Garden EP) +0x33530 (Cloud EP) +0x33600 (Patio Flowers EP) +0x28A2F (Town Sewer EP) +0x28A37 (Town Long Sewer EP) +0x334A3 (Path EP) +0x3352F (Gate EP) +0x33857 (Tutorial EP) +0x33879 (Tutorial Reflection EP) +0x03C19 (Tutorial Moss EP) +0x28B30 (Water EP) +0x035C9 (Cargo Box EP) +0x03335 (Tower Underside Third EP) +0x03412 (Tower Underside Fourth EP) +0x038A6 (Tower Underside First EP) +0x038AA (Tower Underside Second EP) +0x03E3F (RGB House Red EP) +0x03E40 (RGB House Green EP) +0x28B8E (Maze Bridge Underside EP) +0x28B91 (Thundercloud EP) +0x03BCE (Black Line Tower EP) +0x03BCF (Black Line Redirect EP) +0x03BD1 (Black Line Church EP) +0x339B6 (Eclipse EP) +0x33A20 (Theater Flowers EP) +0x33A29 (Window EP) +0x33A2A (Door EP) +0x33B06 (Church EP) diff --git a/worlds/witness/settings/EP_Shuffle/EP_Easy.txt b/worlds/witness/settings/EP_Shuffle/EP_Easy.txt index 939055169a75..6f9c80fc0a94 100644 --- a/worlds/witness/settings/EP_Shuffle/EP_Easy.txt +++ b/worlds/witness/settings/EP_Shuffle/EP_Easy.txt @@ -1,14 +1,17 @@ -Precompleted Locations: -0x339B6 -0x335AE -0x3388F -0x33A20 -0x037B2 -0x000F7 -0x28B29 -0x33857 -0x33879 -0x016B2 -0x036CE -0x03B25 -0x28B2A \ No newline at end of file +Disabled Locations: +0x339B6 (Eclipse EP) +0x335AE (Cloud Cycle EP) +0x3388F (Couch EP) +0x33A20 (Theater Flowers EP) +0x037B2 (Windmill Second Blade EP) +0x000F7 (Windmill Third Blade EP) +0x28B29 (Shipwreck Green EP) +0x33857 (Tutorial EP) +0x33879 (Tutorial Reflection EP) +0x016B2 (Rotating Bridge CCW EP) +0x036CE (Rotating Bridge CW EP) +0x03B25 (Shipwreck CCW Underside EP) +0x28B2A (Shipwreck CW Underside EP) +0x09D63 (Mountain Pink Bridge EP) +0x09D5E (Mountain Blue Bridge EP) +0x09D5D (Mountain Yellow Bridge EP) diff --git a/worlds/witness/settings/EP_Shuffle/EP_NoCavesEPs.txt b/worlds/witness/settings/EP_Shuffle/EP_NoCavesEPs.txt deleted file mode 100644 index 3bb318a69ee9..000000000000 --- a/worlds/witness/settings/EP_Shuffle/EP_NoCavesEPs.txt +++ /dev/null @@ -1,5 +0,0 @@ -Precompleted Locations: -0x3397C -0x33A20 -0x3352F -0x28B30 \ No newline at end of file diff --git a/worlds/witness/settings/EP_Shuffle/EP_NoEclipse.txt b/worlds/witness/settings/EP_Shuffle/EP_NoEclipse.txt index d41badfa1802..f241957c823a 100644 --- a/worlds/witness/settings/EP_Shuffle/EP_NoEclipse.txt +++ b/worlds/witness/settings/EP_Shuffle/EP_NoEclipse.txt @@ -1,2 +1,2 @@ -Precompleted Locations: -0x339B6 \ No newline at end of file +Disabled Locations: +0x339B6 (Eclipse EP) diff --git a/worlds/witness/settings/EP_Shuffle/EP_NoMountainEPs.txt b/worlds/witness/settings/EP_Shuffle/EP_NoMountainEPs.txt deleted file mode 100644 index 3558d77ad888..000000000000 --- a/worlds/witness/settings/EP_Shuffle/EP_NoMountainEPs.txt +++ /dev/null @@ -1,4 +0,0 @@ -Precompleted Locations: -0x09D63 -0x09D5D -0x09D5E \ No newline at end of file diff --git a/worlds/witness/settings/EP_Shuffle/EP_Sides.txt b/worlds/witness/settings/EP_Shuffle/EP_Sides.txt index 1da52ffb8959..82ab63329500 100644 --- a/worlds/witness/settings/EP_Shuffle/EP_Sides.txt +++ b/worlds/witness/settings/EP_Shuffle/EP_Sides.txt @@ -1,34 +1,35 @@ Added Locations: -0xFFE00 -0xFFE01 -0xFFE02 -0xFFE03 -0xFFE04 -0xFFE10 -0xFFE11 -0xFFE12 -0xFFE13 -0xFFE14 -0xFFE15 -0xFFE20 -0xFFE21 -0xFFE22 -0xFFE23 -0xFFE24 -0xFFE25 -0xFFE30 -0xFFE31 -0xFFE32 -0xFFE33 -0xFFE34 -0xFFE35 -0xFFE40 -0xFFE41 -0xFFE42 -0xFFE43 -0xFFE50 -0xFFE51 -0xFFE52 -0xFFE53 -0xFFE54 -0xFFE55 \ No newline at end of file +0xFFE00 (Desert Obelisk Side 1) +0xFFE01 (Desert Obelisk Side 2) +0xFFE02 (Desert Obelisk Side 3) +0xFFE03 (Desert Obelisk Side 4) +0xFFE04 (Desert Obelisk Side 5) +0xFFE10 (Monastery Obelisk Side 1) +0xFFE11 (Monastery Obelisk Side 2) +0xFFE12 (Monastery Obelisk Side 3) +0xFFE13 (Monastery Obelisk Side 4) +0xFFE14 (Monastery Obelisk Side 5) +0xFFE15 (Monastery Obelisk Side 6) +0xFFE20 (Treehouse Obelisk Side 1) +0xFFE21 (Treehouse Obelisk Side 2) +0xFFE22 (Treehouse Obelisk Side 3) +0xFFE23 (Treehouse Obelisk Side 4) +0xFFE24 (Treehouse Obelisk Side 5) +0xFFE25 (Treehouse Obelisk Side 6) +0xFFE30 (River Obelisk Side 1) +0xFFE31 (River Obelisk Side 2) +0xFFE32 (River Obelisk Side 3) +0xFFE33 (River Obelisk Side 4) +0xFFE34 (River Obelisk Side 5) +0xFFE35 (River Obelisk Side 6) +0xFFE40 (Quarry Obelisk Side 1) +0xFFE41 (Quarry Obelisk Side 2) +0xFFE42 (Quarry Obelisk Side 3) +0xFFE43 (Quarry Obelisk Side 4) +0xFFE44 (Quarry Obelisk Side 5) +0xFFE50 (Town Obelisk Side 1) +0xFFE51 (Town Obelisk Side 2) +0xFFE52 (Town Obelisk Side 3) +0xFFE53 (Town Obelisk Side 4) +0xFFE54 (Town Obelisk Side 5) +0xFFE55 (Town Obelisk Side 6) diff --git a/worlds/witness/settings/EP_Shuffle/EP_Videos.txt b/worlds/witness/settings/EP_Shuffle/EP_Videos.txt deleted file mode 100644 index c4aaca13a676..000000000000 --- a/worlds/witness/settings/EP_Shuffle/EP_Videos.txt +++ /dev/null @@ -1,6 +0,0 @@ -Precompleted Locations: -0x339B6 -0x33A29 -0x33A2A -0x33B06 -0x33A20 \ No newline at end of file diff --git a/worlds/witness/settings/Early_Caves.txt b/worlds/witness/settings/Early_Caves.txt new file mode 100644 index 000000000000..48c8056bc7b6 --- /dev/null +++ b/worlds/witness/settings/Early_Caves.txt @@ -0,0 +1,6 @@ +Items: +Caves Shortcuts + +Remove Items: +Caves Mountain Shortcut (Door) +Caves Swamp Shortcut (Door) \ No newline at end of file diff --git a/worlds/witness/settings/Early_UTM.txt b/worlds/witness/settings/Early_Caves_Start.txt similarity index 65% rename from worlds/witness/settings/Early_UTM.txt rename to worlds/witness/settings/Early_Caves_Start.txt index b04aa3d33916..a16a6d02bb9f 100644 --- a/worlds/witness/settings/Early_UTM.txt +++ b/worlds/witness/settings/Early_Caves_Start.txt @@ -1,8 +1,8 @@ Items: -Caves Exits to Main Island +Caves Shortcuts Starting Inventory: -Caves Exits to Main Island +Caves Shortcuts Remove Items: Caves Mountain Shortcut (Door) diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Exclusions/Disable_Unrandomized.txt similarity index 70% rename from worlds/witness/settings/Disable_Unrandomized.txt rename to worlds/witness/settings/Exclusions/Disable_Unrandomized.txt index 3cd7ec1fb5eb..2419bde06c14 100644 --- a/worlds/witness/settings/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Exclusions/Disable_Unrandomized.txt @@ -2,16 +2,20 @@ Event Items: Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9 Bunker Laser Activation - 0x00061,0x17D01,0x17C42 Shadows Laser Activation - 0x00021,0x17D28,0x17C71 +Town Tower 4th Door Opens - 0x17CFB,0x3C12B,0x17CF7 +Jungle Popup Wall Lifts - 0x17FA0,0x17D27,0x17F9B,0x17CAB Requirement Changes: 0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9 0x0C2B2 - 0x00061 | 0x17D01 | 0x17C42 0x181B3 - 0x00021 | 0x17D28 | 0x17C71 -0x28B39 - True - Reflection 0x17CAB - True - True +0x17CA4 - True - True +0x1475B - 0x17FA0 | 0x17D27 | 0x17F9B | 0x17CAB 0x2779A - 0x17CFB | 0x3C12B | 0x17CF7 Disabled Locations: +0x28B39 (Town Tall Hexagonal) 0x03505 (Tutorial Gate Close) 0x0C335 (Tutorial Pillar) 0x0C373 (Tutorial Patio Floor) @@ -25,6 +29,11 @@ Disabled Locations: 0x00055 (Orchard Apple Tree 3) 0x032F7 (Orchard Apple Tree 4) 0x032FF (Orchard Apple Tree 5) +0x334DB (Door Timer Outside) +0x334DC (Door Timer Inside) +0x19B24 (Timed Door) - 0x334DB +0x194B2 (Laser Entry Right) +0x19665 (Laser Entry Left) 0x198B5 (Shadows Intro 1) 0x198BD (Shadows Intro 2) 0x198BF (Shadows Intro 3) @@ -47,11 +56,22 @@ Disabled Locations: 0x197E8 (Shadows Near 4) 0x197E5 (Shadows Near 5) 0x19650 (Shadows Laser) +0x19865 (Quarry Barrier) +0x0A2DF (Quarry Barrier 2) +0x1855B (Ledge Barrier) +0x19ADE (Ledge Barrier 2) 0x00139 (Keep Hedge Maze 1) 0x019DC (Keep Hedge Maze 2) 0x019E7 (Keep Hedge Maze 3) 0x01A0F (Keep Hedge Maze 4) 0x0360E (Laser Hedges) +0x01954 (Hedge 1 Exit) +0x018CE (Hedge 2 Shortcut) +0x019D8 (Hedge 2 Exit) +0x019B5 (Hedge 3 Shortcut) +0x019E6 (Hedge 3 Exit) +0x0199A (Hedge 4 Shortcut) +0x01A0E (Hedge 4 Exit) 0x03307 (First Gate) 0x03313 (Second Gate) 0x0C128 (Entry Inner) @@ -61,16 +81,20 @@ Disabled Locations: 0x00290 (Monastery Outside 1) 0x00038 (Monastery Outside 2) 0x00037 (Monastery Outside 3) +0x03750 (Garden Entry) +0x09D9B (Monastery Shutters Control) 0x193A7 (Monastery Inside 1) 0x193AA (Monastery Inside 2) 0x193AB (Monastery Inside 3) 0x193A6 (Monastery Inside 4) -0x17CA4 (Monastery Laser) +0x17CA4 (Monastery Laser Panel) +0x0364E (Monastery Laser Shortcut Door) +0x03713 (Monastery Laser Shortcut Panel) 0x18590 (Transparent) - True - Symmetry & Environment 0x28AE3 (Vines) - 0x18590 - Shadows Follow & Environment 0x28938 (Apple Tree) - 0x28AE3 - Environment 0x079DF (Triple Exit) - 0x28938 - Shadows Avoid & Environment & Reflection -0x28B39 (Tall Hexagonal) - 0x079DF & 0x2896A - Reflection +0x00815 (Theater Video Input) 0x03553 (Theater Tutorial Video) 0x03552 (Theater Desert Video) 0x0354E (Theater Jungle Video) @@ -84,7 +108,8 @@ Disabled Locations: 0x0070F (Second Row 2) 0x0087D (Second Row 3) 0x002C7 (Second Row 4) -0x17CAA (Monastery Shortcut Panel) +0x17CAA (Monastery Garden Shortcut Panel) +0x0CF2A (Monastery Garden Shortcut) 0x0C2A4 (Bunker Entry) 0x17C79 (Tinted Glass Door) 0x0C2A3 (UV Room Entry) @@ -110,19 +135,16 @@ Disabled Locations: 0x09DE0 (Bunker Laser) 0x0A079 (Bunker Elevator Control) -0x17CAA (River Garden Entry Panel) - -Precompleted Locations: -0x034A7 -0x034AD -0x034AF -0x339B6 -0x33A29 -0x33A2A -0x33B06 -0x3352F -0x33600 -0x035F5 -0x000D3 -0x33A20 -0x03BE2 +0x034A7 (Monastery Left Shutter EP) +0x034AD (Monastery Middle Shutter EP) +0x034AF (Monastery Right Shutter EP) +0x339B6 (Theater Eclipse EP) +0x33A29 (Theater Window EP) +0x33A2A (Theater Door EP) +0x33B06 (Theater Church EP) +0x3352F (Tutorial Gate EP) +0x33600 (Tutorial Patio Flowers EP) +0x035F5 (Bunker Tinted Door EP) +0x000D3 (Bunker Green Room Flowers EP) +0x33A20 (Theater Flowers EP) +0x03BE2 (Monastery Garden Left EP) diff --git a/worlds/witness/settings/Exclusions/Discards.txt b/worlds/witness/settings/Exclusions/Discards.txt new file mode 100644 index 000000000000..e46d1dd82b1b --- /dev/null +++ b/worlds/witness/settings/Exclusions/Discards.txt @@ -0,0 +1,15 @@ +Disabled Locations: +0x17CFB (Outside Tutorial Discard) +0x3C12B (Glass Factory Discard) +0x17CE7 (Desert Discard) +0x17CF0 (Quarry Discard) +0x17D27 (Keep Discard) +0x17D28 (Shipwreck Discard) +0x17D01 (Town Cargo Box Discard) +0x17C71 (Town Rooftop Discard) +0x17CF7 (Theater Discard) +0x17F9B (Jungle Discard) +0x17FA9 (Treehouse Green Bridge Discard) +0x17C42 (Mountainside Discard) +0x17F93 (Mountain Floor 2 Elevator Discard) +0x17FA0 (Treehouse Laser Discard) diff --git a/worlds/witness/settings/Exclusions/Vaults.txt b/worlds/witness/settings/Exclusions/Vaults.txt new file mode 100644 index 000000000000..f23a13183326 --- /dev/null +++ b/worlds/witness/settings/Exclusions/Vaults.txt @@ -0,0 +1,31 @@ +Disabled Locations: +0x033D4 (Outside Tutorial Vault) +0x03481 (Outside Tutorial Vault Box) +0x033D0 (Outside Tutorial Vault Door) +0x0CC7B (Desert Vault) +0x0339E (Desert Vault Box) +0x03444 (Desert Vault Door) +0x00AFB (Shipwreck Vault) +0x03535 (Shipwreck Vault Box) +0x17BB4 (Shipwreck Vault Door) +0x15ADD (River Vault) +0x03702 (River Vault Box) +0x15287 (River Vault Door) +0x002A6 (Mountainside Vault) +0x03542 (Mountainside Vault Box) +0x00085 (Mountainside Vault Door) +0x2FAF6 (Tunnels Vault Box) +0x00815 (Theater Video Input) +0x03553 (Theater Tutorial Video) +0x03552 (Theater Desert Video) +0x0354E (Theater Jungle Video) +0x03549 (Theater Challenge Video) +0x0354F (Theater Shipwreck Video) +0x03545 (Theater Mountain Video) +0x03505 (Tutorial Gate Close) +0x339B6 (Theater clipse EP) +0x33A29 (Theater Window EP) +0x33A2A (Theater Door EP) +0x33B06 (Theater Church EP) +0x33A20 (Theater Flowers EP) +0x3352F (Tutorial Gate EP) diff --git a/worlds/witness/settings/Postgame/Beyond_Challenge.txt b/worlds/witness/settings/Postgame/Beyond_Challenge.txt new file mode 100644 index 000000000000..5cd20b6a5e40 --- /dev/null +++ b/worlds/witness/settings/Postgame/Beyond_Challenge.txt @@ -0,0 +1,4 @@ +Disabled Locations: +0x03549 (Challenge Video) + +0x339B6 (Eclipse EP) diff --git a/worlds/witness/settings/Postgame/Bottom_Floor_Discard.txt b/worlds/witness/settings/Postgame/Bottom_Floor_Discard.txt new file mode 100644 index 000000000000..8f7d6a257a53 --- /dev/null +++ b/worlds/witness/settings/Postgame/Bottom_Floor_Discard.txt @@ -0,0 +1,2 @@ +Disabled Locations: +0x17FA2 (Mountain Bottom Floor Discard) diff --git a/worlds/witness/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt b/worlds/witness/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt new file mode 100644 index 000000000000..5ea7c578d8bf --- /dev/null +++ b/worlds/witness/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt @@ -0,0 +1,6 @@ +Disabled Locations: +0x17FA2 (Mountain Bottom Floor Discard) +0x17F33 (Rock Open Door) +0x00FF8 (Caves Entry Panel) +0x334E1 (Rock Control) +0x2D77D (Caves Entry Door) diff --git a/worlds/witness/settings/Postgame/Caves.txt b/worlds/witness/settings/Postgame/Caves.txt new file mode 100644 index 000000000000..aadb4c3f96e7 --- /dev/null +++ b/worlds/witness/settings/Postgame/Caves.txt @@ -0,0 +1,65 @@ +Disabled Locations: +0x335AB (Elevator Inside Control) +0x335AC (Elevator Upper Outside Control) +0x3369D (Elevator Lower Outside Control) +0x00190 (Blue Tunnel Right First 1) +0x00558 (Blue Tunnel Right First 2) +0x00567 (Blue Tunnel Right First 3) +0x006FE (Blue Tunnel Right First 4) +0x01A0D (Blue Tunnel Left First 1) +0x008B8 (Blue Tunnel Left Second 1) +0x00973 (Blue Tunnel Left Second 2) +0x0097B (Blue Tunnel Left Second 3) +0x0097D (Blue Tunnel Left Second 4) +0x0097E (Blue Tunnel Left Second 5) +0x00994 (Blue Tunnel Right Second 1) +0x334D5 (Blue Tunnel Right Second 2) +0x00995 (Blue Tunnel Right Second 3) +0x00996 (Blue Tunnel Right Second 4) +0x00998 (Blue Tunnel Right Second 5) +0x009A4 (Blue Tunnel Left Third 1) +0x018A0 (Blue Tunnel Right Third 1) +0x00A72 (Blue Tunnel Left Fourth 1) +0x32962 (First Floor Left) +0x32966 (First Floor Grounded) +0x01A31 (First Floor Middle) +0x00B71 (First Floor Right) +0x288EA (First Wooden Beam) +0x288FC (Second Wooden Beam) +0x289E7 (Third Wooden Beam) +0x288AA (Fourth Wooden Beam) +0x17FB9 (Left Upstairs Single) +0x0A16B (Left Upstairs Left Row 1) +0x0A2CE (Left Upstairs Left Row 2) +0x0A2D7 (Left Upstairs Left Row 3) +0x0A2DD (Left Upstairs Left Row 4) +0x0A2EA (Left Upstairs Left Row 5) +0x0008F (Right Upstairs Left Row 1) +0x0006B (Right Upstairs Left Row 2) +0x0008B (Right Upstairs Left Row 3) +0x0008C (Right Upstairs Left Row 4) +0x0008A (Right Upstairs Left Row 5) +0x00089 (Right Upstairs Left Row 6) +0x0006A (Right Upstairs Left Row 7) +0x0006C (Right Upstairs Left Row 8) +0x00027 (Right Upstairs Right Row 1) +0x00028 (Right Upstairs Right Row 2) +0x00029 (Right Upstairs Right Row 3) +0x021D7 (Mountain Shortcut Panel) +0x2D73F (Mountain Shortcut Door) +0x17CF2 (Swamp Shortcut Panel) +0x2D859 (Swamp Shortcut Door) +0x039B4 (Tunnels Entry Panel) +0x0348A (Tunnels Entry Door) +0x2FAF6 (Vault Box) +0x27732 (Tunnels Theater Shortcut Panel) +0x27739 (Tunnels Theater Shortcut Door) +0x2773D (Tunnels Desert Shortcut Panel) +0x27263 (Tunnels Desert Shortcut Door) +0x09E85 (Tunnels Town Shortcut Panel) +0x09E87 (Tunnels Town Shortcut Door) + +0x3397C (Skylight EP) +0x28B30 (Water EP) +0x33A20 (Theater Flowers EP) +0x3352F (Gate EP) diff --git a/worlds/witness/settings/Postgame/Challenge_Vault_Box.txt b/worlds/witness/settings/Postgame/Challenge_Vault_Box.txt new file mode 100644 index 000000000000..d65900418c61 --- /dev/null +++ b/worlds/witness/settings/Postgame/Challenge_Vault_Box.txt @@ -0,0 +1,3 @@ +Disabled Locations: +0x0356B (Challenge Vault Box) +0x04D75 (Vault Door) diff --git a/worlds/witness/settings/Postgame/Mountain_Lower.txt b/worlds/witness/settings/Postgame/Mountain_Lower.txt new file mode 100644 index 000000000000..354e3feb82c3 --- /dev/null +++ b/worlds/witness/settings/Postgame/Mountain_Lower.txt @@ -0,0 +1,27 @@ +Disabled Locations: +0x17F93 (Elevator Discard) +0x09EEB (Elevator Control Panel) +0x09FC1 (Giant Puzzle Bottom Left) +0x09F8E (Giant Puzzle Bottom Right) +0x09F01 (Giant Puzzle Top Right) +0x09EFF (Giant Puzzle Top Left) +0x09FDA (Giant Puzzle) +0x09F89 (Exit Door) +0x01983 (Final Room Entry Left) +0x01987 (Final Room Entry Right) +0x0C141 (Final Room Entry Door) +0x0383A (Right Pillar 1) +0x09E56 (Right Pillar 2) +0x09E5A (Right Pillar 3) +0x33961 (Right Pillar 4) +0x0383D (Left Pillar 1) +0x0383F (Left Pillar 2) +0x03859 (Left Pillar 3) +0x339BB (Left Pillar 4) +0x3D9A6 (Elevator Door Closer Left) +0x3D9A7 (Elevator Door Close Right) +0x3C113 (Elevator Entry Left) +0x3C114 (Elevator Entry Right) +0x3D9AA (Back Wall Left) +0x3D9A8 (Back Wall Right) +0x3D9A9 (Elevator Start) diff --git a/worlds/witness/settings/Postgame/Mountain_Upper.txt b/worlds/witness/settings/Postgame/Mountain_Upper.txt new file mode 100644 index 000000000000..e2b0765f533c --- /dev/null +++ b/worlds/witness/settings/Postgame/Mountain_Upper.txt @@ -0,0 +1,41 @@ +Disabled Locations: +0x17C34 (Mountain Entry Panel) +0x09E39 (Light Bridge Controller) +0x09E7A (Right Row 1) +0x09E71 (Right Row 2) +0x09E72 (Right Row 3) +0x09E69 (Right Row 4) +0x09E7B (Right Row 5) +0x09E73 (Left Row 1) +0x09E75 (Left Row 2) +0x09E78 (Left Row 3) +0x09E79 (Left Row 4) +0x09E6C (Left Row 5) +0x09E6F (Left Row 6) +0x09E6B (Left Row 7) +0x33AF5 (Back Row 1) +0x33AF7 (Back Row 2) +0x09F6E (Back Row 3) +0x09EAD (Trash Pillar 1) +0x09EAF (Trash Pillar 2) +0x09E54 (Mountain Floor 1 Exit Door) +0x09FD3 (Near Row 1) +0x09FD4 (Near Row 2) +0x09FD6 (Near Row 3) +0x09FD7 (Near Row 4) +0x09FD8 (Near Row 5) +0x09FFB (Staircase Near Door) +0x09EDD (Elevator Room Entry Door) +0x09E86 (Light Bridge Controller Near) +0x09FCC (Far Row 1) +0x09FCE (Far Row 2) +0x09FCF (Far Row 3) +0x09FD0 (Far Row 4) +0x09FD1 (Far Row 5) +0x09FD2 (Far Row 6) +0x09E07 (Staircase Far Door) +0x09ED8 (Light Bridge Controller Far) + +0x09D63 (Pink Bridge EP) +0x09D5D (Yellow Bridge EP) +0x09D5E (Blue Bridge EP) diff --git a/worlds/witness/settings/Postgame/Path_To_Challenge.txt b/worlds/witness/settings/Postgame/Path_To_Challenge.txt new file mode 100644 index 000000000000..3f9239cc4832 --- /dev/null +++ b/worlds/witness/settings/Postgame/Path_To_Challenge.txt @@ -0,0 +1,30 @@ +Disabled Locations: +0x0356B (Vault Box) +0x04D75 (Vault Door) +0x17F33 (Rock Open Door) +0x00FF8 (Caves Entry Panel) +0x334E1 (Rock Control) +0x2D77D (Caves Entry Door) +0x09DD5 (Lone Pillar) +0x019A5 (Caves Pillar Door) +0x0A16E (Challenge Entry Panel) +0x0A19A (Challenge Entry Door) +0x0A332 (Start Timer) +0x0088E (Small Basic) +0x00BAF (Big Basic) +0x00BF3 (Square) +0x00C09 (Maze Map) +0x00CDB (Stars and Dots) +0x0051F (Symmetry) +0x00524 (Stars and Shapers) +0x00CD4 (Big Basic 2) +0x00CB9 (Choice Squares Right) +0x00CA1 (Choice Squares Middle) +0x00C80 (Choice Squares Left) +0x00C68 (Choice Squares 2 Right) +0x00C59 (Choice Squares 2 Middle) +0x00C22 (Choice Squares 2 Left) +0x034F4 (Maze Hidden 1) +0x034EC (Maze Hidden 2) +0x1C31A (Dots Pillar) +0x1C319 (Squares Pillar) diff --git a/worlds/witness/static_logic.py b/worlds/witness/static_logic.py index 8dc2a05de56d..29c171d45c33 100644 --- a/worlds/witness/static_logic.py +++ b/worlds/witness/static_logic.py @@ -73,31 +73,34 @@ def read_logic_file(self, lines): location_id = line_split.pop(0) - check_name_full = line_split.pop(0) + entity_name_full = line_split.pop(0) - check_hex = check_name_full[0:7] - check_name = check_name_full[9:-1] + entity_hex = entity_name_full[0:7] + entity_name = entity_name_full[9:-1] required_panel_lambda = line_split.pop(0) - full_check_name = current_region["shortName"] + " " + check_name + full_entity_name = current_region["shortName"] + " " + entity_name if location_id == "Door" or location_id == "Laser": - self.CHECKS_BY_HEX[check_hex] = { - "checkName": full_check_name, - "checkHex": check_hex, - "region": current_region, + self.ENTITIES_BY_HEX[entity_hex] = { + "checkName": full_entity_name, + "entity_hex": entity_hex, + "region": None, "id": None, - "panelType": location_id + "entityType": location_id } - self.CHECKS_BY_NAME[self.CHECKS_BY_HEX[check_hex]["checkName"]] = self.CHECKS_BY_HEX[check_hex] + self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex] - self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[check_hex] = { + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = { "panels": parse_lambda(required_panel_lambda) } - current_region["panels"].append(check_hex) + # Lasers and Doors exist in a region, but don't have a regional *requirement* + # If a laser is activated, you don't need to physically walk up to it for it to count + # As such, logically, they behave more as if they were part of the "Entry" region + self.ALL_REGIONS_BY_NAME["Entry"]["panels"].append(entity_hex) continue required_item_lambda = line_split.pop(0) @@ -108,18 +111,18 @@ def read_logic_file(self, lines): "Laser Pressure Plates", "Desert Laser Redirect" } - is_vault_or_video = "Vault" in check_name or "Video" in check_name + is_vault_or_video = "Vault" in entity_name or "Video" in entity_name - if "Discard" in check_name: + if "Discard" in entity_name: location_type = "Discard" - elif is_vault_or_video or check_name == "Tutorial Gate Close": + elif is_vault_or_video or entity_name == "Tutorial Gate Close": location_type = "Vault" - elif check_name in laser_names: + elif entity_name in laser_names: location_type = "Laser" - elif "Obelisk Side" in check_name: + elif "Obelisk Side" in entity_name: location_type = "Obelisk Side" - full_check_name = check_name - elif "EP" in check_name: + full_entity_name = entity_name + elif "EP" in entity_name: location_type = "EP" else: location_type = "General" @@ -140,32 +143,35 @@ def read_logic_file(self, lines): eps_ints = {int(h, 16) for h in eps} - self.OBELISK_SIDE_ID_TO_EP_HEXES[int(check_hex, 16)] = eps_ints + self.OBELISK_SIDE_ID_TO_EP_HEXES[int(entity_hex, 16)] = eps_ints for ep_hex in eps: - self.EP_TO_OBELISK_SIDE[ep_hex] = check_hex + self.EP_TO_OBELISK_SIDE[ep_hex] = entity_hex - self.CHECKS_BY_HEX[check_hex] = { - "checkName": full_check_name, - "checkHex": check_hex, + self.ENTITIES_BY_HEX[entity_hex] = { + "checkName": full_entity_name, + "entity_hex": entity_hex, "region": current_region, "id": int(location_id), - "panelType": location_type + "entityType": location_type } - self.ENTITY_ID_TO_NAME[check_hex] = full_check_name + self.ENTITY_ID_TO_NAME[entity_hex] = full_entity_name - self.CHECKS_BY_NAME[self.CHECKS_BY_HEX[check_hex]["checkName"]] = self.CHECKS_BY_HEX[check_hex] - self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[check_hex] = requirement + self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex] + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = requirement - current_region["panels"].append(check_hex) + current_region["panels"].append(entity_hex) + + def __init__(self, lines=None): + if lines is None: + lines = get_sigma_normal_logic() - def __init__(self, lines=get_sigma_normal_logic()): # All regions with a list of panels in them and the connections to other regions, before logic adjustments self.ALL_REGIONS_BY_NAME = dict() self.STATIC_CONNECTIONS_BY_REGION_NAME = dict() - self.CHECKS_BY_HEX = dict() - self.CHECKS_BY_NAME = dict() + self.ENTITIES_BY_HEX = dict() + self.ENTITIES_BY_NAME = dict() self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() self.OBELISK_SIDE_ID_TO_EP_HEXES = dict() @@ -187,8 +193,8 @@ class StaticWitnessLogic: OBELISK_SIDE_ID_TO_EP_HEXES = dict() - CHECKS_BY_HEX = dict() - CHECKS_BY_NAME = dict() + ENTITIES_BY_HEX = dict() + ENTITIES_BY_NAME = dict() STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() EP_TO_OBELISK_SIDE = dict() @@ -262,8 +268,8 @@ def __init__(self): self.ALL_REGIONS_BY_NAME.update(self.sigma_normal.ALL_REGIONS_BY_NAME) self.STATIC_CONNECTIONS_BY_REGION_NAME.update(self.sigma_normal.STATIC_CONNECTIONS_BY_REGION_NAME) - self.CHECKS_BY_HEX.update(self.sigma_normal.CHECKS_BY_HEX) - self.CHECKS_BY_NAME.update(self.sigma_normal.CHECKS_BY_NAME) + self.ENTITIES_BY_HEX.update(self.sigma_normal.ENTITIES_BY_HEX) + self.ENTITIES_BY_NAME.update(self.sigma_normal.ENTITIES_BY_NAME) self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX.update(self.sigma_normal.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) self.OBELISK_SIDE_ID_TO_EP_HEXES.update(self.sigma_normal.OBELISK_SIDE_ID_TO_EP_HEXES) diff --git a/worlds/witness/utils.py b/worlds/witness/utils.py index 72dc10fd0617..fbb670fd0877 100644 --- a/worlds/witness/utils.py +++ b/worlds/witness/utils.py @@ -1,6 +1,6 @@ from functools import lru_cache from math import floor -from typing import List, Collection +from typing import List, Collection, FrozenSet, Tuple, Dict, Any, Set from pkgutil import get_data @@ -33,7 +33,7 @@ def build_weighted_int_list(inputs: Collection[float], total: int) -> List[int]: return rounded_output -def define_new_region(region_string): +def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str, FrozenSet[FrozenSet[str]]]]]: """ Returns a region object by parsing a line in the logic file """ @@ -66,7 +66,7 @@ def define_new_region(region_string): return region_obj, options -def parse_lambda(lambda_string): +def parse_lambda(lambda_string) -> FrozenSet[FrozenSet[str]]: """ Turns a lambda String literal like this: a | b & c into a set of sets like this: {{a}, {b, c}} @@ -97,86 +97,168 @@ def __get__(self, instance, class_): @lru_cache(maxsize=None) -def get_adjustment_file(adjustment_file): +def get_adjustment_file(adjustment_file: str) -> List[str]: data = get_data(__name__, adjustment_file).decode('utf-8') return [line.strip() for line in data.split("\n")] -def get_disable_unrandomized_list(): - return get_adjustment_file("settings/Disable_Unrandomized.txt") +def get_disable_unrandomized_list() -> List[str]: + return get_adjustment_file("settings/Exclusions/Disable_Unrandomized.txt") -def get_early_utm_list(): - return get_adjustment_file("settings/Early_UTM.txt") +def get_early_caves_list() -> List[str]: + return get_adjustment_file("settings/Early_Caves.txt") -def get_symbol_shuffle_list(): +def get_early_caves_start_list() -> List[str]: + return get_adjustment_file("settings/Early_Caves_Start.txt") + + +def get_symbol_shuffle_list() -> List[str]: return get_adjustment_file("settings/Symbol_Shuffle.txt") -def get_door_panel_shuffle_list(): - return get_adjustment_file("settings/Door_Panel_Shuffle.txt") +def get_complex_doors() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Complex_Doors.txt") + + +def get_simple_doors() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Simple_Doors.txt") + +def get_complex_door_panels() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Complex_Door_Panels.txt") -def get_doors_simple_list(): - return get_adjustment_file("settings/Doors_Simple.txt") +def get_complex_additional_panels() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Complex_Additional_Panels.txt") -def get_doors_complex_list(): - return get_adjustment_file("settings/Doors_Complex.txt") +def get_simple_panels() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Simple_Panels.txt") -def get_doors_max_list(): - return get_adjustment_file("settings/Doors_Max.txt") +def get_simple_additional_panels() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Simple_Additional_Panels.txt") -def get_laser_shuffle(): + +def get_boat() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Boat.txt") + + +def get_laser_shuffle() -> List[str]: return get_adjustment_file("settings/Laser_Shuffle.txt") -def get_audio_logs(): +def get_audio_logs() -> List[str]: return get_adjustment_file("settings/Audio_Logs.txt") -def get_ep_all_individual(): +def get_ep_all_individual() -> List[str]: return get_adjustment_file("settings/EP_Shuffle/EP_All.txt") -def get_ep_obelisks(): +def get_ep_obelisks() -> List[str]: return get_adjustment_file("settings/EP_Shuffle/EP_Sides.txt") -def get_ep_easy(): +def get_ep_easy() -> List[str]: return get_adjustment_file("settings/EP_Shuffle/EP_Easy.txt") -def get_ep_no_eclipse(): +def get_ep_no_eclipse() -> List[str]: return get_adjustment_file("settings/EP_Shuffle/EP_NoEclipse.txt") -def get_ep_no_caves(): - return get_adjustment_file("settings/EP_Shuffle/EP_NoCavesEPs.txt") +def get_vault_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Exclusions/Vaults.txt") + + +def get_discard_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Exclusions/Discards.txt") + + +def get_caves_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Caves.txt") + + +def get_beyond_challenge_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Beyond_Challenge.txt") + + +def get_bottom_floor_discard_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Bottom_Floor_Discard.txt") + + +def get_bottom_floor_discard_nondoors_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Bottom_Floor_Discard_NonDoors.txt") -def get_ep_no_mountain(): - return get_adjustment_file("settings/EP_Shuffle/EP_NoMountainEPs.txt") +def get_mountain_upper_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Mountain_Upper.txt") -def get_ep_no_videos(): - return get_adjustment_file("settings/EP_Shuffle/EP_Videos.txt") +def get_challenge_vault_box_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Challenge_Vault_Box.txt") -def get_sigma_normal_logic(): +def get_path_to_challenge_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Path_To_Challenge.txt") + + +def get_mountain_lower_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Mountain_Lower.txt") + + +def get_elevators_come_to_you() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Elevators_Come_To_You.txt") + + +def get_sigma_normal_logic() -> List[str]: return get_adjustment_file("WitnessLogic.txt") -def get_sigma_expert_logic(): +def get_sigma_expert_logic() -> List[str]: return get_adjustment_file("WitnessLogicExpert.txt") -def get_vanilla_logic(): +def get_vanilla_logic() -> List[str]: return get_adjustment_file("WitnessLogicVanilla.txt") -def get_items(): +def get_items() -> List[str]: return get_adjustment_file("WitnessItems.txt") + + +def dnf_remove_redundancies(dnf_requirement: FrozenSet[FrozenSet[str]]) -> FrozenSet[FrozenSet[str]]: + """Removes any redundant terms from a logical formula in disjunctive normal form. + This means removing any terms that are a superset of any other term get removed. + This is possible because of the boolean absorption law: a | (a & b) = a""" + to_remove = set() + + for option1 in dnf_requirement: + for option2 in dnf_requirement: + if option2 < option1: + to_remove.add(option1) + + return dnf_requirement - to_remove + + +def dnf_and(dnf_requirements: List[FrozenSet[FrozenSet[str]]]) -> FrozenSet[FrozenSet[str]]: + """ + performs the "and" operator on a list of logical formula in disjunctive normal form, represented as a set of sets. + A logical formula might look like this: {{a, b}, {c, d}}, which would mean "a & b | c & d". + These can be easily and-ed by just using the boolean distributive law: (a | b) & c = a & c | a & b. + """ + current_overall_requirement = frozenset({frozenset()}) + + for next_dnf_requirement in dnf_requirements: + new_requirement: Set[FrozenSet[str]] = set() + + for option1 in current_overall_requirement: + for option2 in next_dnf_requirement: + new_requirement.add(option1 | option2) + + current_overall_requirement = frozenset(new_requirement) + + return dnf_remove_redundancies(current_overall_requirement) From 1ff8ed396b409a3fc5f5e3791a7e5aa1fee6df73 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 24 Nov 2023 11:30:15 -0500 Subject: [PATCH 070/104] Lingo: Demote warpless painting items to filler (#2481) --- worlds/lingo/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 1f426c92f24a..3d98ae91834f 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -1,7 +1,7 @@ """ Archipelago init file for Lingo """ -from BaseClasses import Item, Tutorial +from BaseClasses import Item, ItemClassification, Tutorial from worlds.AutoWorld import WebWorld, World from .items import ALL_ITEM_TABLE, LingoItem from .locations import ALL_LOCATION_TABLE @@ -90,7 +90,16 @@ def create_items(self): def create_item(self, name: str) -> Item: item = ALL_ITEM_TABLE[name] - return LingoItem(name, item.classification, item.code, self.player) + + classification = item.classification + if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\ + and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.PAINTING_MAPPING + for painting_id in item.painting_ids): + # If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings + # go nowhere, then this item should not be progression. + classification = ItemClassification.filler + + return LingoItem(name, classification, item.code, self.player) def set_rules(self): self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) From a8e03420ec93af0b3e30e7fb320daaab94dea914 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Fri, 24 Nov 2023 08:33:59 -0800 Subject: [PATCH 071/104] Fill: Fix plando removing Usefuls first (#2445) Co-authored-by: blastron Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- Fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index 9fdbcc384392..e89db1bd436c 100644 --- a/Fill.py +++ b/Fill.py @@ -471,7 +471,7 @@ def mark_for_locking(location: Location): raise FillError( f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items") - restitempool = usefulitempool + filleritempool + restitempool = filleritempool + usefulitempool remaining_fill(world, defaultlocations, restitempool) From d892622ab1e22ae314c09c5e562e66794feba8d0 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 24 Nov 2023 17:41:56 +0100 Subject: [PATCH 072/104] Plando: verify from_pool type (#2200) --- Fill.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Fill.py b/Fill.py index e89db1bd436c..342c155079dd 100644 --- a/Fill.py +++ b/Fill.py @@ -792,6 +792,9 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: block['force'] = 'silent' if 'from_pool' not in block: block['from_pool'] = True + elif not isinstance(block['from_pool'], bool): + from_pool_type = type(block['from_pool']) + raise Exception(f'Plando "from_pool" has to be boolean, not {from_pool_type} for player {player}.') if 'world' not in block: target_world = False else: From 530e792c3c97ae25b5277d8a4742316a788d9c22 Mon Sep 17 00:00:00 2001 From: Ishigh1 Date: Fri, 24 Nov 2023 17:42:22 +0100 Subject: [PATCH 073/104] Core: Floor and ceil in datastorage (#2448) --- MultiServer.py | 13 +++++++++---- docs/network protocol.md | 2 ++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 8be8d641324a..bd9d2446af65 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -10,6 +10,7 @@ import inspect import itertools import logging +import math import operator import pickle import random @@ -67,21 +68,25 @@ def update_dict(dictionary, entries): # functions callable on storable data on the server by clients modify_functions = { + # generic: + "replace": lambda old, new: new, + "default": lambda old, new: old, + # numeric: "add": operator.add, # add together two objects, using python's "+" operator (works on strings and lists as append) "mul": operator.mul, + "pow": operator.pow, "mod": operator.mod, + "floor": lambda value, _: math.floor(value), + "ceil": lambda value, _: math.ceil(value), "max": max, "min": min, - "replace": lambda old, new: new, - "default": lambda old, new: old, - "pow": operator.pow, # bitwise: "xor": operator.xor, "or": operator.or_, "and": operator.and_, "left_shift": operator.lshift, "right_shift": operator.rshift, - # lists/dicts + # lists/dicts: "remove": remove_from_list, "pop": pop_from_container, "update": update_dict, diff --git a/docs/network protocol.md b/docs/network protocol.md index d461cebce1ec..c17cc74a8ac7 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -415,6 +415,8 @@ The following operations can be applied to a datastorage key | mul | Multiplies the current value of the key by `value`. | | pow | Multiplies the current value of the key to the power of `value`. | | mod | Sets the current value of the key to the remainder after division by `value`. | +| floor | Floors the current value (`value` is ignored). | +| ceil | Ceils the current value (`value` is ignored). | | max | Sets the current value of the key to `value` if `value` is bigger. | | min | Sets the current value of the key to `value` if `value` is lower. | | and | Applies a bitwise AND to the current value of the key with `value`. | From c5b0330223eedcf49b3c4299e424f28045970d58 Mon Sep 17 00:00:00 2001 From: David St-Louis Date: Fri, 24 Nov 2023 12:08:02 -0500 Subject: [PATCH 074/104] DOOM II: implement new game (#2255) --- README.md | 2 + worlds/doom_ii/Items.py | 1071 +++++++++ worlds/doom_ii/Locations.py | 3442 +++++++++++++++++++++++++++++ worlds/doom_ii/Maps.py | 39 + worlds/doom_ii/Options.py | 150 ++ worlds/doom_ii/Regions.py | 502 +++++ worlds/doom_ii/Rules.py | 501 +++++ worlds/doom_ii/__init__.py | 267 +++ worlds/doom_ii/docs/en_DOOM II.md | 23 + worlds/doom_ii/docs/setup_en.md | 51 + 10 files changed, 6048 insertions(+) create mode 100644 worlds/doom_ii/Items.py create mode 100644 worlds/doom_ii/Locations.py create mode 100644 worlds/doom_ii/Maps.py create mode 100644 worlds/doom_ii/Options.py create mode 100644 worlds/doom_ii/Regions.py create mode 100644 worlds/doom_ii/Rules.py create mode 100644 worlds/doom_ii/__init__.py create mode 100644 worlds/doom_ii/docs/en_DOOM II.md create mode 100644 worlds/doom_ii/docs/setup_en.md diff --git a/README.md b/README.md index a6a482942efc..a57f0f9802d4 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ Currently, the following games are supported: * Terraria * Lingo * Pokémon Emerald +* DOOM II + For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/worlds/doom_ii/Items.py b/worlds/doom_ii/Items.py new file mode 100644 index 000000000000..fc426cc883f2 --- /dev/null +++ b/worlds/doom_ii/Items.py @@ -0,0 +1,1071 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from BaseClasses import ItemClassification +from typing import TypedDict, Dict, Set + + +class ItemDict(TypedDict, total=False): + classification: ItemClassification + count: int + name: str + doom_type: int # Unique numerical id used to spawn the item. -1 is level item, -2 is level complete item. + episode: int # Relevant if that item targets a specific level, like keycard or map reveal pickup. + map: int + + +item_table: Dict[int, ItemDict] = { + 360000: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Shotgun', + 'doom_type': 2001, + 'episode': -1, + 'map': -1}, + 360001: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Rocket launcher', + 'doom_type': 2003, + 'episode': -1, + 'map': -1}, + 360002: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Plasma gun', + 'doom_type': 2004, + 'episode': -1, + 'map': -1}, + 360003: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Chainsaw', + 'doom_type': 2005, + 'episode': -1, + 'map': -1}, + 360004: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Chaingun', + 'doom_type': 2002, + 'episode': -1, + 'map': -1}, + 360005: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'BFG9000', + 'doom_type': 2006, + 'episode': -1, + 'map': -1}, + 360006: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Super Shotgun', + 'doom_type': 82, + 'episode': -1, + 'map': -1}, + 360007: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Backpack', + 'doom_type': 8, + 'episode': -1, + 'map': -1}, + 360008: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Armor', + 'doom_type': 2018, + 'episode': -1, + 'map': -1}, + 360009: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Mega Armor', + 'doom_type': 2019, + 'episode': -1, + 'map': -1}, + 360010: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Berserk', + 'doom_type': 2023, + 'episode': -1, + 'map': -1}, + 360011: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Invulnerability', + 'doom_type': 2022, + 'episode': -1, + 'map': -1}, + 360012: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Partial invisibility', + 'doom_type': 2024, + 'episode': -1, + 'map': -1}, + 360013: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Supercharge', + 'doom_type': 2013, + 'episode': -1, + 'map': -1}, + 360014: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Megasphere', + 'doom_type': 83, + 'episode': -1, + 'map': -1}, + 360015: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Medikit', + 'doom_type': 2012, + 'episode': -1, + 'map': -1}, + 360016: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Box of bullets', + 'doom_type': 2048, + 'episode': -1, + 'map': -1}, + 360017: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Box of rockets', + 'doom_type': 2046, + 'episode': -1, + 'map': -1}, + 360018: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Box of shotgun shells', + 'doom_type': 2049, + 'episode': -1, + 'map': -1}, + 360019: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Energy cell pack', + 'doom_type': 17, + 'episode': -1, + 'map': -1}, + 360200: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Underhalls (MAP02) - Red keycard', + 'doom_type': 13, + 'episode': 1, + 'map': 2}, + 360201: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Underhalls (MAP02) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 2}, + 360202: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gantlet (MAP03) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 3}, + 360203: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gantlet (MAP03) - Red keycard', + 'doom_type': 13, + 'episode': 1, + 'map': 3}, + 360204: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Focus (MAP04) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 4}, + 360205: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Focus (MAP04) - Red keycard', + 'doom_type': 13, + 'episode': 1, + 'map': 4}, + 360206: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Focus (MAP04) - Yellow keycard', + 'doom_type': 6, + 'episode': 1, + 'map': 4}, + 360207: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Waste Tunnels (MAP05) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 5}, + 360208: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Waste Tunnels (MAP05) - Red keycard', + 'doom_type': 13, + 'episode': 1, + 'map': 5}, + 360209: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Waste Tunnels (MAP05) - Yellow keycard', + 'doom_type': 6, + 'episode': 1, + 'map': 5}, + 360210: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crusher (MAP06) - Red keycard', + 'doom_type': 13, + 'episode': 1, + 'map': 6}, + 360211: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crusher (MAP06) - Yellow keycard', + 'doom_type': 6, + 'episode': 1, + 'map': 6}, + 360212: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crusher (MAP06) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 6}, + 360213: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tricks and Traps (MAP08) - Yellow skull key', + 'doom_type': 39, + 'episode': 1, + 'map': 8}, + 360214: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tricks and Traps (MAP08) - Red skull key', + 'doom_type': 38, + 'episode': 1, + 'map': 8}, + 360215: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Pit (MAP09) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 9}, + 360216: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Pit (MAP09) - Yellow keycard', + 'doom_type': 6, + 'episode': 1, + 'map': 9}, + 360217: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Refueling Base (MAP10) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 10}, + 360218: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Refueling Base (MAP10) - Yellow keycard', + 'doom_type': 6, + 'episode': 1, + 'map': 10}, + 360219: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Circle of Death (MAP11) - Red keycard', + 'doom_type': 13, + 'episode': 1, + 'map': 11}, + 360220: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Circle of Death (MAP11) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 11}, + 360221: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Factory (MAP12) - Blue keycard', + 'doom_type': 5, + 'episode': 2, + 'map': 1}, + 360222: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Factory (MAP12) - Yellow keycard', + 'doom_type': 6, + 'episode': 2, + 'map': 1}, + 360223: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Downtown (MAP13) - Blue keycard', + 'doom_type': 5, + 'episode': 2, + 'map': 2}, + 360224: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Downtown (MAP13) - Yellow keycard', + 'doom_type': 6, + 'episode': 2, + 'map': 2}, + 360225: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Downtown (MAP13) - Red keycard', + 'doom_type': 13, + 'episode': 2, + 'map': 2}, + 360226: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Inmost Dens (MAP14) - Red skull key', + 'doom_type': 38, + 'episode': 2, + 'map': 3}, + 360227: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Inmost Dens (MAP14) - Blue skull key', + 'doom_type': 40, + 'episode': 2, + 'map': 3}, + 360228: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Industrial Zone (MAP15) - Yellow keycard', + 'doom_type': 6, + 'episode': 2, + 'map': 4}, + 360229: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Industrial Zone (MAP15) - Red keycard', + 'doom_type': 13, + 'episode': 2, + 'map': 4}, + 360230: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Industrial Zone (MAP15) - Blue keycard', + 'doom_type': 5, + 'episode': 2, + 'map': 4}, + 360231: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Suburbs (MAP16) - Blue skull key', + 'doom_type': 40, + 'episode': 2, + 'map': 5}, + 360232: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Suburbs (MAP16) - Red skull key', + 'doom_type': 38, + 'episode': 2, + 'map': 5}, + 360233: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tenements (MAP17) - Red keycard', + 'doom_type': 13, + 'episode': 2, + 'map': 6}, + 360234: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tenements (MAP17) - Blue keycard', + 'doom_type': 5, + 'episode': 2, + 'map': 6}, + 360235: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tenements (MAP17) - Yellow skull key', + 'doom_type': 39, + 'episode': 2, + 'map': 6}, + 360236: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Courtyard (MAP18) - Yellow skull key', + 'doom_type': 39, + 'episode': 2, + 'map': 7}, + 360237: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Courtyard (MAP18) - Blue skull key', + 'doom_type': 40, + 'episode': 2, + 'map': 7}, + 360238: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (MAP19) - Blue skull key', + 'doom_type': 40, + 'episode': 2, + 'map': 8}, + 360239: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (MAP19) - Red skull key', + 'doom_type': 38, + 'episode': 2, + 'map': 8}, + 360240: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (MAP19) - Yellow skull key', + 'doom_type': 39, + 'episode': 2, + 'map': 8}, + 360241: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Nirvana (MAP21) - Yellow skull key', + 'doom_type': 39, + 'episode': 3, + 'map': 1}, + 360242: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Nirvana (MAP21) - Blue skull key', + 'doom_type': 40, + 'episode': 3, + 'map': 1}, + 360243: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Nirvana (MAP21) - Red skull key', + 'doom_type': 38, + 'episode': 3, + 'map': 1}, + 360244: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (MAP22) - Blue skull key', + 'doom_type': 40, + 'episode': 3, + 'map': 2}, + 360245: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (MAP22) - Red skull key', + 'doom_type': 38, + 'episode': 3, + 'map': 2}, + 360246: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Barrels o Fun (MAP23) - Yellow skull key', + 'doom_type': 39, + 'episode': 3, + 'map': 3}, + 360247: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (MAP24) - Blue keycard', + 'doom_type': 5, + 'episode': 3, + 'map': 4}, + 360248: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (MAP24) - Red keycard', + 'doom_type': 13, + 'episode': 3, + 'map': 4}, + 360249: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Bloodfalls (MAP25) - Blue skull key', + 'doom_type': 40, + 'episode': 3, + 'map': 5}, + 360250: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Abandoned Mines (MAP26) - Blue keycard', + 'doom_type': 5, + 'episode': 3, + 'map': 6}, + 360251: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Abandoned Mines (MAP26) - Red keycard', + 'doom_type': 13, + 'episode': 3, + 'map': 6}, + 360252: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Abandoned Mines (MAP26) - Yellow keycard', + 'doom_type': 6, + 'episode': 3, + 'map': 6}, + 360253: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Monster Condo (MAP27) - Yellow skull key', + 'doom_type': 39, + 'episode': 3, + 'map': 7}, + 360254: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Monster Condo (MAP27) - Red skull key', + 'doom_type': 38, + 'episode': 3, + 'map': 7}, + 360255: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Monster Condo (MAP27) - Blue skull key', + 'doom_type': 40, + 'episode': 3, + 'map': 7}, + 360256: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Spirit World (MAP28) - Yellow skull key', + 'doom_type': 39, + 'episode': 3, + 'map': 8}, + 360257: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Spirit World (MAP28) - Red skull key', + 'doom_type': 38, + 'episode': 3, + 'map': 8}, + 360400: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Entryway (MAP01)', + 'doom_type': -1, + 'episode': 1, + 'map': 1}, + 360401: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Entryway (MAP01) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 1}, + 360402: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Entryway (MAP01) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 1}, + 360403: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Underhalls (MAP02)', + 'doom_type': -1, + 'episode': 1, + 'map': 2}, + 360404: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Underhalls (MAP02) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 2}, + 360405: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Underhalls (MAP02) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 2}, + 360406: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gantlet (MAP03)', + 'doom_type': -1, + 'episode': 1, + 'map': 3}, + 360407: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gantlet (MAP03) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 3}, + 360408: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Gantlet (MAP03) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 3}, + 360409: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Focus (MAP04)', + 'doom_type': -1, + 'episode': 1, + 'map': 4}, + 360410: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Focus (MAP04) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 4}, + 360411: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Focus (MAP04) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 4}, + 360412: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Waste Tunnels (MAP05)', + 'doom_type': -1, + 'episode': 1, + 'map': 5}, + 360413: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Waste Tunnels (MAP05) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 5}, + 360414: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Waste Tunnels (MAP05) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 5}, + 360415: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crusher (MAP06)', + 'doom_type': -1, + 'episode': 1, + 'map': 6}, + 360416: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crusher (MAP06) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 6}, + 360417: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Crusher (MAP06) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 6}, + 360418: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Dead Simple (MAP07)', + 'doom_type': -1, + 'episode': 1, + 'map': 7}, + 360419: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Dead Simple (MAP07) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 7}, + 360420: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Dead Simple (MAP07) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 7}, + 360421: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tricks and Traps (MAP08)', + 'doom_type': -1, + 'episode': 1, + 'map': 8}, + 360422: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tricks and Traps (MAP08) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 8}, + 360423: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Tricks and Traps (MAP08) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 8}, + 360424: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Pit (MAP09)', + 'doom_type': -1, + 'episode': 1, + 'map': 9}, + 360425: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Pit (MAP09) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 9}, + 360426: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Pit (MAP09) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 9}, + 360427: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Refueling Base (MAP10)', + 'doom_type': -1, + 'episode': 1, + 'map': 10}, + 360428: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Refueling Base (MAP10) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 10}, + 360429: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Refueling Base (MAP10) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 10}, + 360430: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Circle of Death (MAP11)', + 'doom_type': -1, + 'episode': 1, + 'map': 11}, + 360431: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Circle of Death (MAP11) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 11}, + 360432: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Circle of Death (MAP11) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 11}, + 360433: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Factory (MAP12)', + 'doom_type': -1, + 'episode': 2, + 'map': 1}, + 360434: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Factory (MAP12) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 1}, + 360435: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Factory (MAP12) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 1}, + 360436: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Downtown (MAP13)', + 'doom_type': -1, + 'episode': 2, + 'map': 2}, + 360437: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Downtown (MAP13) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 2}, + 360438: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Downtown (MAP13) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 2}, + 360439: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Inmost Dens (MAP14)', + 'doom_type': -1, + 'episode': 2, + 'map': 3}, + 360440: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Inmost Dens (MAP14) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 3}, + 360441: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Inmost Dens (MAP14) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 3}, + 360442: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Industrial Zone (MAP15)', + 'doom_type': -1, + 'episode': 2, + 'map': 4}, + 360443: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Industrial Zone (MAP15) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 4}, + 360444: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Industrial Zone (MAP15) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 4}, + 360445: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Suburbs (MAP16)', + 'doom_type': -1, + 'episode': 2, + 'map': 5}, + 360446: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Suburbs (MAP16) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 5}, + 360447: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Suburbs (MAP16) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 5}, + 360448: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tenements (MAP17)', + 'doom_type': -1, + 'episode': 2, + 'map': 6}, + 360449: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tenements (MAP17) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 6}, + 360450: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Tenements (MAP17) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 6}, + 360451: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Courtyard (MAP18)', + 'doom_type': -1, + 'episode': 2, + 'map': 7}, + 360452: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Courtyard (MAP18) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 7}, + 360453: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Courtyard (MAP18) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 7}, + 360454: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (MAP19)', + 'doom_type': -1, + 'episode': 2, + 'map': 8}, + 360455: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (MAP19) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 8}, + 360456: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Citadel (MAP19) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 8}, + 360457: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Gotcha! (MAP20)', + 'doom_type': -1, + 'episode': 2, + 'map': 9}, + 360458: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Gotcha! (MAP20) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 9}, + 360459: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Gotcha! (MAP20) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 9}, + 360460: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Nirvana (MAP21)', + 'doom_type': -1, + 'episode': 3, + 'map': 1}, + 360461: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Nirvana (MAP21) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 1}, + 360462: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Nirvana (MAP21) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 1}, + 360463: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (MAP22)', + 'doom_type': -1, + 'episode': 3, + 'map': 2}, + 360464: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (MAP22) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 2}, + 360465: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Catacombs (MAP22) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 2}, + 360466: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Barrels o Fun (MAP23)', + 'doom_type': -1, + 'episode': 3, + 'map': 3}, + 360467: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Barrels o Fun (MAP23) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 3}, + 360468: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Barrels o Fun (MAP23) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 3}, + 360469: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (MAP24)', + 'doom_type': -1, + 'episode': 3, + 'map': 4}, + 360470: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (MAP24) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 4}, + 360471: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Chasm (MAP24) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 4}, + 360472: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Bloodfalls (MAP25)', + 'doom_type': -1, + 'episode': 3, + 'map': 5}, + 360473: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Bloodfalls (MAP25) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 5}, + 360474: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Bloodfalls (MAP25) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 5}, + 360475: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Abandoned Mines (MAP26)', + 'doom_type': -1, + 'episode': 3, + 'map': 6}, + 360476: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Abandoned Mines (MAP26) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 6}, + 360477: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Abandoned Mines (MAP26) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 6}, + 360478: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Monster Condo (MAP27)', + 'doom_type': -1, + 'episode': 3, + 'map': 7}, + 360479: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Monster Condo (MAP27) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 7}, + 360480: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Monster Condo (MAP27) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 7}, + 360481: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Spirit World (MAP28)', + 'doom_type': -1, + 'episode': 3, + 'map': 8}, + 360482: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Spirit World (MAP28) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 8}, + 360483: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Spirit World (MAP28) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 8}, + 360484: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Living End (MAP29)', + 'doom_type': -1, + 'episode': 3, + 'map': 9}, + 360485: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Living End (MAP29) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 9}, + 360486: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Living End (MAP29) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 9}, + 360487: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Icon of Sin (MAP30)', + 'doom_type': -1, + 'episode': 3, + 'map': 10}, + 360488: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Icon of Sin (MAP30) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 10}, + 360489: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Icon of Sin (MAP30) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 10}, + 360490: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Wolfenstein2 (MAP31)', + 'doom_type': -1, + 'episode': 4, + 'map': 1}, + 360491: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Wolfenstein2 (MAP31) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 1}, + 360492: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Wolfenstein2 (MAP31) - Computer area map', + 'doom_type': 2026, + 'episode': 4, + 'map': 1}, + 360493: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Grosse2 (MAP32)', + 'doom_type': -1, + 'episode': 4, + 'map': 2}, + 360494: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Grosse2 (MAP32) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 2}, + 360495: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Grosse2 (MAP32) - Computer area map', + 'doom_type': 2026, + 'episode': 4, + 'map': 2}, +} + + +item_name_groups: Dict[str, Set[str]] = { + 'Ammos': {'Box of bullets', 'Box of rockets', 'Box of shotgun shells', 'Energy cell pack', }, + 'Computer area maps': {'Barrels o Fun (MAP23) - Computer area map', 'Bloodfalls (MAP25) - Computer area map', 'Circle of Death (MAP11) - Computer area map', 'Dead Simple (MAP07) - Computer area map', 'Downtown (MAP13) - Computer area map', 'Entryway (MAP01) - Computer area map', 'Gotcha! (MAP20) - Computer area map', 'Grosse2 (MAP32) - Computer area map', 'Icon of Sin (MAP30) - Computer area map', 'Industrial Zone (MAP15) - Computer area map', 'Monster Condo (MAP27) - Computer area map', 'Nirvana (MAP21) - Computer area map', 'Refueling Base (MAP10) - Computer area map', 'Suburbs (MAP16) - Computer area map', 'Tenements (MAP17) - Computer area map', 'The Abandoned Mines (MAP26) - Computer area map', 'The Catacombs (MAP22) - Computer area map', 'The Chasm (MAP24) - Computer area map', 'The Citadel (MAP19) - Computer area map', 'The Courtyard (MAP18) - Computer area map', 'The Crusher (MAP06) - Computer area map', 'The Factory (MAP12) - Computer area map', 'The Focus (MAP04) - Computer area map', 'The Gantlet (MAP03) - Computer area map', 'The Inmost Dens (MAP14) - Computer area map', 'The Living End (MAP29) - Computer area map', 'The Pit (MAP09) - Computer area map', 'The Spirit World (MAP28) - Computer area map', 'The Waste Tunnels (MAP05) - Computer area map', 'Tricks and Traps (MAP08) - Computer area map', 'Underhalls (MAP02) - Computer area map', 'Wolfenstein2 (MAP31) - Computer area map', }, + 'Keys': {'Barrels o Fun (MAP23) - Yellow skull key', 'Bloodfalls (MAP25) - Blue skull key', 'Circle of Death (MAP11) - Blue keycard', 'Circle of Death (MAP11) - Red keycard', 'Downtown (MAP13) - Blue keycard', 'Downtown (MAP13) - Red keycard', 'Downtown (MAP13) - Yellow keycard', 'Industrial Zone (MAP15) - Blue keycard', 'Industrial Zone (MAP15) - Red keycard', 'Industrial Zone (MAP15) - Yellow keycard', 'Monster Condo (MAP27) - Blue skull key', 'Monster Condo (MAP27) - Red skull key', 'Monster Condo (MAP27) - Yellow skull key', 'Nirvana (MAP21) - Blue skull key', 'Nirvana (MAP21) - Red skull key', 'Nirvana (MAP21) - Yellow skull key', 'Refueling Base (MAP10) - Blue keycard', 'Refueling Base (MAP10) - Yellow keycard', 'Suburbs (MAP16) - Blue skull key', 'Suburbs (MAP16) - Red skull key', 'Tenements (MAP17) - Blue keycard', 'Tenements (MAP17) - Red keycard', 'Tenements (MAP17) - Yellow skull key', 'The Abandoned Mines (MAP26) - Blue keycard', 'The Abandoned Mines (MAP26) - Red keycard', 'The Abandoned Mines (MAP26) - Yellow keycard', 'The Catacombs (MAP22) - Blue skull key', 'The Catacombs (MAP22) - Red skull key', 'The Chasm (MAP24) - Blue keycard', 'The Chasm (MAP24) - Red keycard', 'The Citadel (MAP19) - Blue skull key', 'The Citadel (MAP19) - Red skull key', 'The Citadel (MAP19) - Yellow skull key', 'The Courtyard (MAP18) - Blue skull key', 'The Courtyard (MAP18) - Yellow skull key', 'The Crusher (MAP06) - Blue keycard', 'The Crusher (MAP06) - Red keycard', 'The Crusher (MAP06) - Yellow keycard', 'The Factory (MAP12) - Blue keycard', 'The Factory (MAP12) - Yellow keycard', 'The Focus (MAP04) - Blue keycard', 'The Focus (MAP04) - Red keycard', 'The Focus (MAP04) - Yellow keycard', 'The Gantlet (MAP03) - Blue keycard', 'The Gantlet (MAP03) - Red keycard', 'The Inmost Dens (MAP14) - Blue skull key', 'The Inmost Dens (MAP14) - Red skull key', 'The Pit (MAP09) - Blue keycard', 'The Pit (MAP09) - Yellow keycard', 'The Spirit World (MAP28) - Red skull key', 'The Spirit World (MAP28) - Yellow skull key', 'The Waste Tunnels (MAP05) - Blue keycard', 'The Waste Tunnels (MAP05) - Red keycard', 'The Waste Tunnels (MAP05) - Yellow keycard', 'Tricks and Traps (MAP08) - Red skull key', 'Tricks and Traps (MAP08) - Yellow skull key', 'Underhalls (MAP02) - Blue keycard', 'Underhalls (MAP02) - Red keycard', }, + 'Levels': {'Barrels o Fun (MAP23)', 'Bloodfalls (MAP25)', 'Circle of Death (MAP11)', 'Dead Simple (MAP07)', 'Downtown (MAP13)', 'Entryway (MAP01)', 'Gotcha! (MAP20)', 'Grosse2 (MAP32)', 'Icon of Sin (MAP30)', 'Industrial Zone (MAP15)', 'Monster Condo (MAP27)', 'Nirvana (MAP21)', 'Refueling Base (MAP10)', 'Suburbs (MAP16)', 'Tenements (MAP17)', 'The Abandoned Mines (MAP26)', 'The Catacombs (MAP22)', 'The Chasm (MAP24)', 'The Citadel (MAP19)', 'The Courtyard (MAP18)', 'The Crusher (MAP06)', 'The Factory (MAP12)', 'The Focus (MAP04)', 'The Gantlet (MAP03)', 'The Inmost Dens (MAP14)', 'The Living End (MAP29)', 'The Pit (MAP09)', 'The Spirit World (MAP28)', 'The Waste Tunnels (MAP05)', 'Tricks and Traps (MAP08)', 'Underhalls (MAP02)', 'Wolfenstein2 (MAP31)', }, + 'Powerups': {'Armor', 'Berserk', 'Invulnerability', 'Mega Armor', 'Megasphere', 'Partial invisibility', 'Supercharge', }, + 'Weapons': {'BFG9000', 'Chaingun', 'Chainsaw', 'Plasma gun', 'Rocket launcher', 'Shotgun', 'Super Shotgun', }, +} diff --git a/worlds/doom_ii/Locations.py b/worlds/doom_ii/Locations.py new file mode 100644 index 000000000000..3ce87b8a6662 --- /dev/null +++ b/worlds/doom_ii/Locations.py @@ -0,0 +1,3442 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import Dict, TypedDict, List, Set + + +class LocationDict(TypedDict, total=False): + name: str + episode: int + map: int + index: int # Thing index as it is stored in the wad file. + doom_type: int # In case index end up unreliable, we can use doom type. Maps have often only one of each important things. + region: str + + +location_table: Dict[int, LocationDict] = { + 361000: {'name': 'Entryway (MAP01) - Armor', + 'episode': 1, + 'map': 1, + 'index': 17, + 'doom_type': 2018, + 'region': "Entryway (MAP01) Main"}, + 361001: {'name': 'Entryway (MAP01) - Shotgun', + 'episode': 1, + 'map': 1, + 'index': 37, + 'doom_type': 2001, + 'region': "Entryway (MAP01) Main"}, + 361002: {'name': 'Entryway (MAP01) - Rocket launcher', + 'episode': 1, + 'map': 1, + 'index': 52, + 'doom_type': 2003, + 'region': "Entryway (MAP01) Main"}, + 361003: {'name': 'Entryway (MAP01) - Chainsaw', + 'episode': 1, + 'map': 1, + 'index': 68, + 'doom_type': 2005, + 'region': "Entryway (MAP01) Main"}, + 361004: {'name': 'Entryway (MAP01) - Exit', + 'episode': 1, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "Entryway (MAP01) Main"}, + 361005: {'name': 'Underhalls (MAP02) - Red keycard', + 'episode': 1, + 'map': 2, + 'index': 31, + 'doom_type': 13, + 'region': "Underhalls (MAP02) Main"}, + 361006: {'name': 'Underhalls (MAP02) - Blue keycard', + 'episode': 1, + 'map': 2, + 'index': 44, + 'doom_type': 5, + 'region': "Underhalls (MAP02) Red"}, + 361007: {'name': 'Underhalls (MAP02) - Mega Armor', + 'episode': 1, + 'map': 2, + 'index': 116, + 'doom_type': 2019, + 'region': "Underhalls (MAP02) Main"}, + 361008: {'name': 'Underhalls (MAP02) - Super Shotgun', + 'episode': 1, + 'map': 2, + 'index': 127, + 'doom_type': 82, + 'region': "Underhalls (MAP02) Main"}, + 361009: {'name': 'Underhalls (MAP02) - Exit', + 'episode': 1, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "Underhalls (MAP02) Blue"}, + 361010: {'name': 'The Gantlet (MAP03) - Mega Armor', + 'episode': 1, + 'map': 3, + 'index': 5, + 'doom_type': 2019, + 'region': "The Gantlet (MAP03) Main"}, + 361011: {'name': 'The Gantlet (MAP03) - Shotgun', + 'episode': 1, + 'map': 3, + 'index': 6, + 'doom_type': 2001, + 'region': "The Gantlet (MAP03) Main"}, + 361012: {'name': 'The Gantlet (MAP03) - Blue keycard', + 'episode': 1, + 'map': 3, + 'index': 85, + 'doom_type': 5, + 'region': "The Gantlet (MAP03) Main"}, + 361013: {'name': 'The Gantlet (MAP03) - Rocket launcher', + 'episode': 1, + 'map': 3, + 'index': 86, + 'doom_type': 2003, + 'region': "The Gantlet (MAP03) Main"}, + 361014: {'name': 'The Gantlet (MAP03) - Partial invisibility', + 'episode': 1, + 'map': 3, + 'index': 96, + 'doom_type': 2024, + 'region': "The Gantlet (MAP03) Main"}, + 361015: {'name': 'The Gantlet (MAP03) - Supercharge', + 'episode': 1, + 'map': 3, + 'index': 97, + 'doom_type': 2013, + 'region': "The Gantlet (MAP03) Main"}, + 361016: {'name': 'The Gantlet (MAP03) - Mega Armor 2', + 'episode': 1, + 'map': 3, + 'index': 98, + 'doom_type': 2019, + 'region': "The Gantlet (MAP03) Main"}, + 361017: {'name': 'The Gantlet (MAP03) - Red keycard', + 'episode': 1, + 'map': 3, + 'index': 104, + 'doom_type': 13, + 'region': "The Gantlet (MAP03) Blue"}, + 361018: {'name': 'The Gantlet (MAP03) - Chaingun', + 'episode': 1, + 'map': 3, + 'index': 122, + 'doom_type': 2002, + 'region': "The Gantlet (MAP03) Main"}, + 361019: {'name': 'The Gantlet (MAP03) - Backpack', + 'episode': 1, + 'map': 3, + 'index': 146, + 'doom_type': 8, + 'region': "The Gantlet (MAP03) Blue"}, + 361020: {'name': 'The Gantlet (MAP03) - Exit', + 'episode': 1, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "The Gantlet (MAP03) Red"}, + 361021: {'name': 'The Focus (MAP04) - Super Shotgun', + 'episode': 1, + 'map': 4, + 'index': 4, + 'doom_type': 82, + 'region': "The Focus (MAP04) Main"}, + 361022: {'name': 'The Focus (MAP04) - Blue keycard', + 'episode': 1, + 'map': 4, + 'index': 21, + 'doom_type': 5, + 'region': "The Focus (MAP04) Main"}, + 361023: {'name': 'The Focus (MAP04) - Red keycard', + 'episode': 1, + 'map': 4, + 'index': 32, + 'doom_type': 13, + 'region': "The Focus (MAP04) Blue"}, + 361024: {'name': 'The Focus (MAP04) - Yellow keycard', + 'episode': 1, + 'map': 4, + 'index': 59, + 'doom_type': 6, + 'region': "The Focus (MAP04) Red"}, + 361025: {'name': 'The Focus (MAP04) - Exit', + 'episode': 1, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "The Focus (MAP04) Yellow"}, + 361026: {'name': 'The Waste Tunnels (MAP05) - Rocket launcher', + 'episode': 1, + 'map': 5, + 'index': 45, + 'doom_type': 2003, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361027: {'name': 'The Waste Tunnels (MAP05) - Super Shotgun', + 'episode': 1, + 'map': 5, + 'index': 46, + 'doom_type': 82, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361028: {'name': 'The Waste Tunnels (MAP05) - Blue keycard', + 'episode': 1, + 'map': 5, + 'index': 50, + 'doom_type': 5, + 'region': "The Waste Tunnels (MAP05) Red"}, + 361029: {'name': 'The Waste Tunnels (MAP05) - Plasma gun', + 'episode': 1, + 'map': 5, + 'index': 53, + 'doom_type': 2004, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361030: {'name': 'The Waste Tunnels (MAP05) - Red keycard', + 'episode': 1, + 'map': 5, + 'index': 55, + 'doom_type': 13, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361031: {'name': 'The Waste Tunnels (MAP05) - Supercharge', + 'episode': 1, + 'map': 5, + 'index': 56, + 'doom_type': 2013, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361032: {'name': 'The Waste Tunnels (MAP05) - Mega Armor', + 'episode': 1, + 'map': 5, + 'index': 57, + 'doom_type': 2019, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361033: {'name': 'The Waste Tunnels (MAP05) - Yellow keycard', + 'episode': 1, + 'map': 5, + 'index': 78, + 'doom_type': 6, + 'region': "The Waste Tunnels (MAP05) Blue"}, + 361034: {'name': 'The Waste Tunnels (MAP05) - Armor', + 'episode': 1, + 'map': 5, + 'index': 151, + 'doom_type': 2018, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361035: {'name': 'The Waste Tunnels (MAP05) - Supercharge 2', + 'episode': 1, + 'map': 5, + 'index': 170, + 'doom_type': 2013, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361036: {'name': 'The Waste Tunnels (MAP05) - Shotgun', + 'episode': 1, + 'map': 5, + 'index': 202, + 'doom_type': 2001, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361037: {'name': 'The Waste Tunnels (MAP05) - Berserk', + 'episode': 1, + 'map': 5, + 'index': 215, + 'doom_type': 2023, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361038: {'name': 'The Waste Tunnels (MAP05) - Exit', + 'episode': 1, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "The Waste Tunnels (MAP05) Yellow"}, + 361039: {'name': 'The Crusher (MAP06) - Red keycard', + 'episode': 1, + 'map': 6, + 'index': 0, + 'doom_type': 13, + 'region': "The Crusher (MAP06) Blue"}, + 361040: {'name': 'The Crusher (MAP06) - Yellow keycard', + 'episode': 1, + 'map': 6, + 'index': 1, + 'doom_type': 6, + 'region': "The Crusher (MAP06) Red"}, + 361041: {'name': 'The Crusher (MAP06) - Blue keycard', + 'episode': 1, + 'map': 6, + 'index': 36, + 'doom_type': 5, + 'region': "The Crusher (MAP06) Main"}, + 361042: {'name': 'The Crusher (MAP06) - Supercharge', + 'episode': 1, + 'map': 6, + 'index': 55, + 'doom_type': 2013, + 'region': "The Crusher (MAP06) Main"}, + 361043: {'name': 'The Crusher (MAP06) - Plasma gun', + 'episode': 1, + 'map': 6, + 'index': 59, + 'doom_type': 2004, + 'region': "The Crusher (MAP06) Main"}, + 361044: {'name': 'The Crusher (MAP06) - Blue keycard 2', + 'episode': 1, + 'map': 6, + 'index': 74, + 'doom_type': 5, + 'region': "The Crusher (MAP06) Main"}, + 361045: {'name': 'The Crusher (MAP06) - Blue keycard 3', + 'episode': 1, + 'map': 6, + 'index': 75, + 'doom_type': 5, + 'region': "The Crusher (MAP06) Main"}, + 361046: {'name': 'The Crusher (MAP06) - Megasphere', + 'episode': 1, + 'map': 6, + 'index': 94, + 'doom_type': 83, + 'region': "The Crusher (MAP06) Main"}, + 361047: {'name': 'The Crusher (MAP06) - Armor', + 'episode': 1, + 'map': 6, + 'index': 130, + 'doom_type': 2018, + 'region': "The Crusher (MAP06) Main"}, + 361048: {'name': 'The Crusher (MAP06) - Super Shotgun', + 'episode': 1, + 'map': 6, + 'index': 134, + 'doom_type': 82, + 'region': "The Crusher (MAP06) Blue"}, + 361049: {'name': 'The Crusher (MAP06) - Mega Armor', + 'episode': 1, + 'map': 6, + 'index': 222, + 'doom_type': 2019, + 'region': "The Crusher (MAP06) Blue"}, + 361050: {'name': 'The Crusher (MAP06) - Rocket launcher', + 'episode': 1, + 'map': 6, + 'index': 223, + 'doom_type': 2003, + 'region': "The Crusher (MAP06) Blue"}, + 361051: {'name': 'The Crusher (MAP06) - Backpack', + 'episode': 1, + 'map': 6, + 'index': 225, + 'doom_type': 8, + 'region': "The Crusher (MAP06) Blue"}, + 361052: {'name': 'The Crusher (MAP06) - Megasphere 2', + 'episode': 1, + 'map': 6, + 'index': 246, + 'doom_type': 83, + 'region': "The Crusher (MAP06) Blue"}, + 361053: {'name': 'The Crusher (MAP06) - Exit', + 'episode': 1, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "The Crusher (MAP06) Yellow"}, + 361054: {'name': 'Dead Simple (MAP07) - Megasphere', + 'episode': 1, + 'map': 7, + 'index': 4, + 'doom_type': 83, + 'region': "Dead Simple (MAP07) Main"}, + 361055: {'name': 'Dead Simple (MAP07) - Rocket launcher', + 'episode': 1, + 'map': 7, + 'index': 5, + 'doom_type': 2003, + 'region': "Dead Simple (MAP07) Main"}, + 361056: {'name': 'Dead Simple (MAP07) - Partial invisibility', + 'episode': 1, + 'map': 7, + 'index': 7, + 'doom_type': 2024, + 'region': "Dead Simple (MAP07) Main"}, + 361057: {'name': 'Dead Simple (MAP07) - Super Shotgun', + 'episode': 1, + 'map': 7, + 'index': 8, + 'doom_type': 82, + 'region': "Dead Simple (MAP07) Main"}, + 361058: {'name': 'Dead Simple (MAP07) - Chaingun', + 'episode': 1, + 'map': 7, + 'index': 9, + 'doom_type': 2002, + 'region': "Dead Simple (MAP07) Main"}, + 361059: {'name': 'Dead Simple (MAP07) - Plasma gun', + 'episode': 1, + 'map': 7, + 'index': 10, + 'doom_type': 2004, + 'region': "Dead Simple (MAP07) Main"}, + 361060: {'name': 'Dead Simple (MAP07) - Backpack', + 'episode': 1, + 'map': 7, + 'index': 43, + 'doom_type': 8, + 'region': "Dead Simple (MAP07) Main"}, + 361061: {'name': 'Dead Simple (MAP07) - Berserk', + 'episode': 1, + 'map': 7, + 'index': 44, + 'doom_type': 2023, + 'region': "Dead Simple (MAP07) Main"}, + 361062: {'name': 'Dead Simple (MAP07) - Partial invisibility 2', + 'episode': 1, + 'map': 7, + 'index': 60, + 'doom_type': 2024, + 'region': "Dead Simple (MAP07) Main"}, + 361063: {'name': 'Dead Simple (MAP07) - Partial invisibility 3', + 'episode': 1, + 'map': 7, + 'index': 73, + 'doom_type': 2024, + 'region': "Dead Simple (MAP07) Main"}, + 361064: {'name': 'Dead Simple (MAP07) - Partial invisibility 4', + 'episode': 1, + 'map': 7, + 'index': 74, + 'doom_type': 2024, + 'region': "Dead Simple (MAP07) Main"}, + 361065: {'name': 'Dead Simple (MAP07) - Exit', + 'episode': 1, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "Dead Simple (MAP07) Main"}, + 361066: {'name': 'Tricks and Traps (MAP08) - Plasma gun', + 'episode': 1, + 'map': 8, + 'index': 14, + 'doom_type': 2004, + 'region': "Tricks and Traps (MAP08) Main"}, + 361067: {'name': 'Tricks and Traps (MAP08) - Rocket launcher', + 'episode': 1, + 'map': 8, + 'index': 17, + 'doom_type': 2003, + 'region': "Tricks and Traps (MAP08) Main"}, + 361068: {'name': 'Tricks and Traps (MAP08) - Armor', + 'episode': 1, + 'map': 8, + 'index': 36, + 'doom_type': 2018, + 'region': "Tricks and Traps (MAP08) Main"}, + 361069: {'name': 'Tricks and Traps (MAP08) - Chaingun', + 'episode': 1, + 'map': 8, + 'index': 48, + 'doom_type': 2002, + 'region': "Tricks and Traps (MAP08) Main"}, + 361070: {'name': 'Tricks and Traps (MAP08) - Shotgun', + 'episode': 1, + 'map': 8, + 'index': 87, + 'doom_type': 2001, + 'region': "Tricks and Traps (MAP08) Main"}, + 361071: {'name': 'Tricks and Traps (MAP08) - Supercharge', + 'episode': 1, + 'map': 8, + 'index': 119, + 'doom_type': 2013, + 'region': "Tricks and Traps (MAP08) Main"}, + 361072: {'name': 'Tricks and Traps (MAP08) - Invulnerability', + 'episode': 1, + 'map': 8, + 'index': 120, + 'doom_type': 2022, + 'region': "Tricks and Traps (MAP08) Main"}, + 361073: {'name': 'Tricks and Traps (MAP08) - Invulnerability 2', + 'episode': 1, + 'map': 8, + 'index': 122, + 'doom_type': 2022, + 'region': "Tricks and Traps (MAP08) Main"}, + 361074: {'name': 'Tricks and Traps (MAP08) - Yellow skull key', + 'episode': 1, + 'map': 8, + 'index': 123, + 'doom_type': 39, + 'region': "Tricks and Traps (MAP08) Main"}, + 361075: {'name': 'Tricks and Traps (MAP08) - Backpack', + 'episode': 1, + 'map': 8, + 'index': 133, + 'doom_type': 8, + 'region': "Tricks and Traps (MAP08) Main"}, + 361076: {'name': 'Tricks and Traps (MAP08) - Backpack 2', + 'episode': 1, + 'map': 8, + 'index': 134, + 'doom_type': 8, + 'region': "Tricks and Traps (MAP08) Main"}, + 361077: {'name': 'Tricks and Traps (MAP08) - Invulnerability 3', + 'episode': 1, + 'map': 8, + 'index': 135, + 'doom_type': 2022, + 'region': "Tricks and Traps (MAP08) Main"}, + 361078: {'name': 'Tricks and Traps (MAP08) - Invulnerability 4', + 'episode': 1, + 'map': 8, + 'index': 136, + 'doom_type': 2022, + 'region': "Tricks and Traps (MAP08) Main"}, + 361079: {'name': 'Tricks and Traps (MAP08) - BFG9000', + 'episode': 1, + 'map': 8, + 'index': 161, + 'doom_type': 2006, + 'region': "Tricks and Traps (MAP08) Main"}, + 361080: {'name': 'Tricks and Traps (MAP08) - Supercharge 2', + 'episode': 1, + 'map': 8, + 'index': 162, + 'doom_type': 2013, + 'region': "Tricks and Traps (MAP08) Main"}, + 361081: {'name': 'Tricks and Traps (MAP08) - Backpack 3', + 'episode': 1, + 'map': 8, + 'index': 163, + 'doom_type': 8, + 'region': "Tricks and Traps (MAP08) Main"}, + 361082: {'name': 'Tricks and Traps (MAP08) - Backpack 4', + 'episode': 1, + 'map': 8, + 'index': 164, + 'doom_type': 8, + 'region': "Tricks and Traps (MAP08) Main"}, + 361083: {'name': 'Tricks and Traps (MAP08) - Chainsaw', + 'episode': 1, + 'map': 8, + 'index': 168, + 'doom_type': 2005, + 'region': "Tricks and Traps (MAP08) Main"}, + 361084: {'name': 'Tricks and Traps (MAP08) - Red skull key', + 'episode': 1, + 'map': 8, + 'index': 176, + 'doom_type': 38, + 'region': "Tricks and Traps (MAP08) Yellow"}, + 361085: {'name': 'Tricks and Traps (MAP08) - Invulnerability 5', + 'episode': 1, + 'map': 8, + 'index': 202, + 'doom_type': 2022, + 'region': "Tricks and Traps (MAP08) Yellow"}, + 361086: {'name': 'Tricks and Traps (MAP08) - Armor 2', + 'episode': 1, + 'map': 8, + 'index': 220, + 'doom_type': 2018, + 'region': "Tricks and Traps (MAP08) Main"}, + 361087: {'name': 'Tricks and Traps (MAP08) - Backpack 5', + 'episode': 1, + 'map': 8, + 'index': 226, + 'doom_type': 8, + 'region': "Tricks and Traps (MAP08) Main"}, + 361088: {'name': 'Tricks and Traps (MAP08) - Partial invisibility', + 'episode': 1, + 'map': 8, + 'index': 235, + 'doom_type': 2024, + 'region': "Tricks and Traps (MAP08) Main"}, + 361089: {'name': 'Tricks and Traps (MAP08) - Exit', + 'episode': 1, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "Tricks and Traps (MAP08) Red"}, + 361090: {'name': 'The Pit (MAP09) - Berserk', + 'episode': 1, + 'map': 9, + 'index': 5, + 'doom_type': 2023, + 'region': "The Pit (MAP09) Main"}, + 361091: {'name': 'The Pit (MAP09) - Shotgun', + 'episode': 1, + 'map': 9, + 'index': 21, + 'doom_type': 2001, + 'region': "The Pit (MAP09) Main"}, + 361092: {'name': 'The Pit (MAP09) - Mega Armor', + 'episode': 1, + 'map': 9, + 'index': 26, + 'doom_type': 2019, + 'region': "The Pit (MAP09) Main"}, + 361093: {'name': 'The Pit (MAP09) - Supercharge', + 'episode': 1, + 'map': 9, + 'index': 78, + 'doom_type': 2013, + 'region': "The Pit (MAP09) Main"}, + 361094: {'name': 'The Pit (MAP09) - Berserk 2', + 'episode': 1, + 'map': 9, + 'index': 90, + 'doom_type': 2023, + 'region': "The Pit (MAP09) Main"}, + 361095: {'name': 'The Pit (MAP09) - Rocket launcher', + 'episode': 1, + 'map': 9, + 'index': 92, + 'doom_type': 2003, + 'region': "The Pit (MAP09) Main"}, + 361096: {'name': 'The Pit (MAP09) - BFG9000', + 'episode': 1, + 'map': 9, + 'index': 184, + 'doom_type': 2006, + 'region': "The Pit (MAP09) Main"}, + 361097: {'name': 'The Pit (MAP09) - Blue keycard', + 'episode': 1, + 'map': 9, + 'index': 185, + 'doom_type': 5, + 'region': "The Pit (MAP09) Main"}, + 361098: {'name': 'The Pit (MAP09) - Yellow keycard', + 'episode': 1, + 'map': 9, + 'index': 226, + 'doom_type': 6, + 'region': "The Pit (MAP09) Blue"}, + 361099: {'name': 'The Pit (MAP09) - Backpack', + 'episode': 1, + 'map': 9, + 'index': 244, + 'doom_type': 8, + 'region': "The Pit (MAP09) Blue"}, + 361100: {'name': 'The Pit (MAP09) - Computer area map', + 'episode': 1, + 'map': 9, + 'index': 245, + 'doom_type': 2026, + 'region': "The Pit (MAP09) Blue"}, + 361101: {'name': 'The Pit (MAP09) - Supercharge 2', + 'episode': 1, + 'map': 9, + 'index': 250, + 'doom_type': 2013, + 'region': "The Pit (MAP09) Blue"}, + 361102: {'name': 'The Pit (MAP09) - Mega Armor 2', + 'episode': 1, + 'map': 9, + 'index': 251, + 'doom_type': 2019, + 'region': "The Pit (MAP09) Blue"}, + 361103: {'name': 'The Pit (MAP09) - Berserk 3', + 'episode': 1, + 'map': 9, + 'index': 309, + 'doom_type': 2023, + 'region': "The Pit (MAP09) Blue"}, + 361104: {'name': 'The Pit (MAP09) - Armor', + 'episode': 1, + 'map': 9, + 'index': 348, + 'doom_type': 2018, + 'region': "The Pit (MAP09) Main"}, + 361105: {'name': 'The Pit (MAP09) - Exit', + 'episode': 1, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "The Pit (MAP09) Yellow"}, + 361106: {'name': 'Refueling Base (MAP10) - BFG9000', + 'episode': 1, + 'map': 10, + 'index': 17, + 'doom_type': 2006, + 'region': "Refueling Base (MAP10) Main"}, + 361107: {'name': 'Refueling Base (MAP10) - Supercharge', + 'episode': 1, + 'map': 10, + 'index': 28, + 'doom_type': 2013, + 'region': "Refueling Base (MAP10) Main"}, + 361108: {'name': 'Refueling Base (MAP10) - Plasma gun', + 'episode': 1, + 'map': 10, + 'index': 29, + 'doom_type': 2004, + 'region': "Refueling Base (MAP10) Main"}, + 361109: {'name': 'Refueling Base (MAP10) - Blue keycard', + 'episode': 1, + 'map': 10, + 'index': 50, + 'doom_type': 5, + 'region': "Refueling Base (MAP10) Main"}, + 361110: {'name': 'Refueling Base (MAP10) - Shotgun', + 'episode': 1, + 'map': 10, + 'index': 99, + 'doom_type': 2001, + 'region': "Refueling Base (MAP10) Main"}, + 361111: {'name': 'Refueling Base (MAP10) - Chaingun', + 'episode': 1, + 'map': 10, + 'index': 158, + 'doom_type': 2002, + 'region': "Refueling Base (MAP10) Main"}, + 361112: {'name': 'Refueling Base (MAP10) - Armor', + 'episode': 1, + 'map': 10, + 'index': 172, + 'doom_type': 2018, + 'region': "Refueling Base (MAP10) Main"}, + 361113: {'name': 'Refueling Base (MAP10) - Rocket launcher', + 'episode': 1, + 'map': 10, + 'index': 291, + 'doom_type': 2003, + 'region': "Refueling Base (MAP10) Main"}, + 361114: {'name': 'Refueling Base (MAP10) - Supercharge 2', + 'episode': 1, + 'map': 10, + 'index': 359, + 'doom_type': 2013, + 'region': "Refueling Base (MAP10) Main"}, + 361115: {'name': 'Refueling Base (MAP10) - Backpack', + 'episode': 1, + 'map': 10, + 'index': 368, + 'doom_type': 8, + 'region': "Refueling Base (MAP10) Main"}, + 361116: {'name': 'Refueling Base (MAP10) - Berserk', + 'episode': 1, + 'map': 10, + 'index': 392, + 'doom_type': 2023, + 'region': "Refueling Base (MAP10) Main"}, + 361117: {'name': 'Refueling Base (MAP10) - Mega Armor', + 'episode': 1, + 'map': 10, + 'index': 395, + 'doom_type': 2019, + 'region': "Refueling Base (MAP10) Main"}, + 361118: {'name': 'Refueling Base (MAP10) - Invulnerability', + 'episode': 1, + 'map': 10, + 'index': 396, + 'doom_type': 2022, + 'region': "Refueling Base (MAP10) Main"}, + 361119: {'name': 'Refueling Base (MAP10) - Invulnerability 2', + 'episode': 1, + 'map': 10, + 'index': 398, + 'doom_type': 2022, + 'region': "Refueling Base (MAP10) Main"}, + 361120: {'name': 'Refueling Base (MAP10) - Armor 2', + 'episode': 1, + 'map': 10, + 'index': 400, + 'doom_type': 2018, + 'region': "Refueling Base (MAP10) Main"}, + 361121: {'name': 'Refueling Base (MAP10) - Berserk 2', + 'episode': 1, + 'map': 10, + 'index': 441, + 'doom_type': 2023, + 'region': "Refueling Base (MAP10) Main"}, + 361122: {'name': 'Refueling Base (MAP10) - Partial invisibility', + 'episode': 1, + 'map': 10, + 'index': 470, + 'doom_type': 2024, + 'region': "Refueling Base (MAP10) Main"}, + 361123: {'name': 'Refueling Base (MAP10) - Chainsaw', + 'episode': 1, + 'map': 10, + 'index': 472, + 'doom_type': 2005, + 'region': "Refueling Base (MAP10) Main"}, + 361124: {'name': 'Refueling Base (MAP10) - Yellow keycard', + 'episode': 1, + 'map': 10, + 'index': 473, + 'doom_type': 6, + 'region': "Refueling Base (MAP10) Main"}, + 361125: {'name': 'Refueling Base (MAP10) - Megasphere', + 'episode': 1, + 'map': 10, + 'index': 507, + 'doom_type': 83, + 'region': "Refueling Base (MAP10) Main"}, + 361126: {'name': 'Refueling Base (MAP10) - Exit', + 'episode': 1, + 'map': 10, + 'index': -1, + 'doom_type': -1, + 'region': "Refueling Base (MAP10) Yellow Blue"}, + 361127: {'name': 'Circle of Death (MAP11) - Red keycard', + 'episode': 1, + 'map': 11, + 'index': 1, + 'doom_type': 13, + 'region': "Circle of Death (MAP11) Main"}, + 361128: {'name': 'Circle of Death (MAP11) - Chaingun', + 'episode': 1, + 'map': 11, + 'index': 14, + 'doom_type': 2002, + 'region': "Circle of Death (MAP11) Main"}, + 361129: {'name': 'Circle of Death (MAP11) - Supercharge', + 'episode': 1, + 'map': 11, + 'index': 23, + 'doom_type': 2013, + 'region': "Circle of Death (MAP11) Main"}, + 361130: {'name': 'Circle of Death (MAP11) - Plasma gun', + 'episode': 1, + 'map': 11, + 'index': 30, + 'doom_type': 2004, + 'region': "Circle of Death (MAP11) Main"}, + 361131: {'name': 'Circle of Death (MAP11) - Blue keycard', + 'episode': 1, + 'map': 11, + 'index': 40, + 'doom_type': 5, + 'region': "Circle of Death (MAP11) Main"}, + 361132: {'name': 'Circle of Death (MAP11) - Armor', + 'episode': 1, + 'map': 11, + 'index': 42, + 'doom_type': 2018, + 'region': "Circle of Death (MAP11) Main"}, + 361133: {'name': 'Circle of Death (MAP11) - Shotgun', + 'episode': 1, + 'map': 11, + 'index': 50, + 'doom_type': 2001, + 'region': "Circle of Death (MAP11) Main"}, + 361134: {'name': 'Circle of Death (MAP11) - Mega Armor', + 'episode': 1, + 'map': 11, + 'index': 58, + 'doom_type': 2019, + 'region': "Circle of Death (MAP11) Blue"}, + 361135: {'name': 'Circle of Death (MAP11) - Partial invisibility', + 'episode': 1, + 'map': 11, + 'index': 70, + 'doom_type': 2024, + 'region': "Circle of Death (MAP11) Main"}, + 361136: {'name': 'Circle of Death (MAP11) - Invulnerability', + 'episode': 1, + 'map': 11, + 'index': 83, + 'doom_type': 2022, + 'region': "Circle of Death (MAP11) Red"}, + 361137: {'name': 'Circle of Death (MAP11) - Rocket launcher', + 'episode': 1, + 'map': 11, + 'index': 86, + 'doom_type': 2003, + 'region': "Circle of Death (MAP11) Red"}, + 361138: {'name': 'Circle of Death (MAP11) - Backpack', + 'episode': 1, + 'map': 11, + 'index': 88, + 'doom_type': 8, + 'region': "Circle of Death (MAP11) Red"}, + 361139: {'name': 'Circle of Death (MAP11) - Supercharge 2', + 'episode': 1, + 'map': 11, + 'index': 108, + 'doom_type': 2013, + 'region': "Circle of Death (MAP11) Red"}, + 361140: {'name': 'Circle of Death (MAP11) - BFG9000', + 'episode': 1, + 'map': 11, + 'index': 110, + 'doom_type': 2006, + 'region': "Circle of Death (MAP11) Red"}, + 361141: {'name': 'Circle of Death (MAP11) - Exit', + 'episode': 1, + 'map': 11, + 'index': -1, + 'doom_type': -1, + 'region': "Circle of Death (MAP11) Red"}, + 361142: {'name': 'The Factory (MAP12) - Shotgun', + 'episode': 2, + 'map': 1, + 'index': 14, + 'doom_type': 2001, + 'region': "The Factory (MAP12) Main"}, + 361143: {'name': 'The Factory (MAP12) - Berserk', + 'episode': 2, + 'map': 1, + 'index': 35, + 'doom_type': 2023, + 'region': "The Factory (MAP12) Main"}, + 361144: {'name': 'The Factory (MAP12) - Chaingun', + 'episode': 2, + 'map': 1, + 'index': 38, + 'doom_type': 2002, + 'region': "The Factory (MAP12) Main"}, + 361145: {'name': 'The Factory (MAP12) - Supercharge', + 'episode': 2, + 'map': 1, + 'index': 52, + 'doom_type': 2013, + 'region': "The Factory (MAP12) Main"}, + 361146: {'name': 'The Factory (MAP12) - Blue keycard', + 'episode': 2, + 'map': 1, + 'index': 54, + 'doom_type': 5, + 'region': "The Factory (MAP12) Main"}, + 361147: {'name': 'The Factory (MAP12) - Armor', + 'episode': 2, + 'map': 1, + 'index': 63, + 'doom_type': 2018, + 'region': "The Factory (MAP12) Blue"}, + 361148: {'name': 'The Factory (MAP12) - Backpack', + 'episode': 2, + 'map': 1, + 'index': 70, + 'doom_type': 8, + 'region': "The Factory (MAP12) Blue"}, + 361149: {'name': 'The Factory (MAP12) - Supercharge 2', + 'episode': 2, + 'map': 1, + 'index': 83, + 'doom_type': 2013, + 'region': "The Factory (MAP12) Main"}, + 361150: {'name': 'The Factory (MAP12) - Armor 2', + 'episode': 2, + 'map': 1, + 'index': 92, + 'doom_type': 2018, + 'region': "The Factory (MAP12) Main"}, + 361151: {'name': 'The Factory (MAP12) - Partial invisibility', + 'episode': 2, + 'map': 1, + 'index': 93, + 'doom_type': 2024, + 'region': "The Factory (MAP12) Main"}, + 361152: {'name': 'The Factory (MAP12) - Berserk 2', + 'episode': 2, + 'map': 1, + 'index': 107, + 'doom_type': 2023, + 'region': "The Factory (MAP12) Main"}, + 361153: {'name': 'The Factory (MAP12) - Yellow keycard', + 'episode': 2, + 'map': 1, + 'index': 123, + 'doom_type': 6, + 'region': "The Factory (MAP12) Main"}, + 361154: {'name': 'The Factory (MAP12) - BFG9000', + 'episode': 2, + 'map': 1, + 'index': 135, + 'doom_type': 2006, + 'region': "The Factory (MAP12) Blue"}, + 361155: {'name': 'The Factory (MAP12) - Berserk 3', + 'episode': 2, + 'map': 1, + 'index': 189, + 'doom_type': 2023, + 'region': "The Factory (MAP12) Main"}, + 361156: {'name': 'The Factory (MAP12) - Super Shotgun', + 'episode': 2, + 'map': 1, + 'index': 192, + 'doom_type': 82, + 'region': "The Factory (MAP12) Main"}, + 361157: {'name': 'The Factory (MAP12) - Exit', + 'episode': 2, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "The Factory (MAP12) Yellow"}, + 361158: {'name': 'Downtown (MAP13) - Rocket launcher', + 'episode': 2, + 'map': 2, + 'index': 4, + 'doom_type': 2003, + 'region': "Downtown (MAP13) Main"}, + 361159: {'name': 'Downtown (MAP13) - Shotgun', + 'episode': 2, + 'map': 2, + 'index': 42, + 'doom_type': 2001, + 'region': "Downtown (MAP13) Main"}, + 361160: {'name': 'Downtown (MAP13) - Supercharge', + 'episode': 2, + 'map': 2, + 'index': 73, + 'doom_type': 2013, + 'region': "Downtown (MAP13) Main"}, + 361161: {'name': 'Downtown (MAP13) - Berserk', + 'episode': 2, + 'map': 2, + 'index': 131, + 'doom_type': 2023, + 'region': "Downtown (MAP13) Main"}, + 361162: {'name': 'Downtown (MAP13) - Mega Armor', + 'episode': 2, + 'map': 2, + 'index': 158, + 'doom_type': 2019, + 'region': "Downtown (MAP13) Main"}, + 361163: {'name': 'Downtown (MAP13) - Chaingun', + 'episode': 2, + 'map': 2, + 'index': 183, + 'doom_type': 2002, + 'region': "Downtown (MAP13) Main"}, + 361164: {'name': 'Downtown (MAP13) - Blue keycard', + 'episode': 2, + 'map': 2, + 'index': 195, + 'doom_type': 5, + 'region': "Downtown (MAP13) Main"}, + 361165: {'name': 'Downtown (MAP13) - Yellow keycard', + 'episode': 2, + 'map': 2, + 'index': 201, + 'doom_type': 6, + 'region': "Downtown (MAP13) Red"}, + 361166: {'name': 'Downtown (MAP13) - Berserk 2', + 'episode': 2, + 'map': 2, + 'index': 207, + 'doom_type': 2023, + 'region': "Downtown (MAP13) Red"}, + 361167: {'name': 'Downtown (MAP13) - Plasma gun', + 'episode': 2, + 'map': 2, + 'index': 231, + 'doom_type': 2004, + 'region': "Downtown (MAP13) Main"}, + 361168: {'name': 'Downtown (MAP13) - Partial invisibility', + 'episode': 2, + 'map': 2, + 'index': 249, + 'doom_type': 2024, + 'region': "Downtown (MAP13) Main"}, + 361169: {'name': 'Downtown (MAP13) - Backpack', + 'episode': 2, + 'map': 2, + 'index': 250, + 'doom_type': 8, + 'region': "Downtown (MAP13) Main"}, + 361170: {'name': 'Downtown (MAP13) - Chainsaw', + 'episode': 2, + 'map': 2, + 'index': 257, + 'doom_type': 2005, + 'region': "Downtown (MAP13) Blue"}, + 361171: {'name': 'Downtown (MAP13) - BFG9000', + 'episode': 2, + 'map': 2, + 'index': 258, + 'doom_type': 2006, + 'region': "Downtown (MAP13) Main"}, + 361172: {'name': 'Downtown (MAP13) - Invulnerability', + 'episode': 2, + 'map': 2, + 'index': 269, + 'doom_type': 2022, + 'region': "Downtown (MAP13) Blue"}, + 361173: {'name': 'Downtown (MAP13) - Invulnerability 2', + 'episode': 2, + 'map': 2, + 'index': 280, + 'doom_type': 2022, + 'region': "Downtown (MAP13) Main"}, + 361174: {'name': 'Downtown (MAP13) - Partial invisibility 2', + 'episode': 2, + 'map': 2, + 'index': 281, + 'doom_type': 2024, + 'region': "Downtown (MAP13) Main"}, + 361175: {'name': 'Downtown (MAP13) - Partial invisibility 3', + 'episode': 2, + 'map': 2, + 'index': 282, + 'doom_type': 2024, + 'region': "Downtown (MAP13) Main"}, + 361176: {'name': 'Downtown (MAP13) - Red keycard', + 'episode': 2, + 'map': 2, + 'index': 283, + 'doom_type': 13, + 'region': "Downtown (MAP13) Blue"}, + 361177: {'name': 'Downtown (MAP13) - Berserk 3', + 'episode': 2, + 'map': 2, + 'index': 296, + 'doom_type': 2023, + 'region': "Downtown (MAP13) Yellow"}, + 361178: {'name': 'Downtown (MAP13) - Computer area map', + 'episode': 2, + 'map': 2, + 'index': 298, + 'doom_type': 2026, + 'region': "Downtown (MAP13) Main"}, + 361179: {'name': 'Downtown (MAP13) - Exit', + 'episode': 2, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "Downtown (MAP13) Yellow"}, + 361180: {'name': 'The Inmost Dens (MAP14) - Shotgun', + 'episode': 2, + 'map': 3, + 'index': 13, + 'doom_type': 2001, + 'region': "The Inmost Dens (MAP14) Main"}, + 361181: {'name': 'The Inmost Dens (MAP14) - Supercharge', + 'episode': 2, + 'map': 3, + 'index': 16, + 'doom_type': 2013, + 'region': "The Inmost Dens (MAP14) Main"}, + 361182: {'name': 'The Inmost Dens (MAP14) - Mega Armor', + 'episode': 2, + 'map': 3, + 'index': 22, + 'doom_type': 2019, + 'region': "The Inmost Dens (MAP14) Main"}, + 361183: {'name': 'The Inmost Dens (MAP14) - Berserk', + 'episode': 2, + 'map': 3, + 'index': 78, + 'doom_type': 2023, + 'region': "The Inmost Dens (MAP14) Main"}, + 361184: {'name': 'The Inmost Dens (MAP14) - Chaingun', + 'episode': 2, + 'map': 3, + 'index': 80, + 'doom_type': 2002, + 'region': "The Inmost Dens (MAP14) Main"}, + 361185: {'name': 'The Inmost Dens (MAP14) - Plasma gun', + 'episode': 2, + 'map': 3, + 'index': 81, + 'doom_type': 2004, + 'region': "The Inmost Dens (MAP14) Main"}, + 361186: {'name': 'The Inmost Dens (MAP14) - Red skull key', + 'episode': 2, + 'map': 3, + 'index': 119, + 'doom_type': 38, + 'region': "The Inmost Dens (MAP14) Main"}, + 361187: {'name': 'The Inmost Dens (MAP14) - Rocket launcher', + 'episode': 2, + 'map': 3, + 'index': 123, + 'doom_type': 2003, + 'region': "The Inmost Dens (MAP14) Main"}, + 361188: {'name': 'The Inmost Dens (MAP14) - Blue skull key', + 'episode': 2, + 'map': 3, + 'index': 130, + 'doom_type': 40, + 'region': "The Inmost Dens (MAP14) Red South"}, + 361189: {'name': 'The Inmost Dens (MAP14) - Partial invisibility', + 'episode': 2, + 'map': 3, + 'index': 138, + 'doom_type': 2024, + 'region': "The Inmost Dens (MAP14) Red South"}, + 361190: {'name': 'The Inmost Dens (MAP14) - Exit', + 'episode': 2, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "The Inmost Dens (MAP14) Blue"}, + 361191: {'name': 'Industrial Zone (MAP15) - Berserk', + 'episode': 2, + 'map': 4, + 'index': 4, + 'doom_type': 2023, + 'region': "Industrial Zone (MAP15) Main"}, + 361192: {'name': 'Industrial Zone (MAP15) - Rocket launcher', + 'episode': 2, + 'map': 4, + 'index': 11, + 'doom_type': 2003, + 'region': "Industrial Zone (MAP15) Main"}, + 361193: {'name': 'Industrial Zone (MAP15) - Shotgun', + 'episode': 2, + 'map': 4, + 'index': 13, + 'doom_type': 2001, + 'region': "Industrial Zone (MAP15) Main"}, + 361194: {'name': 'Industrial Zone (MAP15) - Partial invisibility', + 'episode': 2, + 'map': 4, + 'index': 14, + 'doom_type': 2024, + 'region': "Industrial Zone (MAP15) Main"}, + 361195: {'name': 'Industrial Zone (MAP15) - Backpack', + 'episode': 2, + 'map': 4, + 'index': 24, + 'doom_type': 8, + 'region': "Industrial Zone (MAP15) Main"}, + 361196: {'name': 'Industrial Zone (MAP15) - BFG9000', + 'episode': 2, + 'map': 4, + 'index': 48, + 'doom_type': 2006, + 'region': "Industrial Zone (MAP15) Main"}, + 361197: {'name': 'Industrial Zone (MAP15) - Supercharge', + 'episode': 2, + 'map': 4, + 'index': 56, + 'doom_type': 2013, + 'region': "Industrial Zone (MAP15) Main"}, + 361198: {'name': 'Industrial Zone (MAP15) - Mega Armor', + 'episode': 2, + 'map': 4, + 'index': 57, + 'doom_type': 2019, + 'region': "Industrial Zone (MAP15) Main"}, + 361199: {'name': 'Industrial Zone (MAP15) - Armor', + 'episode': 2, + 'map': 4, + 'index': 59, + 'doom_type': 2018, + 'region': "Industrial Zone (MAP15) Main"}, + 361200: {'name': 'Industrial Zone (MAP15) - Yellow keycard', + 'episode': 2, + 'map': 4, + 'index': 71, + 'doom_type': 6, + 'region': "Industrial Zone (MAP15) Main"}, + 361201: {'name': 'Industrial Zone (MAP15) - Chaingun', + 'episode': 2, + 'map': 4, + 'index': 74, + 'doom_type': 2002, + 'region': "Industrial Zone (MAP15) Main"}, + 361202: {'name': 'Industrial Zone (MAP15) - Plasma gun', + 'episode': 2, + 'map': 4, + 'index': 86, + 'doom_type': 2004, + 'region': "Industrial Zone (MAP15) Yellow West"}, + 361203: {'name': 'Industrial Zone (MAP15) - Partial invisibility 2', + 'episode': 2, + 'map': 4, + 'index': 91, + 'doom_type': 2024, + 'region': "Industrial Zone (MAP15) Yellow West"}, + 361204: {'name': 'Industrial Zone (MAP15) - Computer area map', + 'episode': 2, + 'map': 4, + 'index': 93, + 'doom_type': 2026, + 'region': "Industrial Zone (MAP15) Yellow West"}, + 361205: {'name': 'Industrial Zone (MAP15) - Invulnerability', + 'episode': 2, + 'map': 4, + 'index': 94, + 'doom_type': 2022, + 'region': "Industrial Zone (MAP15) Main"}, + 361206: {'name': 'Industrial Zone (MAP15) - Red keycard', + 'episode': 2, + 'map': 4, + 'index': 100, + 'doom_type': 13, + 'region': "Industrial Zone (MAP15) Main"}, + 361207: {'name': 'Industrial Zone (MAP15) - Backpack 2', + 'episode': 2, + 'map': 4, + 'index': 103, + 'doom_type': 8, + 'region': "Industrial Zone (MAP15) Yellow West"}, + 361208: {'name': 'Industrial Zone (MAP15) - Chainsaw', + 'episode': 2, + 'map': 4, + 'index': 113, + 'doom_type': 2005, + 'region': "Industrial Zone (MAP15) Yellow East"}, + 361209: {'name': 'Industrial Zone (MAP15) - Megasphere', + 'episode': 2, + 'map': 4, + 'index': 125, + 'doom_type': 83, + 'region': "Industrial Zone (MAP15) Yellow East"}, + 361210: {'name': 'Industrial Zone (MAP15) - Berserk 2', + 'episode': 2, + 'map': 4, + 'index': 178, + 'doom_type': 2023, + 'region': "Industrial Zone (MAP15) Yellow East"}, + 361211: {'name': 'Industrial Zone (MAP15) - Blue keycard', + 'episode': 2, + 'map': 4, + 'index': 337, + 'doom_type': 5, + 'region': "Industrial Zone (MAP15) Yellow West"}, + 361212: {'name': 'Industrial Zone (MAP15) - Mega Armor 2', + 'episode': 2, + 'map': 4, + 'index': 361, + 'doom_type': 2019, + 'region': "Industrial Zone (MAP15) Main"}, + 361213: {'name': 'Industrial Zone (MAP15) - Exit', + 'episode': 2, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "Industrial Zone (MAP15) Blue"}, + 361214: {'name': 'Suburbs (MAP16) - Megasphere', + 'episode': 2, + 'map': 5, + 'index': 7, + 'doom_type': 83, + 'region': "Suburbs (MAP16) Main"}, + 361215: {'name': 'Suburbs (MAP16) - Super Shotgun', + 'episode': 2, + 'map': 5, + 'index': 11, + 'doom_type': 82, + 'region': "Suburbs (MAP16) Main"}, + 361216: {'name': 'Suburbs (MAP16) - Chaingun', + 'episode': 2, + 'map': 5, + 'index': 15, + 'doom_type': 2002, + 'region': "Suburbs (MAP16) Main"}, + 361217: {'name': 'Suburbs (MAP16) - Backpack', + 'episode': 2, + 'map': 5, + 'index': 53, + 'doom_type': 8, + 'region': "Suburbs (MAP16) Main"}, + 361218: {'name': 'Suburbs (MAP16) - Rocket launcher', + 'episode': 2, + 'map': 5, + 'index': 59, + 'doom_type': 2003, + 'region': "Suburbs (MAP16) Main"}, + 361219: {'name': 'Suburbs (MAP16) - Berserk', + 'episode': 2, + 'map': 5, + 'index': 60, + 'doom_type': 2023, + 'region': "Suburbs (MAP16) Main"}, + 361220: {'name': 'Suburbs (MAP16) - Plasma gun', + 'episode': 2, + 'map': 5, + 'index': 62, + 'doom_type': 2004, + 'region': "Suburbs (MAP16) Blue"}, + 361221: {'name': 'Suburbs (MAP16) - Plasma gun 2', + 'episode': 2, + 'map': 5, + 'index': 63, + 'doom_type': 2004, + 'region': "Suburbs (MAP16) Blue"}, + 361222: {'name': 'Suburbs (MAP16) - Plasma gun 3', + 'episode': 2, + 'map': 5, + 'index': 64, + 'doom_type': 2004, + 'region': "Suburbs (MAP16) Blue"}, + 361223: {'name': 'Suburbs (MAP16) - Plasma gun 4', + 'episode': 2, + 'map': 5, + 'index': 65, + 'doom_type': 2004, + 'region': "Suburbs (MAP16) Blue"}, + 361224: {'name': 'Suburbs (MAP16) - BFG9000', + 'episode': 2, + 'map': 5, + 'index': 169, + 'doom_type': 2006, + 'region': "Suburbs (MAP16) Main"}, + 361225: {'name': 'Suburbs (MAP16) - Shotgun', + 'episode': 2, + 'map': 5, + 'index': 182, + 'doom_type': 2001, + 'region': "Suburbs (MAP16) Main"}, + 361226: {'name': 'Suburbs (MAP16) - Supercharge', + 'episode': 2, + 'map': 5, + 'index': 185, + 'doom_type': 2013, + 'region': "Suburbs (MAP16) Main"}, + 361227: {'name': 'Suburbs (MAP16) - Blue skull key', + 'episode': 2, + 'map': 5, + 'index': 186, + 'doom_type': 40, + 'region': "Suburbs (MAP16) Main"}, + 361228: {'name': 'Suburbs (MAP16) - Invulnerability', + 'episode': 2, + 'map': 5, + 'index': 221, + 'doom_type': 2022, + 'region': "Suburbs (MAP16) Main"}, + 361229: {'name': 'Suburbs (MAP16) - Partial invisibility', + 'episode': 2, + 'map': 5, + 'index': 231, + 'doom_type': 2024, + 'region': "Suburbs (MAP16) Main"}, + 361230: {'name': 'Suburbs (MAP16) - Red skull key', + 'episode': 2, + 'map': 5, + 'index': 236, + 'doom_type': 38, + 'region': "Suburbs (MAP16) Blue"}, + 361231: {'name': 'Suburbs (MAP16) - Exit', + 'episode': 2, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "Suburbs (MAP16) Red"}, + 361232: {'name': 'Tenements (MAP17) - Armor', + 'episode': 2, + 'map': 6, + 'index': 1, + 'doom_type': 2018, + 'region': "Tenements (MAP17) Red"}, + 361233: {'name': 'Tenements (MAP17) - Supercharge', + 'episode': 2, + 'map': 6, + 'index': 7, + 'doom_type': 2013, + 'region': "Tenements (MAP17) Yellow"}, + 361234: {'name': 'Tenements (MAP17) - Shotgun', + 'episode': 2, + 'map': 6, + 'index': 18, + 'doom_type': 2001, + 'region': "Tenements (MAP17) Main"}, + 361235: {'name': 'Tenements (MAP17) - Red keycard', + 'episode': 2, + 'map': 6, + 'index': 34, + 'doom_type': 13, + 'region': "Tenements (MAP17) Main"}, + 361236: {'name': 'Tenements (MAP17) - Blue keycard', + 'episode': 2, + 'map': 6, + 'index': 69, + 'doom_type': 5, + 'region': "Tenements (MAP17) Red"}, + 361237: {'name': 'Tenements (MAP17) - Supercharge 2', + 'episode': 2, + 'map': 6, + 'index': 75, + 'doom_type': 2013, + 'region': "Tenements (MAP17) Blue"}, + 361238: {'name': 'Tenements (MAP17) - Yellow skull key', + 'episode': 2, + 'map': 6, + 'index': 76, + 'doom_type': 39, + 'region': "Tenements (MAP17) Blue"}, + 361239: {'name': 'Tenements (MAP17) - Rocket launcher', + 'episode': 2, + 'map': 6, + 'index': 77, + 'doom_type': 2003, + 'region': "Tenements (MAP17) Blue"}, + 361240: {'name': 'Tenements (MAP17) - Partial invisibility', + 'episode': 2, + 'map': 6, + 'index': 81, + 'doom_type': 2024, + 'region': "Tenements (MAP17) Blue"}, + 361241: {'name': 'Tenements (MAP17) - Chaingun', + 'episode': 2, + 'map': 6, + 'index': 92, + 'doom_type': 2002, + 'region': "Tenements (MAP17) Red"}, + 361242: {'name': 'Tenements (MAP17) - BFG9000', + 'episode': 2, + 'map': 6, + 'index': 102, + 'doom_type': 2006, + 'region': "Tenements (MAP17) Main"}, + 361243: {'name': 'Tenements (MAP17) - Plasma gun', + 'episode': 2, + 'map': 6, + 'index': 114, + 'doom_type': 2004, + 'region': "Tenements (MAP17) Yellow"}, + 361244: {'name': 'Tenements (MAP17) - Mega Armor', + 'episode': 2, + 'map': 6, + 'index': 168, + 'doom_type': 2019, + 'region': "Tenements (MAP17) Red"}, + 361245: {'name': 'Tenements (MAP17) - Armor 2', + 'episode': 2, + 'map': 6, + 'index': 179, + 'doom_type': 2018, + 'region': "Tenements (MAP17) Red"}, + 361246: {'name': 'Tenements (MAP17) - Berserk', + 'episode': 2, + 'map': 6, + 'index': 218, + 'doom_type': 2023, + 'region': "Tenements (MAP17) Red"}, + 361247: {'name': 'Tenements (MAP17) - Backpack', + 'episode': 2, + 'map': 6, + 'index': 261, + 'doom_type': 8, + 'region': "Tenements (MAP17) Blue"}, + 361248: {'name': 'Tenements (MAP17) - Megasphere', + 'episode': 2, + 'map': 6, + 'index': 419, + 'doom_type': 83, + 'region': "Tenements (MAP17) Yellow"}, + 361249: {'name': 'Tenements (MAP17) - Exit', + 'episode': 2, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "Tenements (MAP17) Yellow"}, + 361250: {'name': 'The Courtyard (MAP18) - Shotgun', + 'episode': 2, + 'map': 7, + 'index': 12, + 'doom_type': 2001, + 'region': "The Courtyard (MAP18) Main"}, + 361251: {'name': 'The Courtyard (MAP18) - Plasma gun', + 'episode': 2, + 'map': 7, + 'index': 36, + 'doom_type': 2004, + 'region': "The Courtyard (MAP18) Main"}, + 361252: {'name': 'The Courtyard (MAP18) - Armor', + 'episode': 2, + 'map': 7, + 'index': 48, + 'doom_type': 2018, + 'region': "The Courtyard (MAP18) Main"}, + 361253: {'name': 'The Courtyard (MAP18) - Berserk', + 'episode': 2, + 'map': 7, + 'index': 52, + 'doom_type': 2023, + 'region': "The Courtyard (MAP18) Main"}, + 361254: {'name': 'The Courtyard (MAP18) - Chaingun', + 'episode': 2, + 'map': 7, + 'index': 95, + 'doom_type': 2002, + 'region': "The Courtyard (MAP18) Main"}, + 361255: {'name': 'The Courtyard (MAP18) - Rocket launcher', + 'episode': 2, + 'map': 7, + 'index': 130, + 'doom_type': 2003, + 'region': "The Courtyard (MAP18) Main"}, + 361256: {'name': 'The Courtyard (MAP18) - Partial invisibility', + 'episode': 2, + 'map': 7, + 'index': 170, + 'doom_type': 2024, + 'region': "The Courtyard (MAP18) Main"}, + 361257: {'name': 'The Courtyard (MAP18) - Partial invisibility 2', + 'episode': 2, + 'map': 7, + 'index': 171, + 'doom_type': 2024, + 'region': "The Courtyard (MAP18) Main"}, + 361258: {'name': 'The Courtyard (MAP18) - Backpack', + 'episode': 2, + 'map': 7, + 'index': 198, + 'doom_type': 8, + 'region': "The Courtyard (MAP18) Main"}, + 361259: {'name': 'The Courtyard (MAP18) - Supercharge', + 'episode': 2, + 'map': 7, + 'index': 218, + 'doom_type': 2013, + 'region': "The Courtyard (MAP18) Main"}, + 361260: {'name': 'The Courtyard (MAP18) - Invulnerability', + 'episode': 2, + 'map': 7, + 'index': 228, + 'doom_type': 2022, + 'region': "The Courtyard (MAP18) Main"}, + 361261: {'name': 'The Courtyard (MAP18) - Invulnerability 2', + 'episode': 2, + 'map': 7, + 'index': 229, + 'doom_type': 2022, + 'region': "The Courtyard (MAP18) Main"}, + 361262: {'name': 'The Courtyard (MAP18) - Yellow skull key', + 'episode': 2, + 'map': 7, + 'index': 254, + 'doom_type': 39, + 'region': "The Courtyard (MAP18) Main"}, + 361263: {'name': 'The Courtyard (MAP18) - Blue skull key', + 'episode': 2, + 'map': 7, + 'index': 268, + 'doom_type': 40, + 'region': "The Courtyard (MAP18) Yellow"}, + 361264: {'name': 'The Courtyard (MAP18) - BFG9000', + 'episode': 2, + 'map': 7, + 'index': 400, + 'doom_type': 2006, + 'region': "The Courtyard (MAP18) Main"}, + 361265: {'name': 'The Courtyard (MAP18) - Computer area map', + 'episode': 2, + 'map': 7, + 'index': 458, + 'doom_type': 2026, + 'region': "The Courtyard (MAP18) Main"}, + 361266: {'name': 'The Courtyard (MAP18) - Super Shotgun', + 'episode': 2, + 'map': 7, + 'index': 461, + 'doom_type': 82, + 'region': "The Courtyard (MAP18) Main"}, + 361267: {'name': 'The Courtyard (MAP18) - Exit', + 'episode': 2, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "The Courtyard (MAP18) Blue"}, + 361268: {'name': 'The Citadel (MAP19) - Armor', + 'episode': 2, + 'map': 8, + 'index': 64, + 'doom_type': 2018, + 'region': "The Citadel (MAP19) Main"}, + 361269: {'name': 'The Citadel (MAP19) - Chaingun', + 'episode': 2, + 'map': 8, + 'index': 99, + 'doom_type': 2002, + 'region': "The Citadel (MAP19) Main"}, + 361270: {'name': 'The Citadel (MAP19) - Berserk', + 'episode': 2, + 'map': 8, + 'index': 116, + 'doom_type': 2023, + 'region': "The Citadel (MAP19) Main"}, + 361271: {'name': 'The Citadel (MAP19) - Mega Armor', + 'episode': 2, + 'map': 8, + 'index': 127, + 'doom_type': 2019, + 'region': "The Citadel (MAP19) Main"}, + 361272: {'name': 'The Citadel (MAP19) - Supercharge', + 'episode': 2, + 'map': 8, + 'index': 174, + 'doom_type': 2013, + 'region': "The Citadel (MAP19) Main"}, + 361273: {'name': 'The Citadel (MAP19) - Armor 2', + 'episode': 2, + 'map': 8, + 'index': 223, + 'doom_type': 2018, + 'region': "The Citadel (MAP19) Main"}, + 361274: {'name': 'The Citadel (MAP19) - Backpack', + 'episode': 2, + 'map': 8, + 'index': 232, + 'doom_type': 8, + 'region': "The Citadel (MAP19) Main"}, + 361275: {'name': 'The Citadel (MAP19) - Invulnerability', + 'episode': 2, + 'map': 8, + 'index': 315, + 'doom_type': 2022, + 'region': "The Citadel (MAP19) Main"}, + 361276: {'name': 'The Citadel (MAP19) - Blue skull key', + 'episode': 2, + 'map': 8, + 'index': 370, + 'doom_type': 40, + 'region': "The Citadel (MAP19) Main"}, + 361277: {'name': 'The Citadel (MAP19) - Partial invisibility', + 'episode': 2, + 'map': 8, + 'index': 403, + 'doom_type': 2024, + 'region': "The Citadel (MAP19) Main"}, + 361278: {'name': 'The Citadel (MAP19) - Red skull key', + 'episode': 2, + 'map': 8, + 'index': 404, + 'doom_type': 38, + 'region': "The Citadel (MAP19) Main"}, + 361279: {'name': 'The Citadel (MAP19) - Yellow skull key', + 'episode': 2, + 'map': 8, + 'index': 405, + 'doom_type': 39, + 'region': "The Citadel (MAP19) Main"}, + 361280: {'name': 'The Citadel (MAP19) - Computer area map', + 'episode': 2, + 'map': 8, + 'index': 415, + 'doom_type': 2026, + 'region': "The Citadel (MAP19) Main"}, + 361281: {'name': 'The Citadel (MAP19) - Rocket launcher', + 'episode': 2, + 'map': 8, + 'index': 416, + 'doom_type': 2003, + 'region': "The Citadel (MAP19) Main"}, + 361282: {'name': 'The Citadel (MAP19) - Super Shotgun', + 'episode': 2, + 'map': 8, + 'index': 431, + 'doom_type': 82, + 'region': "The Citadel (MAP19) Main"}, + 361283: {'name': 'The Citadel (MAP19) - Exit', + 'episode': 2, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "The Citadel (MAP19) Red"}, + 361284: {'name': 'Gotcha! (MAP20) - Mega Armor', + 'episode': 2, + 'map': 9, + 'index': 9, + 'doom_type': 2019, + 'region': "Gotcha! (MAP20) Main"}, + 361285: {'name': 'Gotcha! (MAP20) - Rocket launcher', + 'episode': 2, + 'map': 9, + 'index': 10, + 'doom_type': 2003, + 'region': "Gotcha! (MAP20) Main"}, + 361286: {'name': 'Gotcha! (MAP20) - Supercharge', + 'episode': 2, + 'map': 9, + 'index': 12, + 'doom_type': 2013, + 'region': "Gotcha! (MAP20) Main"}, + 361287: {'name': 'Gotcha! (MAP20) - Armor', + 'episode': 2, + 'map': 9, + 'index': 33, + 'doom_type': 2018, + 'region': "Gotcha! (MAP20) Main"}, + 361288: {'name': 'Gotcha! (MAP20) - Megasphere', + 'episode': 2, + 'map': 9, + 'index': 43, + 'doom_type': 83, + 'region': "Gotcha! (MAP20) Main"}, + 361289: {'name': 'Gotcha! (MAP20) - Armor 2', + 'episode': 2, + 'map': 9, + 'index': 47, + 'doom_type': 2018, + 'region': "Gotcha! (MAP20) Main"}, + 361290: {'name': 'Gotcha! (MAP20) - Super Shotgun', + 'episode': 2, + 'map': 9, + 'index': 54, + 'doom_type': 82, + 'region': "Gotcha! (MAP20) Main"}, + 361291: {'name': 'Gotcha! (MAP20) - Plasma gun', + 'episode': 2, + 'map': 9, + 'index': 70, + 'doom_type': 2004, + 'region': "Gotcha! (MAP20) Main"}, + 361292: {'name': 'Gotcha! (MAP20) - Mega Armor 2', + 'episode': 2, + 'map': 9, + 'index': 96, + 'doom_type': 2019, + 'region': "Gotcha! (MAP20) Main"}, + 361293: {'name': 'Gotcha! (MAP20) - Berserk', + 'episode': 2, + 'map': 9, + 'index': 109, + 'doom_type': 2023, + 'region': "Gotcha! (MAP20) Main"}, + 361294: {'name': 'Gotcha! (MAP20) - Supercharge 2', + 'episode': 2, + 'map': 9, + 'index': 119, + 'doom_type': 2013, + 'region': "Gotcha! (MAP20) Main"}, + 361295: {'name': 'Gotcha! (MAP20) - Supercharge 3', + 'episode': 2, + 'map': 9, + 'index': 122, + 'doom_type': 2013, + 'region': "Gotcha! (MAP20) Main"}, + 361296: {'name': 'Gotcha! (MAP20) - BFG9000', + 'episode': 2, + 'map': 9, + 'index': 142, + 'doom_type': 2006, + 'region': "Gotcha! (MAP20) Main"}, + 361297: {'name': 'Gotcha! (MAP20) - Supercharge 4', + 'episode': 2, + 'map': 9, + 'index': 145, + 'doom_type': 2013, + 'region': "Gotcha! (MAP20) Main"}, + 361298: {'name': 'Gotcha! (MAP20) - Exit', + 'episode': 2, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "Gotcha! (MAP20) Main"}, + 361299: {'name': 'Nirvana (MAP21) - Super Shotgun', + 'episode': 3, + 'map': 1, + 'index': 70, + 'doom_type': 82, + 'region': "Nirvana (MAP21) Main"}, + 361300: {'name': 'Nirvana (MAP21) - Rocket launcher', + 'episode': 3, + 'map': 1, + 'index': 76, + 'doom_type': 2003, + 'region': "Nirvana (MAP21) Main"}, + 361301: {'name': 'Nirvana (MAP21) - Yellow skull key', + 'episode': 3, + 'map': 1, + 'index': 108, + 'doom_type': 39, + 'region': "Nirvana (MAP21) Main"}, + 361302: {'name': 'Nirvana (MAP21) - Backpack', + 'episode': 3, + 'map': 1, + 'index': 109, + 'doom_type': 8, + 'region': "Nirvana (MAP21) Main"}, + 361303: {'name': 'Nirvana (MAP21) - Megasphere', + 'episode': 3, + 'map': 1, + 'index': 112, + 'doom_type': 83, + 'region': "Nirvana (MAP21) Main"}, + 361304: {'name': 'Nirvana (MAP21) - Invulnerability', + 'episode': 3, + 'map': 1, + 'index': 194, + 'doom_type': 2022, + 'region': "Nirvana (MAP21) Yellow"}, + 361305: {'name': 'Nirvana (MAP21) - Blue skull key', + 'episode': 3, + 'map': 1, + 'index': 199, + 'doom_type': 40, + 'region': "Nirvana (MAP21) Yellow"}, + 361306: {'name': 'Nirvana (MAP21) - Red skull key', + 'episode': 3, + 'map': 1, + 'index': 215, + 'doom_type': 38, + 'region': "Nirvana (MAP21) Yellow"}, + 361307: {'name': 'Nirvana (MAP21) - Exit', + 'episode': 3, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "Nirvana (MAP21) Magenta"}, + 361308: {'name': 'The Catacombs (MAP22) - Rocket launcher', + 'episode': 3, + 'map': 2, + 'index': 4, + 'doom_type': 2003, + 'region': "The Catacombs (MAP22) Main"}, + 361309: {'name': 'The Catacombs (MAP22) - Blue skull key', + 'episode': 3, + 'map': 2, + 'index': 5, + 'doom_type': 40, + 'region': "The Catacombs (MAP22) Main"}, + 361310: {'name': 'The Catacombs (MAP22) - Red skull key', + 'episode': 3, + 'map': 2, + 'index': 12, + 'doom_type': 38, + 'region': "The Catacombs (MAP22) Blue"}, + 361311: {'name': 'The Catacombs (MAP22) - Shotgun', + 'episode': 3, + 'map': 2, + 'index': 28, + 'doom_type': 2001, + 'region': "The Catacombs (MAP22) Main"}, + 361312: {'name': 'The Catacombs (MAP22) - Berserk', + 'episode': 3, + 'map': 2, + 'index': 45, + 'doom_type': 2023, + 'region': "The Catacombs (MAP22) Main"}, + 361313: {'name': 'The Catacombs (MAP22) - Plasma gun', + 'episode': 3, + 'map': 2, + 'index': 83, + 'doom_type': 2004, + 'region': "The Catacombs (MAP22) Main"}, + 361314: {'name': 'The Catacombs (MAP22) - Supercharge', + 'episode': 3, + 'map': 2, + 'index': 118, + 'doom_type': 2013, + 'region': "The Catacombs (MAP22) Main"}, + 361315: {'name': 'The Catacombs (MAP22) - Armor', + 'episode': 3, + 'map': 2, + 'index': 119, + 'doom_type': 2018, + 'region': "The Catacombs (MAP22) Main"}, + 361316: {'name': 'The Catacombs (MAP22) - Exit', + 'episode': 3, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "The Catacombs (MAP22) Red"}, + 361317: {'name': 'Barrels o Fun (MAP23) - Shotgun', + 'episode': 3, + 'map': 3, + 'index': 136, + 'doom_type': 2001, + 'region': "Barrels o Fun (MAP23) Main"}, + 361318: {'name': 'Barrels o Fun (MAP23) - Berserk', + 'episode': 3, + 'map': 3, + 'index': 222, + 'doom_type': 2023, + 'region': "Barrels o Fun (MAP23) Main"}, + 361319: {'name': 'Barrels o Fun (MAP23) - Backpack', + 'episode': 3, + 'map': 3, + 'index': 223, + 'doom_type': 8, + 'region': "Barrels o Fun (MAP23) Main"}, + 361320: {'name': 'Barrels o Fun (MAP23) - Computer area map', + 'episode': 3, + 'map': 3, + 'index': 224, + 'doom_type': 2026, + 'region': "Barrels o Fun (MAP23) Main"}, + 361321: {'name': 'Barrels o Fun (MAP23) - Armor', + 'episode': 3, + 'map': 3, + 'index': 249, + 'doom_type': 2018, + 'region': "Barrels o Fun (MAP23) Main"}, + 361322: {'name': 'Barrels o Fun (MAP23) - Rocket launcher', + 'episode': 3, + 'map': 3, + 'index': 264, + 'doom_type': 2003, + 'region': "Barrels o Fun (MAP23) Main"}, + 361323: {'name': 'Barrels o Fun (MAP23) - Megasphere', + 'episode': 3, + 'map': 3, + 'index': 266, + 'doom_type': 83, + 'region': "Barrels o Fun (MAP23) Main"}, + 361324: {'name': 'Barrels o Fun (MAP23) - Supercharge', + 'episode': 3, + 'map': 3, + 'index': 277, + 'doom_type': 2013, + 'region': "Barrels o Fun (MAP23) Main"}, + 361325: {'name': 'Barrels o Fun (MAP23) - Backpack 2', + 'episode': 3, + 'map': 3, + 'index': 301, + 'doom_type': 8, + 'region': "Barrels o Fun (MAP23) Main"}, + 361326: {'name': 'Barrels o Fun (MAP23) - Yellow skull key', + 'episode': 3, + 'map': 3, + 'index': 307, + 'doom_type': 39, + 'region': "Barrels o Fun (MAP23) Main"}, + 361327: {'name': 'Barrels o Fun (MAP23) - BFG9000', + 'episode': 3, + 'map': 3, + 'index': 342, + 'doom_type': 2006, + 'region': "Barrels o Fun (MAP23) Main"}, + 361328: {'name': 'Barrels o Fun (MAP23) - Exit', + 'episode': 3, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "Barrels o Fun (MAP23) Yellow"}, + 361329: {'name': 'The Chasm (MAP24) - Plasma gun', + 'episode': 3, + 'map': 4, + 'index': 5, + 'doom_type': 2004, + 'region': "The Chasm (MAP24) Main"}, + 361330: {'name': 'The Chasm (MAP24) - Shotgun', + 'episode': 3, + 'map': 4, + 'index': 6, + 'doom_type': 2001, + 'region': "The Chasm (MAP24) Main"}, + 361331: {'name': 'The Chasm (MAP24) - Invulnerability', + 'episode': 3, + 'map': 4, + 'index': 12, + 'doom_type': 2022, + 'region': "The Chasm (MAP24) Main"}, + 361332: {'name': 'The Chasm (MAP24) - Rocket launcher', + 'episode': 3, + 'map': 4, + 'index': 22, + 'doom_type': 2003, + 'region': "The Chasm (MAP24) Main"}, + 361333: {'name': 'The Chasm (MAP24) - Blue keycard', + 'episode': 3, + 'map': 4, + 'index': 23, + 'doom_type': 5, + 'region': "The Chasm (MAP24) Main"}, + 361334: {'name': 'The Chasm (MAP24) - Backpack', + 'episode': 3, + 'map': 4, + 'index': 31, + 'doom_type': 8, + 'region': "The Chasm (MAP24) Main"}, + 361335: {'name': 'The Chasm (MAP24) - Berserk', + 'episode': 3, + 'map': 4, + 'index': 79, + 'doom_type': 2023, + 'region': "The Chasm (MAP24) Main"}, + 361336: {'name': 'The Chasm (MAP24) - Berserk 2', + 'episode': 3, + 'map': 4, + 'index': 155, + 'doom_type': 2023, + 'region': "The Chasm (MAP24) Main"}, + 361337: {'name': 'The Chasm (MAP24) - Armor', + 'episode': 3, + 'map': 4, + 'index': 169, + 'doom_type': 2018, + 'region': "The Chasm (MAP24) Main"}, + 361338: {'name': 'The Chasm (MAP24) - Red keycard', + 'episode': 3, + 'map': 4, + 'index': 261, + 'doom_type': 13, + 'region': "The Chasm (MAP24) Main"}, + 361339: {'name': 'The Chasm (MAP24) - BFG9000', + 'episode': 3, + 'map': 4, + 'index': 295, + 'doom_type': 2006, + 'region': "The Chasm (MAP24) Main"}, + 361340: {'name': 'The Chasm (MAP24) - Super Shotgun', + 'episode': 3, + 'map': 4, + 'index': 353, + 'doom_type': 82, + 'region': "The Chasm (MAP24) Main"}, + 361341: {'name': 'The Chasm (MAP24) - Megasphere', + 'episode': 3, + 'map': 4, + 'index': 355, + 'doom_type': 83, + 'region': "The Chasm (MAP24) Main"}, + 361342: {'name': 'The Chasm (MAP24) - Megasphere 2', + 'episode': 3, + 'map': 4, + 'index': 362, + 'doom_type': 83, + 'region': "The Chasm (MAP24) Main"}, + 361343: {'name': 'The Chasm (MAP24) - Exit', + 'episode': 3, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "The Chasm (MAP24) Red"}, + 361344: {'name': 'Bloodfalls (MAP25) - Super Shotgun', + 'episode': 3, + 'map': 5, + 'index': 6, + 'doom_type': 82, + 'region': "Bloodfalls (MAP25) Main"}, + 361345: {'name': 'Bloodfalls (MAP25) - Partial invisibility', + 'episode': 3, + 'map': 5, + 'index': 7, + 'doom_type': 2024, + 'region': "Bloodfalls (MAP25) Blue"}, + 361346: {'name': 'Bloodfalls (MAP25) - Megasphere', + 'episode': 3, + 'map': 5, + 'index': 23, + 'doom_type': 83, + 'region': "Bloodfalls (MAP25) Main"}, + 361347: {'name': 'Bloodfalls (MAP25) - BFG9000', + 'episode': 3, + 'map': 5, + 'index': 34, + 'doom_type': 2006, + 'region': "Bloodfalls (MAP25) Blue"}, + 361348: {'name': 'Bloodfalls (MAP25) - Mega Armor', + 'episode': 3, + 'map': 5, + 'index': 103, + 'doom_type': 2019, + 'region': "Bloodfalls (MAP25) Main"}, + 361349: {'name': 'Bloodfalls (MAP25) - Armor', + 'episode': 3, + 'map': 5, + 'index': 104, + 'doom_type': 2018, + 'region': "Bloodfalls (MAP25) Main"}, + 361350: {'name': 'Bloodfalls (MAP25) - Blue skull key', + 'episode': 3, + 'map': 5, + 'index': 106, + 'doom_type': 40, + 'region': "Bloodfalls (MAP25) Main"}, + 361351: {'name': 'Bloodfalls (MAP25) - Chaingun', + 'episode': 3, + 'map': 5, + 'index': 150, + 'doom_type': 2002, + 'region': "Bloodfalls (MAP25) Main"}, + 361352: {'name': 'Bloodfalls (MAP25) - Plasma gun', + 'episode': 3, + 'map': 5, + 'index': 169, + 'doom_type': 2004, + 'region': "Bloodfalls (MAP25) Main"}, + 361353: {'name': 'Bloodfalls (MAP25) - BFG9000 2', + 'episode': 3, + 'map': 5, + 'index': 186, + 'doom_type': 2006, + 'region': "Bloodfalls (MAP25) Main"}, + 361354: {'name': 'Bloodfalls (MAP25) - Rocket launcher', + 'episode': 3, + 'map': 5, + 'index': 236, + 'doom_type': 2003, + 'region': "Bloodfalls (MAP25) Main"}, + 361355: {'name': 'Bloodfalls (MAP25) - Exit', + 'episode': 3, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "Bloodfalls (MAP25) Blue"}, + 361356: {'name': 'The Abandoned Mines (MAP26) - Blue keycard', + 'episode': 3, + 'map': 6, + 'index': 20, + 'doom_type': 5, + 'region': "The Abandoned Mines (MAP26) Red"}, + 361357: {'name': 'The Abandoned Mines (MAP26) - Super Shotgun', + 'episode': 3, + 'map': 6, + 'index': 21, + 'doom_type': 82, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361358: {'name': 'The Abandoned Mines (MAP26) - Rocket launcher', + 'episode': 3, + 'map': 6, + 'index': 49, + 'doom_type': 2003, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361359: {'name': 'The Abandoned Mines (MAP26) - Mega Armor', + 'episode': 3, + 'map': 6, + 'index': 95, + 'doom_type': 2019, + 'region': "The Abandoned Mines (MAP26) Red"}, + 361360: {'name': 'The Abandoned Mines (MAP26) - Plasma gun', + 'episode': 3, + 'map': 6, + 'index': 107, + 'doom_type': 2004, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361361: {'name': 'The Abandoned Mines (MAP26) - Supercharge', + 'episode': 3, + 'map': 6, + 'index': 154, + 'doom_type': 2013, + 'region': "The Abandoned Mines (MAP26) Red"}, + 361362: {'name': 'The Abandoned Mines (MAP26) - Chaingun', + 'episode': 3, + 'map': 6, + 'index': 155, + 'doom_type': 2002, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361363: {'name': 'The Abandoned Mines (MAP26) - Partial invisibility', + 'episode': 3, + 'map': 6, + 'index': 159, + 'doom_type': 2024, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361364: {'name': 'The Abandoned Mines (MAP26) - Armor', + 'episode': 3, + 'map': 6, + 'index': 170, + 'doom_type': 2018, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361365: {'name': 'The Abandoned Mines (MAP26) - Red keycard', + 'episode': 3, + 'map': 6, + 'index': 182, + 'doom_type': 13, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361366: {'name': 'The Abandoned Mines (MAP26) - Yellow keycard', + 'episode': 3, + 'map': 6, + 'index': 229, + 'doom_type': 6, + 'region': "The Abandoned Mines (MAP26) Blue"}, + 361367: {'name': 'The Abandoned Mines (MAP26) - Backpack', + 'episode': 3, + 'map': 6, + 'index': 254, + 'doom_type': 8, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361368: {'name': 'The Abandoned Mines (MAP26) - Exit', + 'episode': 3, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "The Abandoned Mines (MAP26) Yellow"}, + 361369: {'name': 'Monster Condo (MAP27) - Rocket launcher', + 'episode': 3, + 'map': 7, + 'index': 4, + 'doom_type': 2003, + 'region': "Monster Condo (MAP27) Main"}, + 361370: {'name': 'Monster Condo (MAP27) - Partial invisibility', + 'episode': 3, + 'map': 7, + 'index': 51, + 'doom_type': 2024, + 'region': "Monster Condo (MAP27) Main"}, + 361371: {'name': 'Monster Condo (MAP27) - Plasma gun', + 'episode': 3, + 'map': 7, + 'index': 58, + 'doom_type': 2004, + 'region': "Monster Condo (MAP27) Main"}, + 361372: {'name': 'Monster Condo (MAP27) - Invulnerability', + 'episode': 3, + 'map': 7, + 'index': 60, + 'doom_type': 2022, + 'region': "Monster Condo (MAP27) Main"}, + 361373: {'name': 'Monster Condo (MAP27) - Armor', + 'episode': 3, + 'map': 7, + 'index': 86, + 'doom_type': 2018, + 'region': "Monster Condo (MAP27) Main"}, + 361374: {'name': 'Monster Condo (MAP27) - Backpack', + 'episode': 3, + 'map': 7, + 'index': 105, + 'doom_type': 8, + 'region': "Monster Condo (MAP27) Main"}, + 361375: {'name': 'Monster Condo (MAP27) - Invulnerability 2', + 'episode': 3, + 'map': 7, + 'index': 107, + 'doom_type': 2022, + 'region': "Monster Condo (MAP27) Main"}, + 361376: {'name': 'Monster Condo (MAP27) - Partial invisibility 2', + 'episode': 3, + 'map': 7, + 'index': 122, + 'doom_type': 2024, + 'region': "Monster Condo (MAP27) Main"}, + 361377: {'name': 'Monster Condo (MAP27) - Supercharge', + 'episode': 3, + 'map': 7, + 'index': 236, + 'doom_type': 2013, + 'region': "Monster Condo (MAP27) Main"}, + 361378: {'name': 'Monster Condo (MAP27) - Armor 2', + 'episode': 3, + 'map': 7, + 'index': 239, + 'doom_type': 2018, + 'region': "Monster Condo (MAP27) Main"}, + 361379: {'name': 'Monster Condo (MAP27) - Chaingun', + 'episode': 3, + 'map': 7, + 'index': 251, + 'doom_type': 2002, + 'region': "Monster Condo (MAP27) Main"}, + 361380: {'name': 'Monster Condo (MAP27) - BFG9000', + 'episode': 3, + 'map': 7, + 'index': 279, + 'doom_type': 2006, + 'region': "Monster Condo (MAP27) Main"}, + 361381: {'name': 'Monster Condo (MAP27) - Backpack 2', + 'episode': 3, + 'map': 7, + 'index': 285, + 'doom_type': 8, + 'region': "Monster Condo (MAP27) Main"}, + 361382: {'name': 'Monster Condo (MAP27) - Backpack 3', + 'episode': 3, + 'map': 7, + 'index': 286, + 'doom_type': 8, + 'region': "Monster Condo (MAP27) Main"}, + 361383: {'name': 'Monster Condo (MAP27) - Backpack 4', + 'episode': 3, + 'map': 7, + 'index': 287, + 'doom_type': 8, + 'region': "Monster Condo (MAP27) Main"}, + 361384: {'name': 'Monster Condo (MAP27) - Yellow skull key', + 'episode': 3, + 'map': 7, + 'index': 310, + 'doom_type': 39, + 'region': "Monster Condo (MAP27) Main"}, + 361385: {'name': 'Monster Condo (MAP27) - Red skull key', + 'episode': 3, + 'map': 7, + 'index': 364, + 'doom_type': 38, + 'region': "Monster Condo (MAP27) Blue"}, + 361386: {'name': 'Monster Condo (MAP27) - Supercharge 2', + 'episode': 3, + 'map': 7, + 'index': 365, + 'doom_type': 2013, + 'region': "Monster Condo (MAP27) Blue"}, + 361387: {'name': 'Monster Condo (MAP27) - Blue skull key', + 'episode': 3, + 'map': 7, + 'index': 382, + 'doom_type': 40, + 'region': "Monster Condo (MAP27) Yellow"}, + 361388: {'name': 'Monster Condo (MAP27) - Supercharge 3', + 'episode': 3, + 'map': 7, + 'index': 392, + 'doom_type': 2013, + 'region': "Monster Condo (MAP27) Yellow"}, + 361389: {'name': 'Monster Condo (MAP27) - Computer area map', + 'episode': 3, + 'map': 7, + 'index': 393, + 'doom_type': 2026, + 'region': "Monster Condo (MAP27) Yellow"}, + 361390: {'name': 'Monster Condo (MAP27) - Berserk', + 'episode': 3, + 'map': 7, + 'index': 394, + 'doom_type': 2023, + 'region': "Monster Condo (MAP27) Yellow"}, + 361391: {'name': 'Monster Condo (MAP27) - Supercharge 4', + 'episode': 3, + 'map': 7, + 'index': 414, + 'doom_type': 2013, + 'region': "Monster Condo (MAP27) Yellow"}, + 361392: {'name': 'Monster Condo (MAP27) - Supercharge 5', + 'episode': 3, + 'map': 7, + 'index': 424, + 'doom_type': 2013, + 'region': "Monster Condo (MAP27) Yellow"}, + 361393: {'name': 'Monster Condo (MAP27) - Computer area map 2', + 'episode': 3, + 'map': 7, + 'index': 425, + 'doom_type': 2026, + 'region': "Monster Condo (MAP27) Yellow"}, + 361394: {'name': 'Monster Condo (MAP27) - Berserk 2', + 'episode': 3, + 'map': 7, + 'index': 426, + 'doom_type': 2023, + 'region': "Monster Condo (MAP27) Yellow"}, + 361395: {'name': 'Monster Condo (MAP27) - Partial invisibility 3', + 'episode': 3, + 'map': 7, + 'index': 454, + 'doom_type': 2024, + 'region': "Monster Condo (MAP27) Yellow"}, + 361396: {'name': 'Monster Condo (MAP27) - Invulnerability 3', + 'episode': 3, + 'map': 7, + 'index': 455, + 'doom_type': 2022, + 'region': "Monster Condo (MAP27) Yellow"}, + 361397: {'name': 'Monster Condo (MAP27) - Chainsaw', + 'episode': 3, + 'map': 7, + 'index': 460, + 'doom_type': 2005, + 'region': "Monster Condo (MAP27) Main"}, + 361398: {'name': 'Monster Condo (MAP27) - Super Shotgun', + 'episode': 3, + 'map': 7, + 'index': 470, + 'doom_type': 82, + 'region': "Monster Condo (MAP27) Main"}, + 361399: {'name': 'Monster Condo (MAP27) - Exit', + 'episode': 3, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "Monster Condo (MAP27) Red"}, + 361400: {'name': 'The Spirit World (MAP28) - Armor', + 'episode': 3, + 'map': 8, + 'index': 19, + 'doom_type': 2018, + 'region': "The Spirit World (MAP28) Main"}, + 361401: {'name': 'The Spirit World (MAP28) - Chainsaw', + 'episode': 3, + 'map': 8, + 'index': 66, + 'doom_type': 2005, + 'region': "The Spirit World (MAP28) Main"}, + 361402: {'name': 'The Spirit World (MAP28) - Invulnerability', + 'episode': 3, + 'map': 8, + 'index': 76, + 'doom_type': 2022, + 'region': "The Spirit World (MAP28) Main"}, + 361403: {'name': 'The Spirit World (MAP28) - Yellow skull key', + 'episode': 3, + 'map': 8, + 'index': 87, + 'doom_type': 39, + 'region': "The Spirit World (MAP28) Main"}, + 361404: {'name': 'The Spirit World (MAP28) - Supercharge', + 'episode': 3, + 'map': 8, + 'index': 95, + 'doom_type': 2013, + 'region': "The Spirit World (MAP28) Main"}, + 361405: {'name': 'The Spirit World (MAP28) - Chaingun', + 'episode': 3, + 'map': 8, + 'index': 96, + 'doom_type': 2002, + 'region': "The Spirit World (MAP28) Main"}, + 361406: {'name': 'The Spirit World (MAP28) - Rocket launcher', + 'episode': 3, + 'map': 8, + 'index': 124, + 'doom_type': 2003, + 'region': "The Spirit World (MAP28) Main"}, + 361407: {'name': 'The Spirit World (MAP28) - Backpack', + 'episode': 3, + 'map': 8, + 'index': 155, + 'doom_type': 8, + 'region': "The Spirit World (MAP28) Main"}, + 361408: {'name': 'The Spirit World (MAP28) - Backpack 2', + 'episode': 3, + 'map': 8, + 'index': 156, + 'doom_type': 8, + 'region': "The Spirit World (MAP28) Main"}, + 361409: {'name': 'The Spirit World (MAP28) - Backpack 3', + 'episode': 3, + 'map': 8, + 'index': 157, + 'doom_type': 8, + 'region': "The Spirit World (MAP28) Main"}, + 361410: {'name': 'The Spirit World (MAP28) - Backpack 4', + 'episode': 3, + 'map': 8, + 'index': 158, + 'doom_type': 8, + 'region': "The Spirit World (MAP28) Main"}, + 361411: {'name': 'The Spirit World (MAP28) - Berserk', + 'episode': 3, + 'map': 8, + 'index': 159, + 'doom_type': 2023, + 'region': "The Spirit World (MAP28) Main"}, + 361412: {'name': 'The Spirit World (MAP28) - Plasma gun', + 'episode': 3, + 'map': 8, + 'index': 163, + 'doom_type': 2004, + 'region': "The Spirit World (MAP28) Main"}, + 361413: {'name': 'The Spirit World (MAP28) - Invulnerability 2', + 'episode': 3, + 'map': 8, + 'index': 179, + 'doom_type': 2022, + 'region': "The Spirit World (MAP28) Main"}, + 361414: {'name': 'The Spirit World (MAP28) - Invulnerability 3', + 'episode': 3, + 'map': 8, + 'index': 180, + 'doom_type': 2022, + 'region': "The Spirit World (MAP28) Main"}, + 361415: {'name': 'The Spirit World (MAP28) - BFG9000', + 'episode': 3, + 'map': 8, + 'index': 181, + 'doom_type': 2006, + 'region': "The Spirit World (MAP28) Main"}, + 361416: {'name': 'The Spirit World (MAP28) - Megasphere', + 'episode': 3, + 'map': 8, + 'index': 183, + 'doom_type': 83, + 'region': "The Spirit World (MAP28) Main"}, + 361417: {'name': 'The Spirit World (MAP28) - Megasphere 2', + 'episode': 3, + 'map': 8, + 'index': 185, + 'doom_type': 83, + 'region': "The Spirit World (MAP28) Main"}, + 361418: {'name': 'The Spirit World (MAP28) - Invulnerability 4', + 'episode': 3, + 'map': 8, + 'index': 186, + 'doom_type': 2022, + 'region': "The Spirit World (MAP28) Main"}, + 361419: {'name': 'The Spirit World (MAP28) - Invulnerability 5', + 'episode': 3, + 'map': 8, + 'index': 195, + 'doom_type': 2022, + 'region': "The Spirit World (MAP28) Main"}, + 361420: {'name': 'The Spirit World (MAP28) - Super Shotgun', + 'episode': 3, + 'map': 8, + 'index': 214, + 'doom_type': 82, + 'region': "The Spirit World (MAP28) Main"}, + 361421: {'name': 'The Spirit World (MAP28) - Red skull key', + 'episode': 3, + 'map': 8, + 'index': 216, + 'doom_type': 38, + 'region': "The Spirit World (MAP28) Yellow"}, + 361422: {'name': 'The Spirit World (MAP28) - Exit', + 'episode': 3, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "The Spirit World (MAP28) Red"}, + 361423: {'name': 'The Living End (MAP29) - Chaingun', + 'episode': 3, + 'map': 9, + 'index': 85, + 'doom_type': 2002, + 'region': "The Living End (MAP29) Main"}, + 361424: {'name': 'The Living End (MAP29) - Plasma gun', + 'episode': 3, + 'map': 9, + 'index': 124, + 'doom_type': 2004, + 'region': "The Living End (MAP29) Main"}, + 361425: {'name': 'The Living End (MAP29) - Backpack', + 'episode': 3, + 'map': 9, + 'index': 179, + 'doom_type': 8, + 'region': "The Living End (MAP29) Main"}, + 361426: {'name': 'The Living End (MAP29) - Super Shotgun', + 'episode': 3, + 'map': 9, + 'index': 195, + 'doom_type': 82, + 'region': "The Living End (MAP29) Main"}, + 361427: {'name': 'The Living End (MAP29) - Mega Armor', + 'episode': 3, + 'map': 9, + 'index': 216, + 'doom_type': 2019, + 'region': "The Living End (MAP29) Main"}, + 361428: {'name': 'The Living End (MAP29) - Armor', + 'episode': 3, + 'map': 9, + 'index': 224, + 'doom_type': 2018, + 'region': "The Living End (MAP29) Main"}, + 361429: {'name': 'The Living End (MAP29) - Backpack 2', + 'episode': 3, + 'map': 9, + 'index': 235, + 'doom_type': 8, + 'region': "The Living End (MAP29) Main"}, + 361430: {'name': 'The Living End (MAP29) - Supercharge', + 'episode': 3, + 'map': 9, + 'index': 237, + 'doom_type': 2013, + 'region': "The Living End (MAP29) Main"}, + 361431: {'name': 'The Living End (MAP29) - Berserk', + 'episode': 3, + 'map': 9, + 'index': 241, + 'doom_type': 2023, + 'region': "The Living End (MAP29) Main"}, + 361432: {'name': 'The Living End (MAP29) - Berserk 2', + 'episode': 3, + 'map': 9, + 'index': 263, + 'doom_type': 2023, + 'region': "The Living End (MAP29) Main"}, + 361433: {'name': 'The Living End (MAP29) - Exit', + 'episode': 3, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "The Living End (MAP29) Main"}, + 361434: {'name': 'Icon of Sin (MAP30) - Supercharge', + 'episode': 3, + 'map': 10, + 'index': 25, + 'doom_type': 2013, + 'region': "Icon of Sin (MAP30) Main"}, + 361435: {'name': 'Icon of Sin (MAP30) - Supercharge 2', + 'episode': 3, + 'map': 10, + 'index': 26, + 'doom_type': 2013, + 'region': "Icon of Sin (MAP30) Main"}, + 361436: {'name': 'Icon of Sin (MAP30) - Supercharge 3', + 'episode': 3, + 'map': 10, + 'index': 28, + 'doom_type': 2013, + 'region': "Icon of Sin (MAP30) Main"}, + 361437: {'name': 'Icon of Sin (MAP30) - Invulnerability', + 'episode': 3, + 'map': 10, + 'index': 29, + 'doom_type': 2022, + 'region': "Icon of Sin (MAP30) Main"}, + 361438: {'name': 'Icon of Sin (MAP30) - Invulnerability 2', + 'episode': 3, + 'map': 10, + 'index': 30, + 'doom_type': 2022, + 'region': "Icon of Sin (MAP30) Main"}, + 361439: {'name': 'Icon of Sin (MAP30) - Invulnerability 3', + 'episode': 3, + 'map': 10, + 'index': 31, + 'doom_type': 2022, + 'region': "Icon of Sin (MAP30) Main"}, + 361440: {'name': 'Icon of Sin (MAP30) - Invulnerability 4', + 'episode': 3, + 'map': 10, + 'index': 32, + 'doom_type': 2022, + 'region': "Icon of Sin (MAP30) Main"}, + 361441: {'name': 'Icon of Sin (MAP30) - BFG9000', + 'episode': 3, + 'map': 10, + 'index': 40, + 'doom_type': 2006, + 'region': "Icon of Sin (MAP30) Main"}, + 361442: {'name': 'Icon of Sin (MAP30) - Chaingun', + 'episode': 3, + 'map': 10, + 'index': 41, + 'doom_type': 2002, + 'region': "Icon of Sin (MAP30) Main"}, + 361443: {'name': 'Icon of Sin (MAP30) - Chainsaw', + 'episode': 3, + 'map': 10, + 'index': 42, + 'doom_type': 2005, + 'region': "Icon of Sin (MAP30) Main"}, + 361444: {'name': 'Icon of Sin (MAP30) - Plasma gun', + 'episode': 3, + 'map': 10, + 'index': 43, + 'doom_type': 2004, + 'region': "Icon of Sin (MAP30) Main"}, + 361445: {'name': 'Icon of Sin (MAP30) - Rocket launcher', + 'episode': 3, + 'map': 10, + 'index': 44, + 'doom_type': 2003, + 'region': "Icon of Sin (MAP30) Main"}, + 361446: {'name': 'Icon of Sin (MAP30) - Shotgun', + 'episode': 3, + 'map': 10, + 'index': 45, + 'doom_type': 2001, + 'region': "Icon of Sin (MAP30) Main"}, + 361447: {'name': 'Icon of Sin (MAP30) - Super Shotgun', + 'episode': 3, + 'map': 10, + 'index': 46, + 'doom_type': 82, + 'region': "Icon of Sin (MAP30) Main"}, + 361448: {'name': 'Icon of Sin (MAP30) - Backpack', + 'episode': 3, + 'map': 10, + 'index': 47, + 'doom_type': 8, + 'region': "Icon of Sin (MAP30) Main"}, + 361449: {'name': 'Icon of Sin (MAP30) - Megasphere', + 'episode': 3, + 'map': 10, + 'index': 64, + 'doom_type': 83, + 'region': "Icon of Sin (MAP30) Main"}, + 361450: {'name': 'Icon of Sin (MAP30) - Megasphere 2', + 'episode': 3, + 'map': 10, + 'index': 85, + 'doom_type': 83, + 'region': "Icon of Sin (MAP30) Main"}, + 361451: {'name': 'Icon of Sin (MAP30) - Berserk', + 'episode': 3, + 'map': 10, + 'index': 94, + 'doom_type': 2023, + 'region': "Icon of Sin (MAP30) Main"}, + 361452: {'name': 'Icon of Sin (MAP30) - Exit', + 'episode': 3, + 'map': 10, + 'index': -1, + 'doom_type': -1, + 'region': "Icon of Sin (MAP30) Main"}, + 361453: {'name': 'Wolfenstein2 (MAP31) - Rocket launcher', + 'episode': 4, + 'map': 1, + 'index': 110, + 'doom_type': 2003, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361454: {'name': 'Wolfenstein2 (MAP31) - Shotgun', + 'episode': 4, + 'map': 1, + 'index': 139, + 'doom_type': 2001, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361455: {'name': 'Wolfenstein2 (MAP31) - Berserk', + 'episode': 4, + 'map': 1, + 'index': 263, + 'doom_type': 2023, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361456: {'name': 'Wolfenstein2 (MAP31) - Supercharge', + 'episode': 4, + 'map': 1, + 'index': 278, + 'doom_type': 2013, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361457: {'name': 'Wolfenstein2 (MAP31) - Chaingun', + 'episode': 4, + 'map': 1, + 'index': 305, + 'doom_type': 2002, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361458: {'name': 'Wolfenstein2 (MAP31) - Super Shotgun', + 'episode': 4, + 'map': 1, + 'index': 308, + 'doom_type': 82, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361459: {'name': 'Wolfenstein2 (MAP31) - Partial invisibility', + 'episode': 4, + 'map': 1, + 'index': 309, + 'doom_type': 2024, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361460: {'name': 'Wolfenstein2 (MAP31) - Megasphere', + 'episode': 4, + 'map': 1, + 'index': 310, + 'doom_type': 83, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361461: {'name': 'Wolfenstein2 (MAP31) - Backpack', + 'episode': 4, + 'map': 1, + 'index': 311, + 'doom_type': 8, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361462: {'name': 'Wolfenstein2 (MAP31) - Backpack 2', + 'episode': 4, + 'map': 1, + 'index': 312, + 'doom_type': 8, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361463: {'name': 'Wolfenstein2 (MAP31) - Backpack 3', + 'episode': 4, + 'map': 1, + 'index': 313, + 'doom_type': 8, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361464: {'name': 'Wolfenstein2 (MAP31) - Backpack 4', + 'episode': 4, + 'map': 1, + 'index': 314, + 'doom_type': 8, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361465: {'name': 'Wolfenstein2 (MAP31) - BFG9000', + 'episode': 4, + 'map': 1, + 'index': 315, + 'doom_type': 2006, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361466: {'name': 'Wolfenstein2 (MAP31) - Plasma gun', + 'episode': 4, + 'map': 1, + 'index': 316, + 'doom_type': 2004, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361467: {'name': 'Wolfenstein2 (MAP31) - Exit', + 'episode': 4, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361468: {'name': 'Grosse2 (MAP32) - Plasma gun', + 'episode': 4, + 'map': 2, + 'index': 33, + 'doom_type': 2004, + 'region': "Grosse2 (MAP32) Main"}, + 361469: {'name': 'Grosse2 (MAP32) - Rocket launcher', + 'episode': 4, + 'map': 2, + 'index': 57, + 'doom_type': 2003, + 'region': "Grosse2 (MAP32) Main"}, + 361470: {'name': 'Grosse2 (MAP32) - Invulnerability', + 'episode': 4, + 'map': 2, + 'index': 70, + 'doom_type': 2022, + 'region': "Grosse2 (MAP32) Main"}, + 361471: {'name': 'Grosse2 (MAP32) - Super Shotgun', + 'episode': 4, + 'map': 2, + 'index': 74, + 'doom_type': 82, + 'region': "Grosse2 (MAP32) Main"}, + 361472: {'name': 'Grosse2 (MAP32) - BFG9000', + 'episode': 4, + 'map': 2, + 'index': 75, + 'doom_type': 2006, + 'region': "Grosse2 (MAP32) Main"}, + 361473: {'name': 'Grosse2 (MAP32) - Megasphere', + 'episode': 4, + 'map': 2, + 'index': 78, + 'doom_type': 83, + 'region': "Grosse2 (MAP32) Main"}, + 361474: {'name': 'Grosse2 (MAP32) - Chaingun', + 'episode': 4, + 'map': 2, + 'index': 79, + 'doom_type': 2002, + 'region': "Grosse2 (MAP32) Main"}, + 361475: {'name': 'Grosse2 (MAP32) - Chaingun 2', + 'episode': 4, + 'map': 2, + 'index': 80, + 'doom_type': 2002, + 'region': "Grosse2 (MAP32) Main"}, + 361476: {'name': 'Grosse2 (MAP32) - Chaingun 3', + 'episode': 4, + 'map': 2, + 'index': 81, + 'doom_type': 2002, + 'region': "Grosse2 (MAP32) Main"}, + 361477: {'name': 'Grosse2 (MAP32) - Berserk', + 'episode': 4, + 'map': 2, + 'index': 82, + 'doom_type': 2023, + 'region': "Grosse2 (MAP32) Main"}, + 361478: {'name': 'Grosse2 (MAP32) - Exit', + 'episode': 4, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "Grosse2 (MAP32) Main"}, +} + + +location_name_groups: Dict[str, Set[str]] = { + 'Barrels o Fun (MAP23)': { + 'Barrels o Fun (MAP23) - Armor', + 'Barrels o Fun (MAP23) - BFG9000', + 'Barrels o Fun (MAP23) - Backpack', + 'Barrels o Fun (MAP23) - Backpack 2', + 'Barrels o Fun (MAP23) - Berserk', + 'Barrels o Fun (MAP23) - Computer area map', + 'Barrels o Fun (MAP23) - Exit', + 'Barrels o Fun (MAP23) - Megasphere', + 'Barrels o Fun (MAP23) - Rocket launcher', + 'Barrels o Fun (MAP23) - Shotgun', + 'Barrels o Fun (MAP23) - Supercharge', + 'Barrels o Fun (MAP23) - Yellow skull key', + }, + 'Bloodfalls (MAP25)': { + 'Bloodfalls (MAP25) - Armor', + 'Bloodfalls (MAP25) - BFG9000', + 'Bloodfalls (MAP25) - BFG9000 2', + 'Bloodfalls (MAP25) - Blue skull key', + 'Bloodfalls (MAP25) - Chaingun', + 'Bloodfalls (MAP25) - Exit', + 'Bloodfalls (MAP25) - Mega Armor', + 'Bloodfalls (MAP25) - Megasphere', + 'Bloodfalls (MAP25) - Partial invisibility', + 'Bloodfalls (MAP25) - Plasma gun', + 'Bloodfalls (MAP25) - Rocket launcher', + 'Bloodfalls (MAP25) - Super Shotgun', + }, + 'Circle of Death (MAP11)': { + 'Circle of Death (MAP11) - Armor', + 'Circle of Death (MAP11) - BFG9000', + 'Circle of Death (MAP11) - Backpack', + 'Circle of Death (MAP11) - Blue keycard', + 'Circle of Death (MAP11) - Chaingun', + 'Circle of Death (MAP11) - Exit', + 'Circle of Death (MAP11) - Invulnerability', + 'Circle of Death (MAP11) - Mega Armor', + 'Circle of Death (MAP11) - Partial invisibility', + 'Circle of Death (MAP11) - Plasma gun', + 'Circle of Death (MAP11) - Red keycard', + 'Circle of Death (MAP11) - Rocket launcher', + 'Circle of Death (MAP11) - Shotgun', + 'Circle of Death (MAP11) - Supercharge', + 'Circle of Death (MAP11) - Supercharge 2', + }, + 'Dead Simple (MAP07)': { + 'Dead Simple (MAP07) - Backpack', + 'Dead Simple (MAP07) - Berserk', + 'Dead Simple (MAP07) - Chaingun', + 'Dead Simple (MAP07) - Exit', + 'Dead Simple (MAP07) - Megasphere', + 'Dead Simple (MAP07) - Partial invisibility', + 'Dead Simple (MAP07) - Partial invisibility 2', + 'Dead Simple (MAP07) - Partial invisibility 3', + 'Dead Simple (MAP07) - Partial invisibility 4', + 'Dead Simple (MAP07) - Plasma gun', + 'Dead Simple (MAP07) - Rocket launcher', + 'Dead Simple (MAP07) - Super Shotgun', + }, + 'Downtown (MAP13)': { + 'Downtown (MAP13) - BFG9000', + 'Downtown (MAP13) - Backpack', + 'Downtown (MAP13) - Berserk', + 'Downtown (MAP13) - Berserk 2', + 'Downtown (MAP13) - Berserk 3', + 'Downtown (MAP13) - Blue keycard', + 'Downtown (MAP13) - Chaingun', + 'Downtown (MAP13) - Chainsaw', + 'Downtown (MAP13) - Computer area map', + 'Downtown (MAP13) - Exit', + 'Downtown (MAP13) - Invulnerability', + 'Downtown (MAP13) - Invulnerability 2', + 'Downtown (MAP13) - Mega Armor', + 'Downtown (MAP13) - Partial invisibility', + 'Downtown (MAP13) - Partial invisibility 2', + 'Downtown (MAP13) - Partial invisibility 3', + 'Downtown (MAP13) - Plasma gun', + 'Downtown (MAP13) - Red keycard', + 'Downtown (MAP13) - Rocket launcher', + 'Downtown (MAP13) - Shotgun', + 'Downtown (MAP13) - Supercharge', + 'Downtown (MAP13) - Yellow keycard', + }, + 'Entryway (MAP01)': { + 'Entryway (MAP01) - Armor', + 'Entryway (MAP01) - Chainsaw', + 'Entryway (MAP01) - Exit', + 'Entryway (MAP01) - Rocket launcher', + 'Entryway (MAP01) - Shotgun', + }, + 'Gotcha! (MAP20)': { + 'Gotcha! (MAP20) - Armor', + 'Gotcha! (MAP20) - Armor 2', + 'Gotcha! (MAP20) - BFG9000', + 'Gotcha! (MAP20) - Berserk', + 'Gotcha! (MAP20) - Exit', + 'Gotcha! (MAP20) - Mega Armor', + 'Gotcha! (MAP20) - Mega Armor 2', + 'Gotcha! (MAP20) - Megasphere', + 'Gotcha! (MAP20) - Plasma gun', + 'Gotcha! (MAP20) - Rocket launcher', + 'Gotcha! (MAP20) - Super Shotgun', + 'Gotcha! (MAP20) - Supercharge', + 'Gotcha! (MAP20) - Supercharge 2', + 'Gotcha! (MAP20) - Supercharge 3', + 'Gotcha! (MAP20) - Supercharge 4', + }, + 'Grosse2 (MAP32)': { + 'Grosse2 (MAP32) - BFG9000', + 'Grosse2 (MAP32) - Berserk', + 'Grosse2 (MAP32) - Chaingun', + 'Grosse2 (MAP32) - Chaingun 2', + 'Grosse2 (MAP32) - Chaingun 3', + 'Grosse2 (MAP32) - Exit', + 'Grosse2 (MAP32) - Invulnerability', + 'Grosse2 (MAP32) - Megasphere', + 'Grosse2 (MAP32) - Plasma gun', + 'Grosse2 (MAP32) - Rocket launcher', + 'Grosse2 (MAP32) - Super Shotgun', + }, + 'Icon of Sin (MAP30)': { + 'Icon of Sin (MAP30) - BFG9000', + 'Icon of Sin (MAP30) - Backpack', + 'Icon of Sin (MAP30) - Berserk', + 'Icon of Sin (MAP30) - Chaingun', + 'Icon of Sin (MAP30) - Chainsaw', + 'Icon of Sin (MAP30) - Exit', + 'Icon of Sin (MAP30) - Invulnerability', + 'Icon of Sin (MAP30) - Invulnerability 2', + 'Icon of Sin (MAP30) - Invulnerability 3', + 'Icon of Sin (MAP30) - Invulnerability 4', + 'Icon of Sin (MAP30) - Megasphere', + 'Icon of Sin (MAP30) - Megasphere 2', + 'Icon of Sin (MAP30) - Plasma gun', + 'Icon of Sin (MAP30) - Rocket launcher', + 'Icon of Sin (MAP30) - Shotgun', + 'Icon of Sin (MAP30) - Super Shotgun', + 'Icon of Sin (MAP30) - Supercharge', + 'Icon of Sin (MAP30) - Supercharge 2', + 'Icon of Sin (MAP30) - Supercharge 3', + }, + 'Industrial Zone (MAP15)': { + 'Industrial Zone (MAP15) - Armor', + 'Industrial Zone (MAP15) - BFG9000', + 'Industrial Zone (MAP15) - Backpack', + 'Industrial Zone (MAP15) - Backpack 2', + 'Industrial Zone (MAP15) - Berserk', + 'Industrial Zone (MAP15) - Berserk 2', + 'Industrial Zone (MAP15) - Blue keycard', + 'Industrial Zone (MAP15) - Chaingun', + 'Industrial Zone (MAP15) - Chainsaw', + 'Industrial Zone (MAP15) - Computer area map', + 'Industrial Zone (MAP15) - Exit', + 'Industrial Zone (MAP15) - Invulnerability', + 'Industrial Zone (MAP15) - Mega Armor', + 'Industrial Zone (MAP15) - Mega Armor 2', + 'Industrial Zone (MAP15) - Megasphere', + 'Industrial Zone (MAP15) - Partial invisibility', + 'Industrial Zone (MAP15) - Partial invisibility 2', + 'Industrial Zone (MAP15) - Plasma gun', + 'Industrial Zone (MAP15) - Red keycard', + 'Industrial Zone (MAP15) - Rocket launcher', + 'Industrial Zone (MAP15) - Shotgun', + 'Industrial Zone (MAP15) - Supercharge', + 'Industrial Zone (MAP15) - Yellow keycard', + }, + 'Monster Condo (MAP27)': { + 'Monster Condo (MAP27) - Armor', + 'Monster Condo (MAP27) - Armor 2', + 'Monster Condo (MAP27) - BFG9000', + 'Monster Condo (MAP27) - Backpack', + 'Monster Condo (MAP27) - Backpack 2', + 'Monster Condo (MAP27) - Backpack 3', + 'Monster Condo (MAP27) - Backpack 4', + 'Monster Condo (MAP27) - Berserk', + 'Monster Condo (MAP27) - Berserk 2', + 'Monster Condo (MAP27) - Blue skull key', + 'Monster Condo (MAP27) - Chaingun', + 'Monster Condo (MAP27) - Chainsaw', + 'Monster Condo (MAP27) - Computer area map', + 'Monster Condo (MAP27) - Computer area map 2', + 'Monster Condo (MAP27) - Exit', + 'Monster Condo (MAP27) - Invulnerability', + 'Monster Condo (MAP27) - Invulnerability 2', + 'Monster Condo (MAP27) - Invulnerability 3', + 'Monster Condo (MAP27) - Partial invisibility', + 'Monster Condo (MAP27) - Partial invisibility 2', + 'Monster Condo (MAP27) - Partial invisibility 3', + 'Monster Condo (MAP27) - Plasma gun', + 'Monster Condo (MAP27) - Red skull key', + 'Monster Condo (MAP27) - Rocket launcher', + 'Monster Condo (MAP27) - Super Shotgun', + 'Monster Condo (MAP27) - Supercharge', + 'Monster Condo (MAP27) - Supercharge 2', + 'Monster Condo (MAP27) - Supercharge 3', + 'Monster Condo (MAP27) - Supercharge 4', + 'Monster Condo (MAP27) - Supercharge 5', + 'Monster Condo (MAP27) - Yellow skull key', + }, + 'Nirvana (MAP21)': { + 'Nirvana (MAP21) - Backpack', + 'Nirvana (MAP21) - Blue skull key', + 'Nirvana (MAP21) - Exit', + 'Nirvana (MAP21) - Invulnerability', + 'Nirvana (MAP21) - Megasphere', + 'Nirvana (MAP21) - Red skull key', + 'Nirvana (MAP21) - Rocket launcher', + 'Nirvana (MAP21) - Super Shotgun', + 'Nirvana (MAP21) - Yellow skull key', + }, + 'Refueling Base (MAP10)': { + 'Refueling Base (MAP10) - Armor', + 'Refueling Base (MAP10) - Armor 2', + 'Refueling Base (MAP10) - BFG9000', + 'Refueling Base (MAP10) - Backpack', + 'Refueling Base (MAP10) - Berserk', + 'Refueling Base (MAP10) - Berserk 2', + 'Refueling Base (MAP10) - Blue keycard', + 'Refueling Base (MAP10) - Chaingun', + 'Refueling Base (MAP10) - Chainsaw', + 'Refueling Base (MAP10) - Exit', + 'Refueling Base (MAP10) - Invulnerability', + 'Refueling Base (MAP10) - Invulnerability 2', + 'Refueling Base (MAP10) - Mega Armor', + 'Refueling Base (MAP10) - Megasphere', + 'Refueling Base (MAP10) - Partial invisibility', + 'Refueling Base (MAP10) - Plasma gun', + 'Refueling Base (MAP10) - Rocket launcher', + 'Refueling Base (MAP10) - Shotgun', + 'Refueling Base (MAP10) - Supercharge', + 'Refueling Base (MAP10) - Supercharge 2', + 'Refueling Base (MAP10) - Yellow keycard', + }, + 'Suburbs (MAP16)': { + 'Suburbs (MAP16) - BFG9000', + 'Suburbs (MAP16) - Backpack', + 'Suburbs (MAP16) - Berserk', + 'Suburbs (MAP16) - Blue skull key', + 'Suburbs (MAP16) - Chaingun', + 'Suburbs (MAP16) - Exit', + 'Suburbs (MAP16) - Invulnerability', + 'Suburbs (MAP16) - Megasphere', + 'Suburbs (MAP16) - Partial invisibility', + 'Suburbs (MAP16) - Plasma gun', + 'Suburbs (MAP16) - Plasma gun 2', + 'Suburbs (MAP16) - Plasma gun 3', + 'Suburbs (MAP16) - Plasma gun 4', + 'Suburbs (MAP16) - Red skull key', + 'Suburbs (MAP16) - Rocket launcher', + 'Suburbs (MAP16) - Shotgun', + 'Suburbs (MAP16) - Super Shotgun', + 'Suburbs (MAP16) - Supercharge', + }, + 'Tenements (MAP17)': { + 'Tenements (MAP17) - Armor', + 'Tenements (MAP17) - Armor 2', + 'Tenements (MAP17) - BFG9000', + 'Tenements (MAP17) - Backpack', + 'Tenements (MAP17) - Berserk', + 'Tenements (MAP17) - Blue keycard', + 'Tenements (MAP17) - Chaingun', + 'Tenements (MAP17) - Exit', + 'Tenements (MAP17) - Mega Armor', + 'Tenements (MAP17) - Megasphere', + 'Tenements (MAP17) - Partial invisibility', + 'Tenements (MAP17) - Plasma gun', + 'Tenements (MAP17) - Red keycard', + 'Tenements (MAP17) - Rocket launcher', + 'Tenements (MAP17) - Shotgun', + 'Tenements (MAP17) - Supercharge', + 'Tenements (MAP17) - Supercharge 2', + 'Tenements (MAP17) - Yellow skull key', + }, + 'The Abandoned Mines (MAP26)': { + 'The Abandoned Mines (MAP26) - Armor', + 'The Abandoned Mines (MAP26) - Backpack', + 'The Abandoned Mines (MAP26) - Blue keycard', + 'The Abandoned Mines (MAP26) - Chaingun', + 'The Abandoned Mines (MAP26) - Exit', + 'The Abandoned Mines (MAP26) - Mega Armor', + 'The Abandoned Mines (MAP26) - Partial invisibility', + 'The Abandoned Mines (MAP26) - Plasma gun', + 'The Abandoned Mines (MAP26) - Red keycard', + 'The Abandoned Mines (MAP26) - Rocket launcher', + 'The Abandoned Mines (MAP26) - Super Shotgun', + 'The Abandoned Mines (MAP26) - Supercharge', + 'The Abandoned Mines (MAP26) - Yellow keycard', + }, + 'The Catacombs (MAP22)': { + 'The Catacombs (MAP22) - Armor', + 'The Catacombs (MAP22) - Berserk', + 'The Catacombs (MAP22) - Blue skull key', + 'The Catacombs (MAP22) - Exit', + 'The Catacombs (MAP22) - Plasma gun', + 'The Catacombs (MAP22) - Red skull key', + 'The Catacombs (MAP22) - Rocket launcher', + 'The Catacombs (MAP22) - Shotgun', + 'The Catacombs (MAP22) - Supercharge', + }, + 'The Chasm (MAP24)': { + 'The Chasm (MAP24) - Armor', + 'The Chasm (MAP24) - BFG9000', + 'The Chasm (MAP24) - Backpack', + 'The Chasm (MAP24) - Berserk', + 'The Chasm (MAP24) - Berserk 2', + 'The Chasm (MAP24) - Blue keycard', + 'The Chasm (MAP24) - Exit', + 'The Chasm (MAP24) - Invulnerability', + 'The Chasm (MAP24) - Megasphere', + 'The Chasm (MAP24) - Megasphere 2', + 'The Chasm (MAP24) - Plasma gun', + 'The Chasm (MAP24) - Red keycard', + 'The Chasm (MAP24) - Rocket launcher', + 'The Chasm (MAP24) - Shotgun', + 'The Chasm (MAP24) - Super Shotgun', + }, + 'The Citadel (MAP19)': { + 'The Citadel (MAP19) - Armor', + 'The Citadel (MAP19) - Armor 2', + 'The Citadel (MAP19) - Backpack', + 'The Citadel (MAP19) - Berserk', + 'The Citadel (MAP19) - Blue skull key', + 'The Citadel (MAP19) - Chaingun', + 'The Citadel (MAP19) - Computer area map', + 'The Citadel (MAP19) - Exit', + 'The Citadel (MAP19) - Invulnerability', + 'The Citadel (MAP19) - Mega Armor', + 'The Citadel (MAP19) - Partial invisibility', + 'The Citadel (MAP19) - Red skull key', + 'The Citadel (MAP19) - Rocket launcher', + 'The Citadel (MAP19) - Super Shotgun', + 'The Citadel (MAP19) - Supercharge', + 'The Citadel (MAP19) - Yellow skull key', + }, + 'The Courtyard (MAP18)': { + 'The Courtyard (MAP18) - Armor', + 'The Courtyard (MAP18) - BFG9000', + 'The Courtyard (MAP18) - Backpack', + 'The Courtyard (MAP18) - Berserk', + 'The Courtyard (MAP18) - Blue skull key', + 'The Courtyard (MAP18) - Chaingun', + 'The Courtyard (MAP18) - Computer area map', + 'The Courtyard (MAP18) - Exit', + 'The Courtyard (MAP18) - Invulnerability', + 'The Courtyard (MAP18) - Invulnerability 2', + 'The Courtyard (MAP18) - Partial invisibility', + 'The Courtyard (MAP18) - Partial invisibility 2', + 'The Courtyard (MAP18) - Plasma gun', + 'The Courtyard (MAP18) - Rocket launcher', + 'The Courtyard (MAP18) - Shotgun', + 'The Courtyard (MAP18) - Super Shotgun', + 'The Courtyard (MAP18) - Supercharge', + 'The Courtyard (MAP18) - Yellow skull key', + }, + 'The Crusher (MAP06)': { + 'The Crusher (MAP06) - Armor', + 'The Crusher (MAP06) - Backpack', + 'The Crusher (MAP06) - Blue keycard', + 'The Crusher (MAP06) - Blue keycard 2', + 'The Crusher (MAP06) - Blue keycard 3', + 'The Crusher (MAP06) - Exit', + 'The Crusher (MAP06) - Mega Armor', + 'The Crusher (MAP06) - Megasphere', + 'The Crusher (MAP06) - Megasphere 2', + 'The Crusher (MAP06) - Plasma gun', + 'The Crusher (MAP06) - Red keycard', + 'The Crusher (MAP06) - Rocket launcher', + 'The Crusher (MAP06) - Super Shotgun', + 'The Crusher (MAP06) - Supercharge', + 'The Crusher (MAP06) - Yellow keycard', + }, + 'The Factory (MAP12)': { + 'The Factory (MAP12) - Armor', + 'The Factory (MAP12) - Armor 2', + 'The Factory (MAP12) - BFG9000', + 'The Factory (MAP12) - Backpack', + 'The Factory (MAP12) - Berserk', + 'The Factory (MAP12) - Berserk 2', + 'The Factory (MAP12) - Berserk 3', + 'The Factory (MAP12) - Blue keycard', + 'The Factory (MAP12) - Chaingun', + 'The Factory (MAP12) - Exit', + 'The Factory (MAP12) - Partial invisibility', + 'The Factory (MAP12) - Shotgun', + 'The Factory (MAP12) - Super Shotgun', + 'The Factory (MAP12) - Supercharge', + 'The Factory (MAP12) - Supercharge 2', + 'The Factory (MAP12) - Yellow keycard', + }, + 'The Focus (MAP04)': { + 'The Focus (MAP04) - Blue keycard', + 'The Focus (MAP04) - Exit', + 'The Focus (MAP04) - Red keycard', + 'The Focus (MAP04) - Super Shotgun', + 'The Focus (MAP04) - Yellow keycard', + }, + 'The Gantlet (MAP03)': { + 'The Gantlet (MAP03) - Backpack', + 'The Gantlet (MAP03) - Blue keycard', + 'The Gantlet (MAP03) - Chaingun', + 'The Gantlet (MAP03) - Exit', + 'The Gantlet (MAP03) - Mega Armor', + 'The Gantlet (MAP03) - Mega Armor 2', + 'The Gantlet (MAP03) - Partial invisibility', + 'The Gantlet (MAP03) - Red keycard', + 'The Gantlet (MAP03) - Rocket launcher', + 'The Gantlet (MAP03) - Shotgun', + 'The Gantlet (MAP03) - Supercharge', + }, + 'The Inmost Dens (MAP14)': { + 'The Inmost Dens (MAP14) - Berserk', + 'The Inmost Dens (MAP14) - Blue skull key', + 'The Inmost Dens (MAP14) - Chaingun', + 'The Inmost Dens (MAP14) - Exit', + 'The Inmost Dens (MAP14) - Mega Armor', + 'The Inmost Dens (MAP14) - Partial invisibility', + 'The Inmost Dens (MAP14) - Plasma gun', + 'The Inmost Dens (MAP14) - Red skull key', + 'The Inmost Dens (MAP14) - Rocket launcher', + 'The Inmost Dens (MAP14) - Shotgun', + 'The Inmost Dens (MAP14) - Supercharge', + }, + 'The Living End (MAP29)': { + 'The Living End (MAP29) - Armor', + 'The Living End (MAP29) - Backpack', + 'The Living End (MAP29) - Backpack 2', + 'The Living End (MAP29) - Berserk', + 'The Living End (MAP29) - Berserk 2', + 'The Living End (MAP29) - Chaingun', + 'The Living End (MAP29) - Exit', + 'The Living End (MAP29) - Mega Armor', + 'The Living End (MAP29) - Plasma gun', + 'The Living End (MAP29) - Super Shotgun', + 'The Living End (MAP29) - Supercharge', + }, + 'The Pit (MAP09)': { + 'The Pit (MAP09) - Armor', + 'The Pit (MAP09) - BFG9000', + 'The Pit (MAP09) - Backpack', + 'The Pit (MAP09) - Berserk', + 'The Pit (MAP09) - Berserk 2', + 'The Pit (MAP09) - Berserk 3', + 'The Pit (MAP09) - Blue keycard', + 'The Pit (MAP09) - Computer area map', + 'The Pit (MAP09) - Exit', + 'The Pit (MAP09) - Mega Armor', + 'The Pit (MAP09) - Mega Armor 2', + 'The Pit (MAP09) - Rocket launcher', + 'The Pit (MAP09) - Shotgun', + 'The Pit (MAP09) - Supercharge', + 'The Pit (MAP09) - Supercharge 2', + 'The Pit (MAP09) - Yellow keycard', + }, + 'The Spirit World (MAP28)': { + 'The Spirit World (MAP28) - Armor', + 'The Spirit World (MAP28) - BFG9000', + 'The Spirit World (MAP28) - Backpack', + 'The Spirit World (MAP28) - Backpack 2', + 'The Spirit World (MAP28) - Backpack 3', + 'The Spirit World (MAP28) - Backpack 4', + 'The Spirit World (MAP28) - Berserk', + 'The Spirit World (MAP28) - Chaingun', + 'The Spirit World (MAP28) - Chainsaw', + 'The Spirit World (MAP28) - Exit', + 'The Spirit World (MAP28) - Invulnerability', + 'The Spirit World (MAP28) - Invulnerability 2', + 'The Spirit World (MAP28) - Invulnerability 3', + 'The Spirit World (MAP28) - Invulnerability 4', + 'The Spirit World (MAP28) - Invulnerability 5', + 'The Spirit World (MAP28) - Megasphere', + 'The Spirit World (MAP28) - Megasphere 2', + 'The Spirit World (MAP28) - Plasma gun', + 'The Spirit World (MAP28) - Red skull key', + 'The Spirit World (MAP28) - Rocket launcher', + 'The Spirit World (MAP28) - Super Shotgun', + 'The Spirit World (MAP28) - Supercharge', + 'The Spirit World (MAP28) - Yellow skull key', + }, + 'The Waste Tunnels (MAP05)': { + 'The Waste Tunnels (MAP05) - Armor', + 'The Waste Tunnels (MAP05) - Berserk', + 'The Waste Tunnels (MAP05) - Blue keycard', + 'The Waste Tunnels (MAP05) - Exit', + 'The Waste Tunnels (MAP05) - Mega Armor', + 'The Waste Tunnels (MAP05) - Plasma gun', + 'The Waste Tunnels (MAP05) - Red keycard', + 'The Waste Tunnels (MAP05) - Rocket launcher', + 'The Waste Tunnels (MAP05) - Shotgun', + 'The Waste Tunnels (MAP05) - Super Shotgun', + 'The Waste Tunnels (MAP05) - Supercharge', + 'The Waste Tunnels (MAP05) - Supercharge 2', + 'The Waste Tunnels (MAP05) - Yellow keycard', + }, + 'Tricks and Traps (MAP08)': { + 'Tricks and Traps (MAP08) - Armor', + 'Tricks and Traps (MAP08) - Armor 2', + 'Tricks and Traps (MAP08) - BFG9000', + 'Tricks and Traps (MAP08) - Backpack', + 'Tricks and Traps (MAP08) - Backpack 2', + 'Tricks and Traps (MAP08) - Backpack 3', + 'Tricks and Traps (MAP08) - Backpack 4', + 'Tricks and Traps (MAP08) - Backpack 5', + 'Tricks and Traps (MAP08) - Chaingun', + 'Tricks and Traps (MAP08) - Chainsaw', + 'Tricks and Traps (MAP08) - Exit', + 'Tricks and Traps (MAP08) - Invulnerability', + 'Tricks and Traps (MAP08) - Invulnerability 2', + 'Tricks and Traps (MAP08) - Invulnerability 3', + 'Tricks and Traps (MAP08) - Invulnerability 4', + 'Tricks and Traps (MAP08) - Invulnerability 5', + 'Tricks and Traps (MAP08) - Partial invisibility', + 'Tricks and Traps (MAP08) - Plasma gun', + 'Tricks and Traps (MAP08) - Red skull key', + 'Tricks and Traps (MAP08) - Rocket launcher', + 'Tricks and Traps (MAP08) - Shotgun', + 'Tricks and Traps (MAP08) - Supercharge', + 'Tricks and Traps (MAP08) - Supercharge 2', + 'Tricks and Traps (MAP08) - Yellow skull key', + }, + 'Underhalls (MAP02)': { + 'Underhalls (MAP02) - Blue keycard', + 'Underhalls (MAP02) - Exit', + 'Underhalls (MAP02) - Mega Armor', + 'Underhalls (MAP02) - Red keycard', + 'Underhalls (MAP02) - Super Shotgun', + }, + 'Wolfenstein2 (MAP31)': { + 'Wolfenstein2 (MAP31) - BFG9000', + 'Wolfenstein2 (MAP31) - Backpack', + 'Wolfenstein2 (MAP31) - Backpack 2', + 'Wolfenstein2 (MAP31) - Backpack 3', + 'Wolfenstein2 (MAP31) - Backpack 4', + 'Wolfenstein2 (MAP31) - Berserk', + 'Wolfenstein2 (MAP31) - Chaingun', + 'Wolfenstein2 (MAP31) - Exit', + 'Wolfenstein2 (MAP31) - Megasphere', + 'Wolfenstein2 (MAP31) - Partial invisibility', + 'Wolfenstein2 (MAP31) - Plasma gun', + 'Wolfenstein2 (MAP31) - Rocket launcher', + 'Wolfenstein2 (MAP31) - Shotgun', + 'Wolfenstein2 (MAP31) - Super Shotgun', + 'Wolfenstein2 (MAP31) - Supercharge', + }, +} + + +death_logic_locations = [ + "Entryway (MAP01) - Armor", +] diff --git a/worlds/doom_ii/Maps.py b/worlds/doom_ii/Maps.py new file mode 100644 index 000000000000..cf41939fa513 --- /dev/null +++ b/worlds/doom_ii/Maps.py @@ -0,0 +1,39 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import List + + +map_names: List[str] = [ + 'Entryway (MAP01)', + 'Underhalls (MAP02)', + 'The Gantlet (MAP03)', + 'The Focus (MAP04)', + 'The Waste Tunnels (MAP05)', + 'The Crusher (MAP06)', + 'Dead Simple (MAP07)', + 'Tricks and Traps (MAP08)', + 'The Pit (MAP09)', + 'Refueling Base (MAP10)', + 'Circle of Death (MAP11)', + 'The Factory (MAP12)', + 'Downtown (MAP13)', + 'The Inmost Dens (MAP14)', + 'Industrial Zone (MAP15)', + 'Suburbs (MAP16)', + 'Tenements (MAP17)', + 'The Courtyard (MAP18)', + 'The Citadel (MAP19)', + 'Gotcha! (MAP20)', + 'Nirvana (MAP21)', + 'The Catacombs (MAP22)', + 'Barrels o Fun (MAP23)', + 'The Chasm (MAP24)', + 'Bloodfalls (MAP25)', + 'The Abandoned Mines (MAP26)', + 'Monster Condo (MAP27)', + 'The Spirit World (MAP28)', + 'The Living End (MAP29)', + 'Icon of Sin (MAP30)', + 'Wolfenstein2 (MAP31)', + 'Grosse2 (MAP32)', +] diff --git a/worlds/doom_ii/Options.py b/worlds/doom_ii/Options.py new file mode 100644 index 000000000000..cc39512a176e --- /dev/null +++ b/worlds/doom_ii/Options.py @@ -0,0 +1,150 @@ +import typing + +from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool +from dataclasses import dataclass + + +class Difficulty(Choice): + """ + Choose the difficulty option. Those match DOOM's difficulty options. + baby (I'm too young to die.) double ammos, half damage, less monsters or strength. + easy (Hey, not too rough.) less monsters or strength. + medium (Hurt me plenty.) Default. + hard (Ultra-Violence.) More monsters or strength. + nightmare (Nightmare!) Monsters attack more rapidly and respawn. + """ + display_name = "Difficulty" + option_baby = 0 + option_easy = 1 + option_medium = 2 + option_hard = 3 + option_nightmare = 4 + default = 2 + + +class RandomMonsters(Choice): + """ + Choose how monsters are randomized. + vanilla: No randomization + shuffle: Monsters are shuffled within the level + random_balanced: Monsters are completely randomized, but balanced based on existing ratio in the level. (Small monsters vs medium vs big) + random_chaotic: Monsters are completely randomized, but balanced based on existing ratio in the entire game. + """ + display_name = "Random Monsters" + option_vanilla = 0 + option_shuffle = 1 + option_random_balanced = 2 + option_random_chaotic = 3 + default = 2 + + +class RandomPickups(Choice): + """ + Choose how pickups are randomized. + vanilla: No randomization + shuffle: Pickups are shuffled within the level + random_balanced: Pickups are completely randomized, but balanced based on existing ratio in the level. (Small pickups vs Big) + """ + display_name = "Random Pickups" + option_vanilla = 0 + option_shuffle = 1 + option_random_balanced = 2 + default = 1 + + +class RandomMusic(Choice): + """ + Level musics will be randomized. + vanilla: No randomization + shuffle_selected: Selected episodes' levels will be shuffled + shuffle_game: All the music will be shuffled + """ + display_name = "Random Music" + option_vanilla = 0 + option_shuffle_selected = 1 + option_shuffle_game = 2 + default = 0 + + +class FlipLevels(Choice): + """ + Flip levels on one axis. + vanilla: No flipping + flipped: All levels are flipped + random: Random levels are flipped + """ + display_name = "Flip Levels" + option_vanilla = 0 + option_flipped = 1 + option_randomly_flipped = 2 + default = 0 + + +class AllowDeathLogic(Toggle): + """Some locations require a timed puzzle that can only be tried once. + After which, if the player failed to get it, the location cannot be checked anymore. + By default, no progression items are placed here. There is a way, hovewer, to still get them: + Get killed in the current map. The map will reset, you can now attempt the puzzle again.""" + display_name = "Allow Death Logic" + + +class Pro(Toggle): + """Include difficult tricks into rules. Mostly employed by speed runners. + i.e.: Leaps across to a locked area, trigger a switch behind a window at the right angle, etc.""" + display_name = "Pro Doom" + + +class StartWithComputerAreaMaps(Toggle): + """Give the player all Computer Area Map items from the start.""" + display_name = "Start With Computer Area Maps" + + +class ResetLevelOnDeath(DefaultOnToggle): + """When dying, levels are reset and monsters respawned. But inventory and checks are kept. + Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" + display_message="Reset level on death" + + +class Episode1(DefaultOnToggle): + """Subterranean and Outpost. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 1" + + +class Episode2(DefaultOnToggle): + """City. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 2" + + +class Episode3(DefaultOnToggle): + """Hell. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 3" + + +class SecretLevels(Toggle): + """Secret levels. + This is too short to be an episode. It's additive. + Another episode will have to be selected along with this one. + Otherwise episode 1 will be added.""" + display_name = "Secret Levels" + + +@dataclass +class DOOM2Options(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool + difficulty: Difficulty + random_monsters: RandomMonsters + random_pickups: RandomPickups + random_music: RandomMusic + flip_levels: FlipLevels + allow_death_logic: AllowDeathLogic + pro: Pro + start_with_computer_area_maps: StartWithComputerAreaMaps + death_link: DeathLink + reset_level_on_death: ResetLevelOnDeath + episode1: Episode1 + episode2: Episode2 + episode3: Episode3 + episode4: SecretLevels diff --git a/worlds/doom_ii/Regions.py b/worlds/doom_ii/Regions.py new file mode 100644 index 000000000000..3d81d7abb84e --- /dev/null +++ b/worlds/doom_ii/Regions.py @@ -0,0 +1,502 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import List +from BaseClasses import TypedDict + +class ConnectionDict(TypedDict, total=False): + target: str + pro: bool + +class RegionDict(TypedDict, total=False): + name: str + connects_to_hub: bool + episode: int + connections: List[ConnectionDict] + + +regions:List[RegionDict] = [ + # Entryway (MAP01) + {"name":"Entryway (MAP01) Main", + "connects_to_hub":True, + "episode":1, + "connections":[]}, + + # Underhalls (MAP02) + {"name":"Underhalls (MAP02) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"Underhalls (MAP02) Red","pro":False}]}, + {"name":"Underhalls (MAP02) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"Underhalls (MAP02) Red","pro":False}]}, + {"name":"Underhalls (MAP02) Red", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"Underhalls (MAP02) Blue","pro":False}, + {"target":"Underhalls (MAP02) Main","pro":False}]}, + + # The Gantlet (MAP03) + {"name":"The Gantlet (MAP03) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Gantlet (MAP03) Blue","pro":False}, + {"target":"The Gantlet (MAP03) Blue Pro Jump","pro":True}]}, + {"name":"The Gantlet (MAP03) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Gantlet (MAP03) Main","pro":False}, + {"target":"The Gantlet (MAP03) Red","pro":False}, + {"target":"The Gantlet (MAP03) Blue Pro Jump","pro":False}]}, + {"name":"The Gantlet (MAP03) Red", + "connects_to_hub":False, + "episode":1, + "connections":[]}, + {"name":"The Gantlet (MAP03) Blue Pro Jump", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Gantlet (MAP03) Blue","pro":False}]}, + + # The Focus (MAP04) + {"name":"The Focus (MAP04) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Focus (MAP04) Red","pro":False}, + {"target":"The Focus (MAP04) Blue","pro":False}]}, + {"name":"The Focus (MAP04) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Focus (MAP04) Main","pro":False}]}, + {"name":"The Focus (MAP04) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Focus (MAP04) Red","pro":False}]}, + {"name":"The Focus (MAP04) Red", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Focus (MAP04) Yellow","pro":False}, + {"target":"The Focus (MAP04) Main","pro":False}]}, + + # The Waste Tunnels (MAP05) + {"name":"The Waste Tunnels (MAP05) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Waste Tunnels (MAP05) Red","pro":False}, + {"target":"The Waste Tunnels (MAP05) Blue","pro":False}]}, + {"name":"The Waste Tunnels (MAP05) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Waste Tunnels (MAP05) Yellow","pro":False}, + {"target":"The Waste Tunnels (MAP05) Main","pro":False}]}, + {"name":"The Waste Tunnels (MAP05) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Waste Tunnels (MAP05) Blue","pro":False}]}, + {"name":"The Waste Tunnels (MAP05) Red", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Waste Tunnels (MAP05) Main","pro":False}]}, + + # The Crusher (MAP06) + {"name":"The Crusher (MAP06) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"The Crusher (MAP06) Blue","pro":False}]}, + {"name":"The Crusher (MAP06) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Crusher (MAP06) Red","pro":False}, + {"target":"The Crusher (MAP06) Main","pro":False}]}, + {"name":"The Crusher (MAP06) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Crusher (MAP06) Red","pro":False}]}, + {"name":"The Crusher (MAP06) Red", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Crusher (MAP06) Yellow","pro":False}, + {"target":"The Crusher (MAP06) Blue","pro":False}, + {"target":"The Crusher (MAP06) Main","pro":False}]}, + + # Dead Simple (MAP07) + {"name":"Dead Simple (MAP07) Main", + "connects_to_hub":True, + "episode":1, + "connections":[]}, + + # Tricks and Traps (MAP08) + {"name":"Tricks and Traps (MAP08) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"Tricks and Traps (MAP08) Red","pro":False}, + {"target":"Tricks and Traps (MAP08) Yellow","pro":False}]}, + {"name":"Tricks and Traps (MAP08) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"Tricks and Traps (MAP08) Main","pro":False}]}, + {"name":"Tricks and Traps (MAP08) Red", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"Tricks and Traps (MAP08) Main","pro":False}]}, + + # The Pit (MAP09) + {"name":"The Pit (MAP09) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Pit (MAP09) Yellow","pro":False}, + {"target":"The Pit (MAP09) Blue","pro":False}]}, + {"name":"The Pit (MAP09) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[]}, + {"name":"The Pit (MAP09) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Pit (MAP09) Main","pro":False}]}, + + # Refueling Base (MAP10) + {"name":"Refueling Base (MAP10) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]}, + {"name":"Refueling Base (MAP10) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"Refueling Base (MAP10) Main","pro":False}, + {"target":"Refueling Base (MAP10) Yellow Blue","pro":False}]}, + {"name":"Refueling Base (MAP10) Yellow Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]}, + + # Circle of Death (MAP11) + {"name":"Circle of Death (MAP11) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"Circle of Death (MAP11) Blue","pro":False}, + {"target":"Circle of Death (MAP11) Red","pro":False}]}, + {"name":"Circle of Death (MAP11) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]}, + {"name":"Circle of Death (MAP11) Red", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]}, + + # The Factory (MAP12) + {"name":"The Factory (MAP12) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The Factory (MAP12) Yellow","pro":False}, + {"target":"The Factory (MAP12) Blue","pro":False}]}, + {"name":"The Factory (MAP12) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Factory (MAP12) Main","pro":False}]}, + {"name":"The Factory (MAP12) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[]}, + + # Downtown (MAP13) + {"name":"Downtown (MAP13) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"Downtown (MAP13) Yellow","pro":False}, + {"target":"Downtown (MAP13) Red","pro":False}, + {"target":"Downtown (MAP13) Blue","pro":False}]}, + {"name":"Downtown (MAP13) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Downtown (MAP13) Main","pro":False}]}, + {"name":"Downtown (MAP13) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Downtown (MAP13) Main","pro":False}]}, + {"name":"Downtown (MAP13) Red", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Downtown (MAP13) Main","pro":False}]}, + + # The Inmost Dens (MAP14) + {"name":"The Inmost Dens (MAP14) Main", + "connects_to_hub":True, + "episode":2, + "connections":[{"target":"The Inmost Dens (MAP14) Red","pro":False}]}, + {"name":"The Inmost Dens (MAP14) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Inmost Dens (MAP14) Main","pro":False}, + {"target":"The Inmost Dens (MAP14) Red East","pro":False}]}, + {"name":"The Inmost Dens (MAP14) Red", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Inmost Dens (MAP14) Main","pro":False}, + {"target":"The Inmost Dens (MAP14) Red South","pro":False}, + {"target":"The Inmost Dens (MAP14) Red East","pro":False}]}, + {"name":"The Inmost Dens (MAP14) Red East", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Inmost Dens (MAP14) Blue","pro":False}, + {"target":"The Inmost Dens (MAP14) Main","pro":False}]}, + {"name":"The Inmost Dens (MAP14) Red South", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Inmost Dens (MAP14) Main","pro":False}]}, + + # Industrial Zone (MAP15) + {"name":"Industrial Zone (MAP15) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"Industrial Zone (MAP15) Yellow East","pro":False}, + {"target":"Industrial Zone (MAP15) Yellow West","pro":False}]}, + {"name":"Industrial Zone (MAP15) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Industrial Zone (MAP15) Yellow East","pro":False}]}, + {"name":"Industrial Zone (MAP15) Yellow East", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"Industrial Zone (MAP15) Blue","pro":False}, + {"target":"Industrial Zone (MAP15) Main","pro":False}]}, + {"name":"Industrial Zone (MAP15) Yellow West", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Industrial Zone (MAP15) Main","pro":False}]}, + + # Suburbs (MAP16) + {"name":"Suburbs (MAP16) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"Suburbs (MAP16) Red","pro":False}, + {"target":"Suburbs (MAP16) Blue","pro":False}]}, + {"name":"Suburbs (MAP16) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Suburbs (MAP16) Main","pro":False}]}, + {"name":"Suburbs (MAP16) Red", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Suburbs (MAP16) Main","pro":False}]}, + + # Tenements (MAP17) + {"name":"Tenements (MAP17) Main", + "connects_to_hub":True, + "episode":2, + "connections":[{"target":"Tenements (MAP17) Red","pro":False}]}, + {"name":"Tenements (MAP17) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Tenements (MAP17) Red","pro":False}]}, + {"name":"Tenements (MAP17) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"Tenements (MAP17) Red","pro":False}, + {"target":"Tenements (MAP17) Blue","pro":False}]}, + {"name":"Tenements (MAP17) Red", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"Tenements (MAP17) Yellow","pro":False}, + {"target":"Tenements (MAP17) Blue","pro":False}, + {"target":"Tenements (MAP17) Main","pro":False}]}, + + # The Courtyard (MAP18) + {"name":"The Courtyard (MAP18) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The Courtyard (MAP18) Yellow","pro":False}, + {"target":"The Courtyard (MAP18) Blue","pro":False}]}, + {"name":"The Courtyard (MAP18) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Courtyard (MAP18) Main","pro":False}]}, + {"name":"The Courtyard (MAP18) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Courtyard (MAP18) Main","pro":False}]}, + + # The Citadel (MAP19) + {"name":"The Citadel (MAP19) Main", + "connects_to_hub":True, + "episode":2, + "connections":[{"target":"The Citadel (MAP19) Red","pro":False}]}, + {"name":"The Citadel (MAP19) Red", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Citadel (MAP19) Main","pro":False}]}, + + # Gotcha! (MAP20) + {"name":"Gotcha! (MAP20) Main", + "connects_to_hub":True, + "episode":2, + "connections":[]}, + + # Nirvana (MAP21) + {"name":"Nirvana (MAP21) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]}, + {"name":"Nirvana (MAP21) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"Nirvana (MAP21) Main","pro":False}, + {"target":"Nirvana (MAP21) Magenta","pro":False}]}, + {"name":"Nirvana (MAP21) Magenta", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]}, + + # The Catacombs (MAP22) + {"name":"The Catacombs (MAP22) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Catacombs (MAP22) Blue","pro":False}, + {"target":"The Catacombs (MAP22) Red","pro":False}]}, + {"name":"The Catacombs (MAP22) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]}, + {"name":"The Catacombs (MAP22) Red", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]}, + + # Barrels o Fun (MAP23) + {"name":"Barrels o Fun (MAP23) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"Barrels o Fun (MAP23) Yellow","pro":False}]}, + {"name":"Barrels o Fun (MAP23) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"Barrels o Fun (MAP23) Main","pro":False}]}, + + # The Chasm (MAP24) + {"name":"The Chasm (MAP24) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"The Chasm (MAP24) Red","pro":False}]}, + {"name":"The Chasm (MAP24) Red", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Chasm (MAP24) Main","pro":False}]}, + + # Bloodfalls (MAP25) + {"name":"Bloodfalls (MAP25) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"Bloodfalls (MAP25) Blue","pro":False}]}, + {"name":"Bloodfalls (MAP25) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"Bloodfalls (MAP25) Main","pro":False}]}, + + # The Abandoned Mines (MAP26) + {"name":"The Abandoned Mines (MAP26) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Abandoned Mines (MAP26) Yellow","pro":False}, + {"target":"The Abandoned Mines (MAP26) Red","pro":False}, + {"target":"The Abandoned Mines (MAP26) Blue","pro":False}]}, + {"name":"The Abandoned Mines (MAP26) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Abandoned Mines (MAP26) Main","pro":False}]}, + {"name":"The Abandoned Mines (MAP26) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Abandoned Mines (MAP26) Main","pro":False}]}, + {"name":"The Abandoned Mines (MAP26) Red", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Abandoned Mines (MAP26) Main","pro":False}]}, + + # Monster Condo (MAP27) + {"name":"Monster Condo (MAP27) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"Monster Condo (MAP27) Yellow","pro":False}, + {"target":"Monster Condo (MAP27) Red","pro":False}, + {"target":"Monster Condo (MAP27) Blue","pro":False}]}, + {"name":"Monster Condo (MAP27) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"Monster Condo (MAP27) Main","pro":False}]}, + {"name":"Monster Condo (MAP27) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"Monster Condo (MAP27) Main","pro":False}]}, + {"name":"Monster Condo (MAP27) Red", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"Monster Condo (MAP27) Main","pro":False}]}, + + # The Spirit World (MAP28) + {"name":"The Spirit World (MAP28) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Spirit World (MAP28) Yellow","pro":False}, + {"target":"The Spirit World (MAP28) Red","pro":False}]}, + {"name":"The Spirit World (MAP28) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Spirit World (MAP28) Main","pro":False}]}, + {"name":"The Spirit World (MAP28) Red", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Spirit World (MAP28) Main","pro":False}]}, + + # The Living End (MAP29) + {"name":"The Living End (MAP29) Main", + "connects_to_hub":True, + "episode":3, + "connections":[]}, + + # Icon of Sin (MAP30) + {"name":"Icon of Sin (MAP30) Main", + "connects_to_hub":True, + "episode":3, + "connections":[]}, + + # Wolfenstein2 (MAP31) + {"name":"Wolfenstein2 (MAP31) Main", + "connects_to_hub":True, + "episode":4, + "connections":[]}, + + # Grosse2 (MAP32) + {"name":"Grosse2 (MAP32) Main", + "connects_to_hub":True, + "episode":4, + "connections":[]}, +] diff --git a/worlds/doom_ii/Rules.py b/worlds/doom_ii/Rules.py new file mode 100644 index 000000000000..89f3a10f9faf --- /dev/null +++ b/worlds/doom_ii/Rules.py @@ -0,0 +1,501 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import TYPE_CHECKING +from worlds.generic.Rules import set_rule + +if TYPE_CHECKING: + from . import DOOM2World + + +def set_episode1_rules(player, world, pro): + # Entryway (MAP01) + set_rule(world.get_entrance("Hub -> Entryway (MAP01) Main", player), lambda state: + state.has("Entryway (MAP01)", player, 1)) + set_rule(world.get_entrance("Hub -> Entryway (MAP01) Main", player), lambda state: + state.has("Entryway (MAP01)", player, 1)) + + # Underhalls (MAP02) + set_rule(world.get_entrance("Hub -> Underhalls (MAP02) Main", player), lambda state: + state.has("Underhalls (MAP02)", player, 1)) + set_rule(world.get_entrance("Hub -> Underhalls (MAP02) Main", player), lambda state: + state.has("Underhalls (MAP02)", player, 1)) + set_rule(world.get_entrance("Hub -> Underhalls (MAP02) Main", player), lambda state: + state.has("Underhalls (MAP02)", player, 1)) + set_rule(world.get_entrance("Underhalls (MAP02) Main -> Underhalls (MAP02) Red", player), lambda state: + state.has("Underhalls (MAP02) - Red keycard", player, 1)) + set_rule(world.get_entrance("Underhalls (MAP02) Blue -> Underhalls (MAP02) Red", player), lambda state: + state.has("Underhalls (MAP02) - Blue keycard", player, 1)) + set_rule(world.get_entrance("Underhalls (MAP02) Red -> Underhalls (MAP02) Blue", player), lambda state: + state.has("Underhalls (MAP02) - Blue keycard", player, 1)) + + # The Gantlet (MAP03) + set_rule(world.get_entrance("Hub -> The Gantlet (MAP03) Main", player), lambda state: + (state.has("The Gantlet (MAP03)", player, 1)) and + (state.has("Shotgun", player, 1) or + state.has("Chaingun", player, 1) or + state.has("Super Shotgun", player, 1))) + set_rule(world.get_entrance("The Gantlet (MAP03) Main -> The Gantlet (MAP03) Blue", player), lambda state: + state.has("The Gantlet (MAP03) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Gantlet (MAP03) Blue -> The Gantlet (MAP03) Red", player), lambda state: + state.has("The Gantlet (MAP03) - Red keycard", player, 1)) + + # The Focus (MAP04) + set_rule(world.get_entrance("Hub -> The Focus (MAP04) Main", player), lambda state: + (state.has("The Focus (MAP04)", player, 1)) and + (state.has("Shotgun", player, 1) or + state.has("Chaingun", player, 1) or + state.has("Super Shotgun", player, 1))) + set_rule(world.get_entrance("The Focus (MAP04) Main -> The Focus (MAP04) Red", player), lambda state: + state.has("The Focus (MAP04) - Red keycard", player, 1)) + set_rule(world.get_entrance("The Focus (MAP04) Main -> The Focus (MAP04) Blue", player), lambda state: + state.has("The Focus (MAP04) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Focus (MAP04) Yellow -> The Focus (MAP04) Red", player), lambda state: + state.has("The Focus (MAP04) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Focus (MAP04) Red -> The Focus (MAP04) Yellow", player), lambda state: + state.has("The Focus (MAP04) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Focus (MAP04) Red -> The Focus (MAP04) Main", player), lambda state: + state.has("The Focus (MAP04) - Red keycard", player, 1)) + + # The Waste Tunnels (MAP05) + set_rule(world.get_entrance("Hub -> The Waste Tunnels (MAP05) Main", player), lambda state: + (state.has("The Waste Tunnels (MAP05)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Red", player), lambda state: + state.has("The Waste Tunnels (MAP05) - Red keycard", player, 1)) + set_rule(world.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Blue", player), lambda state: + state.has("The Waste Tunnels (MAP05) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Waste Tunnels (MAP05) Blue -> The Waste Tunnels (MAP05) Yellow", player), lambda state: + state.has("The Waste Tunnels (MAP05) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Waste Tunnels (MAP05) Blue -> The Waste Tunnels (MAP05) Main", player), lambda state: + state.has("The Waste Tunnels (MAP05) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Waste Tunnels (MAP05) Yellow -> The Waste Tunnels (MAP05) Blue", player), lambda state: + state.has("The Waste Tunnels (MAP05) - Yellow keycard", player, 1)) + + # The Crusher (MAP06) + set_rule(world.get_entrance("Hub -> The Crusher (MAP06) Main", player), lambda state: + (state.has("The Crusher (MAP06)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Crusher (MAP06) Main -> The Crusher (MAP06) Blue", player), lambda state: + state.has("The Crusher (MAP06) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Red", player), lambda state: + state.has("The Crusher (MAP06) - Red keycard", player, 1)) + set_rule(world.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Main", player), lambda state: + state.has("The Crusher (MAP06) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Crusher (MAP06) Yellow -> The Crusher (MAP06) Red", player), lambda state: + state.has("The Crusher (MAP06) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Crusher (MAP06) Red -> The Crusher (MAP06) Yellow", player), lambda state: + state.has("The Crusher (MAP06) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Crusher (MAP06) Red -> The Crusher (MAP06) Blue", player), lambda state: + state.has("The Crusher (MAP06) - Red keycard", player, 1)) + + # Dead Simple (MAP07) + set_rule(world.get_entrance("Hub -> Dead Simple (MAP07) Main", player), lambda state: + (state.has("Dead Simple (MAP07)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + + # Tricks and Traps (MAP08) + set_rule(world.get_entrance("Hub -> Tricks and Traps (MAP08) Main", player), lambda state: + (state.has("Tricks and Traps (MAP08)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Tricks and Traps (MAP08) Main -> Tricks and Traps (MAP08) Red", player), lambda state: + state.has("Tricks and Traps (MAP08) - Red skull key", player, 1)) + set_rule(world.get_entrance("Tricks and Traps (MAP08) Main -> Tricks and Traps (MAP08) Yellow", player), lambda state: + state.has("Tricks and Traps (MAP08) - Yellow skull key", player, 1)) + + # The Pit (MAP09) + set_rule(world.get_entrance("Hub -> The Pit (MAP09) Main", player), lambda state: + (state.has("The Pit (MAP09)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Yellow", player), lambda state: + state.has("The Pit (MAP09) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Blue", player), lambda state: + state.has("The Pit (MAP09) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Pit (MAP09) Yellow -> The Pit (MAP09) Main", player), lambda state: + state.has("The Pit (MAP09) - Yellow keycard", player, 1)) + + # Refueling Base (MAP10) + set_rule(world.get_entrance("Hub -> Refueling Base (MAP10) Main", player), lambda state: + (state.has("Refueling Base (MAP10)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Refueling Base (MAP10) Main -> Refueling Base (MAP10) Yellow", player), lambda state: + state.has("Refueling Base (MAP10) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("Refueling Base (MAP10) Yellow -> Refueling Base (MAP10) Yellow Blue", player), lambda state: + state.has("Refueling Base (MAP10) - Blue keycard", player, 1)) + + # Circle of Death (MAP11) + set_rule(world.get_entrance("Hub -> Circle of Death (MAP11) Main", player), lambda state: + (state.has("Circle of Death (MAP11)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Circle of Death (MAP11) Main -> Circle of Death (MAP11) Blue", player), lambda state: + state.has("Circle of Death (MAP11) - Blue keycard", player, 1)) + set_rule(world.get_entrance("Circle of Death (MAP11) Main -> Circle of Death (MAP11) Red", player), lambda state: + state.has("Circle of Death (MAP11) - Red keycard", player, 1)) + + +def set_episode2_rules(player, world, pro): + # The Factory (MAP12) + set_rule(world.get_entrance("Hub -> The Factory (MAP12) Main", player), lambda state: + (state.has("The Factory (MAP12)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Yellow", player), lambda state: + state.has("The Factory (MAP12) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Blue", player), lambda state: + state.has("The Factory (MAP12) - Blue keycard", player, 1)) + + # Downtown (MAP13) + set_rule(world.get_entrance("Hub -> Downtown (MAP13) Main", player), lambda state: + (state.has("Downtown (MAP13)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Downtown (MAP13) Main -> Downtown (MAP13) Yellow", player), lambda state: + state.has("Downtown (MAP13) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("Downtown (MAP13) Main -> Downtown (MAP13) Red", player), lambda state: + state.has("Downtown (MAP13) - Red keycard", player, 1)) + set_rule(world.get_entrance("Downtown (MAP13) Main -> Downtown (MAP13) Blue", player), lambda state: + state.has("Downtown (MAP13) - Blue keycard", player, 1)) + + # The Inmost Dens (MAP14) + set_rule(world.get_entrance("Hub -> The Inmost Dens (MAP14) Main", player), lambda state: + (state.has("The Inmost Dens (MAP14)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Inmost Dens (MAP14) Main -> The Inmost Dens (MAP14) Red", player), lambda state: + state.has("The Inmost Dens (MAP14) - Red skull key", player, 1)) + set_rule(world.get_entrance("The Inmost Dens (MAP14) Blue -> The Inmost Dens (MAP14) Red East", player), lambda state: + state.has("The Inmost Dens (MAP14) - Blue skull key", player, 1)) + set_rule(world.get_entrance("The Inmost Dens (MAP14) Red -> The Inmost Dens (MAP14) Main", player), lambda state: + state.has("The Inmost Dens (MAP14) - Red skull key", player, 1)) + set_rule(world.get_entrance("The Inmost Dens (MAP14) Red East -> The Inmost Dens (MAP14) Blue", player), lambda state: + state.has("The Inmost Dens (MAP14) - Blue skull key", player, 1)) + + # Industrial Zone (MAP15) + set_rule(world.get_entrance("Hub -> Industrial Zone (MAP15) Main", player), lambda state: + (state.has("Industrial Zone (MAP15)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Industrial Zone (MAP15) Main -> Industrial Zone (MAP15) Yellow East", player), lambda state: + state.has("Industrial Zone (MAP15) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("Industrial Zone (MAP15) Main -> Industrial Zone (MAP15) Yellow West", player), lambda state: + state.has("Industrial Zone (MAP15) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("Industrial Zone (MAP15) Blue -> Industrial Zone (MAP15) Yellow East", player), lambda state: + state.has("Industrial Zone (MAP15) - Blue keycard", player, 1)) + set_rule(world.get_entrance("Industrial Zone (MAP15) Yellow East -> Industrial Zone (MAP15) Blue", player), lambda state: + state.has("Industrial Zone (MAP15) - Blue keycard", player, 1)) + + # Suburbs (MAP16) + set_rule(world.get_entrance("Hub -> Suburbs (MAP16) Main", player), lambda state: + (state.has("Suburbs (MAP16)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Suburbs (MAP16) Main -> Suburbs (MAP16) Red", player), lambda state: + state.has("Suburbs (MAP16) - Red skull key", player, 1)) + set_rule(world.get_entrance("Suburbs (MAP16) Main -> Suburbs (MAP16) Blue", player), lambda state: + state.has("Suburbs (MAP16) - Blue skull key", player, 1)) + + # Tenements (MAP17) + set_rule(world.get_entrance("Hub -> Tenements (MAP17) Main", player), lambda state: + (state.has("Tenements (MAP17)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Tenements (MAP17) Main -> Tenements (MAP17) Red", player), lambda state: + state.has("Tenements (MAP17) - Red keycard", player, 1)) + set_rule(world.get_entrance("Tenements (MAP17) Red -> Tenements (MAP17) Yellow", player), lambda state: + state.has("Tenements (MAP17) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("Tenements (MAP17) Red -> Tenements (MAP17) Blue", player), lambda state: + state.has("Tenements (MAP17) - Blue keycard", player, 1)) + + # The Courtyard (MAP18) + set_rule(world.get_entrance("Hub -> The Courtyard (MAP18) Main", player), lambda state: + (state.has("The Courtyard (MAP18)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Courtyard (MAP18) Main -> The Courtyard (MAP18) Yellow", player), lambda state: + state.has("The Courtyard (MAP18) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("The Courtyard (MAP18) Main -> The Courtyard (MAP18) Blue", player), lambda state: + state.has("The Courtyard (MAP18) - Blue skull key", player, 1)) + set_rule(world.get_entrance("The Courtyard (MAP18) Blue -> The Courtyard (MAP18) Main", player), lambda state: + state.has("The Courtyard (MAP18) - Blue skull key", player, 1)) + set_rule(world.get_entrance("The Courtyard (MAP18) Yellow -> The Courtyard (MAP18) Main", player), lambda state: + state.has("The Courtyard (MAP18) - Yellow skull key", player, 1)) + + # The Citadel (MAP19) + set_rule(world.get_entrance("Hub -> The Citadel (MAP19) Main", player), lambda state: + (state.has("The Citadel (MAP19)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Citadel (MAP19) Main -> The Citadel (MAP19) Red", player), lambda state: + (state.has("The Citadel (MAP19) - Red skull key", player, 1)) and (state.has("The Citadel (MAP19) - Blue skull key", player, 1) or + state.has("The Citadel (MAP19) - Yellow skull key", player, 1))) + set_rule(world.get_entrance("The Citadel (MAP19) Red -> The Citadel (MAP19) Main", player), lambda state: + (state.has("The Citadel (MAP19) - Red skull key", player, 1)) and (state.has("The Citadel (MAP19) - Yellow skull key", player, 1) or + state.has("The Citadel (MAP19) - Blue skull key", player, 1))) + + # Gotcha! (MAP20) + set_rule(world.get_entrance("Hub -> Gotcha! (MAP20) Main", player), lambda state: + (state.has("Gotcha! (MAP20)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + + +def set_episode3_rules(player, world, pro): + # Nirvana (MAP21) + set_rule(world.get_entrance("Hub -> Nirvana (MAP21) Main", player), lambda state: + (state.has("Nirvana (MAP21)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Nirvana (MAP21) Main -> Nirvana (MAP21) Yellow", player), lambda state: + state.has("Nirvana (MAP21) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Main", player), lambda state: + state.has("Nirvana (MAP21) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Magenta", player), lambda state: + state.has("Nirvana (MAP21) - Red skull key", player, 1) and + state.has("Nirvana (MAP21) - Blue skull key", player, 1)) + set_rule(world.get_entrance("Nirvana (MAP21) Magenta -> Nirvana (MAP21) Yellow", player), lambda state: + state.has("Nirvana (MAP21) - Red skull key", player, 1) and + state.has("Nirvana (MAP21) - Blue skull key", player, 1)) + + # The Catacombs (MAP22) + set_rule(world.get_entrance("Hub -> The Catacombs (MAP22) Main", player), lambda state: + (state.has("The Catacombs (MAP22)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("BFG9000", player, 1) or + state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1))) + set_rule(world.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Blue", player), lambda state: + state.has("The Catacombs (MAP22) - Blue skull key", player, 1)) + set_rule(world.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Red", player), lambda state: + state.has("The Catacombs (MAP22) - Red skull key", player, 1)) + set_rule(world.get_entrance("The Catacombs (MAP22) Red -> The Catacombs (MAP22) Main", player), lambda state: + state.has("The Catacombs (MAP22) - Red skull key", player, 1)) + + # Barrels o Fun (MAP23) + set_rule(world.get_entrance("Hub -> Barrels o Fun (MAP23) Main", player), lambda state: + (state.has("Barrels o Fun (MAP23)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Barrels o Fun (MAP23) Main -> Barrels o Fun (MAP23) Yellow", player), lambda state: + state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("Barrels o Fun (MAP23) Yellow -> Barrels o Fun (MAP23) Main", player), lambda state: + state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1)) + + # The Chasm (MAP24) + set_rule(world.get_entrance("Hub -> The Chasm (MAP24) Main", player), lambda state: + state.has("The Chasm (MAP24)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Super Shotgun", player, 1)) + set_rule(world.get_entrance("The Chasm (MAP24) Main -> The Chasm (MAP24) Red", player), lambda state: + state.has("The Chasm (MAP24) - Red keycard", player, 1)) + set_rule(world.get_entrance("The Chasm (MAP24) Red -> The Chasm (MAP24) Main", player), lambda state: + state.has("The Chasm (MAP24) - Red keycard", player, 1)) + + # Bloodfalls (MAP25) + set_rule(world.get_entrance("Hub -> Bloodfalls (MAP25) Main", player), lambda state: + state.has("Bloodfalls (MAP25)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Super Shotgun", player, 1)) + set_rule(world.get_entrance("Bloodfalls (MAP25) Main -> Bloodfalls (MAP25) Blue", player), lambda state: + state.has("Bloodfalls (MAP25) - Blue skull key", player, 1)) + set_rule(world.get_entrance("Bloodfalls (MAP25) Blue -> Bloodfalls (MAP25) Main", player), lambda state: + state.has("Bloodfalls (MAP25) - Blue skull key", player, 1)) + + # The Abandoned Mines (MAP26) + set_rule(world.get_entrance("Hub -> The Abandoned Mines (MAP26) Main", player), lambda state: + state.has("The Abandoned Mines (MAP26)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("Super Shotgun", player, 1)) + set_rule(world.get_entrance("The Abandoned Mines (MAP26) Main -> The Abandoned Mines (MAP26) Yellow", player), lambda state: + state.has("The Abandoned Mines (MAP26) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Abandoned Mines (MAP26) Main -> The Abandoned Mines (MAP26) Red", player), lambda state: + state.has("The Abandoned Mines (MAP26) - Red keycard", player, 1)) + set_rule(world.get_entrance("The Abandoned Mines (MAP26) Main -> The Abandoned Mines (MAP26) Blue", player), lambda state: + state.has("The Abandoned Mines (MAP26) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Abandoned Mines (MAP26) Blue -> The Abandoned Mines (MAP26) Main", player), lambda state: + state.has("The Abandoned Mines (MAP26) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Abandoned Mines (MAP26) Yellow -> The Abandoned Mines (MAP26) Main", player), lambda state: + state.has("The Abandoned Mines (MAP26) - Yellow keycard", player, 1)) + + # Monster Condo (MAP27) + set_rule(world.get_entrance("Hub -> Monster Condo (MAP27) Main", player), lambda state: + state.has("Monster Condo (MAP27)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Super Shotgun", player, 1)) + set_rule(world.get_entrance("Monster Condo (MAP27) Main -> Monster Condo (MAP27) Yellow", player), lambda state: + state.has("Monster Condo (MAP27) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("Monster Condo (MAP27) Main -> Monster Condo (MAP27) Red", player), lambda state: + state.has("Monster Condo (MAP27) - Red skull key", player, 1)) + set_rule(world.get_entrance("Monster Condo (MAP27) Main -> Monster Condo (MAP27) Blue", player), lambda state: + state.has("Monster Condo (MAP27) - Blue skull key", player, 1)) + set_rule(world.get_entrance("Monster Condo (MAP27) Red -> Monster Condo (MAP27) Main", player), lambda state: + state.has("Monster Condo (MAP27) - Red skull key", player, 1)) + + # The Spirit World (MAP28) + set_rule(world.get_entrance("Hub -> The Spirit World (MAP28) Main", player), lambda state: + state.has("The Spirit World (MAP28)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Super Shotgun", player, 1)) + set_rule(world.get_entrance("The Spirit World (MAP28) Main -> The Spirit World (MAP28) Yellow", player), lambda state: + state.has("The Spirit World (MAP28) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("The Spirit World (MAP28) Main -> The Spirit World (MAP28) Red", player), lambda state: + state.has("The Spirit World (MAP28) - Red skull key", player, 1)) + set_rule(world.get_entrance("The Spirit World (MAP28) Yellow -> The Spirit World (MAP28) Main", player), lambda state: + state.has("The Spirit World (MAP28) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("The Spirit World (MAP28) Red -> The Spirit World (MAP28) Main", player), lambda state: + state.has("The Spirit World (MAP28) - Red skull key", player, 1)) + + # The Living End (MAP29) + set_rule(world.get_entrance("Hub -> The Living End (MAP29) Main", player), lambda state: + state.has("The Living End (MAP29)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Super Shotgun", player, 1)) + + # Icon of Sin (MAP30) + set_rule(world.get_entrance("Hub -> Icon of Sin (MAP30) Main", player), lambda state: + state.has("Icon of Sin (MAP30)", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Super Shotgun", player, 1)) + + +def set_episode4_rules(player, world, pro): + # Wolfenstein2 (MAP31) + set_rule(world.get_entrance("Hub -> Wolfenstein2 (MAP31) Main", player), lambda state: + (state.has("Wolfenstein2 (MAP31)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + + # Grosse2 (MAP32) + set_rule(world.get_entrance("Hub -> Grosse2 (MAP32) Main", player), lambda state: + (state.has("Grosse2 (MAP32)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + + +def set_rules(doom_ii_world: "DOOM2World", included_episodes, pro): + player = doom_ii_world.player + world = doom_ii_world.multiworld + + if included_episodes[0]: + set_episode1_rules(player, world, pro) + if included_episodes[1]: + set_episode2_rules(player, world, pro) + if included_episodes[2]: + set_episode3_rules(player, world, pro) + if included_episodes[3]: + set_episode4_rules(player, world, pro) diff --git a/worlds/doom_ii/__init__.py b/worlds/doom_ii/__init__.py new file mode 100644 index 000000000000..22dee2ab743e --- /dev/null +++ b/worlds/doom_ii/__init__.py @@ -0,0 +1,267 @@ +import functools +import logging +from typing import Any, Dict, List + +from BaseClasses import Entrance, CollectionState, Item, Location, MultiWorld, Region, Tutorial +from worlds.AutoWorld import WebWorld, World +from . import Items, Locations, Maps, Regions, Rules +from .Options import DOOM2Options + +logger = logging.getLogger("DOOM II") + +DOOM_TYPE_LEVEL_COMPLETE = -2 +DOOM_TYPE_COMPUTER_AREA_MAP = 2026 + + +class DOOM2Location(Location): + game: str = "DOOM II" + + +class DOOM2Item(Item): + game: str = "DOOM II" + + +class DOOM2Web(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the DOOM II randomizer connected to an Archipelago Multiworld", + "English", + "setup_en.md", + "setup/en", + ["Daivuk"] + )] + theme = "dirt" + + +class DOOM2World(World): + """ + Doom II, also known as Doom II: Hell on Earth, is a first-person shooter game by id Software. + It was released for MS-DOS in 1994. + Compared to its predecessor, Doom II features larger levels, new enemies, a new "super shotgun" weapon + """ + options_dataclass = DOOM2Options + options: DOOM2Options + game = "DOOM II" + web = DOOM2Web() + data_version = 3 + required_client_version = (0, 3, 9) + + item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} + item_name_groups = Items.item_name_groups + + location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()} + location_name_groups = Locations.location_name_groups + + starting_level_for_episode: List[str] = [ + "Entryway (MAP01)", + "The Factory (MAP12)", + "Nirvana (MAP21)" + ] + + # Item ratio that scales depending on episode count. These are the ratio for 3 episode. In DOOM1. + # The ratio have been tweaked seem, and feel good. + items_ratio: Dict[str, float] = { + "Armor": 41, + "Mega Armor": 25, + "Berserk": 12, + "Invulnerability": 10, + "Partial invisibility": 18, + "Supercharge": 28, + "Medikit": 15, + "Box of bullets": 13, + "Box of rockets": 13, + "Box of shotgun shells": 13, + "Energy cell pack": 10 + } + + def __init__(self, world: MultiWorld, player: int): + self.included_episodes = [1, 1, 1, 0] + self.location_count = 0 + + super().__init__(world, player) + + def get_episode_count(self): + # Don't include 4th, those are secret levels they are additive + return sum(self.included_episodes[:3]) + + def generate_early(self): + # Cache which episodes are included + self.included_episodes[0] = self.options.episode1.value + self.included_episodes[1] = self.options.episode2.value + self.included_episodes[2] = self.options.episode3.value + self.included_episodes[3] = self.options.episode4.value # 4th episode are secret levels + + # If no episodes selected, select Episode 1 + if self.get_episode_count() == 0: + self.included_episodes[0] = 1 + + def create_regions(self): + pro = self.options.pro.value + + # Main regions + menu_region = Region("Menu", self.player, self.multiworld) + hub_region = Region("Hub", self.player, self.multiworld) + self.multiworld.regions += [menu_region, hub_region] + menu_region.add_exits(["Hub"]) + + # Create regions and locations + main_regions = [] + connections = [] + for region_dict in Regions.regions: + if not self.included_episodes[region_dict["episode"] - 1]: + continue + + region_name = region_dict["name"] + if region_dict["connects_to_hub"]: + main_regions.append(region_name) + + region = Region(region_name, self.player, self.multiworld) + region.add_locations({ + loc["name"]: loc_id + for loc_id, loc in Locations.location_table.items() + if loc["region"] == region_name and self.included_episodes[loc["episode"] - 1] + }, DOOM2Location) + + self.multiworld.regions.append(region) + + for connection_dict in region_dict["connections"]: + # Check if it's a pro-only connection + if connection_dict["pro"] and not pro: + continue + connections.append((region, connection_dict["target"])) + + # Connect main regions to Hub + hub_region.add_exits(main_regions) + + # Do the other connections between regions (They are not all both ways) + for connection in connections: + source = connection[0] + target = self.multiworld.get_region(connection[1], self.player) + + entrance = Entrance(self.player, f"{source.name} -> {target.name}", source) + source.exits.append(entrance) + entrance.connect(target) + + # Sum locations for items creation + self.location_count = len(self.multiworld.get_locations(self.player)) + + def completion_rule(self, state: CollectionState): + for map_name in Maps.map_names: + if map_name + " - Exit" not in self.location_name_to_id: + continue + + # Exit location names are in form: Entryway (MAP01) - Exit + loc = Locations.location_table[self.location_name_to_id[map_name + " - Exit"]] + if not self.included_episodes[loc["episode"] - 1]: + continue + + # Map complete item names are in form: Entryway (MAP01) - Complete + if not state.has(map_name + " - Complete", self.player, 1): + return False + + return True + + def set_rules(self): + pro = self.options.pro.value + allow_death_logic = self.options.allow_death_logic.value + + Rules.set_rules(self, self.included_episodes, pro) + self.multiworld.completion_condition[self.player] = lambda state: self.completion_rule(state) + + # Forbid progression items to locations that can be missed and can't be picked up. (e.g. One-time timed + # platform) Unless the user allows for it. + if not allow_death_logic: + for death_logic_location in Locations.death_logic_locations: + self.multiworld.exclude_locations[self.player].value.add(death_logic_location) + + def create_item(self, name: str) -> DOOM2Item: + item_id: int = self.item_name_to_id[name] + return DOOM2Item(name, Items.item_table[item_id]["classification"], item_id, self.player) + + def create_items(self): + itempool: List[DOOM2Item] = [] + start_with_computer_area_maps: bool = self.options.start_with_computer_area_maps.value + + # Items + for item_id, item in Items.item_table.items(): + if item["doom_type"] == DOOM_TYPE_LEVEL_COMPLETE: + continue # We'll fill it manually later + + if item["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP and start_with_computer_area_maps: + continue # We'll fill it manually, and we will put fillers in place + + if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]: + continue + + count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1 + itempool += [self.create_item(item["name"]) for _ in range(count)] + + # Place end level items in locked locations + for map_name in Maps.map_names: + loc_name = map_name + " - Exit" + item_name = map_name + " - Complete" + + if loc_name not in self.location_name_to_id: + continue + + if item_name not in self.item_name_to_id: + continue + + loc = Locations.location_table[self.location_name_to_id[loc_name]] + if not self.included_episodes[loc["episode"] - 1]: + continue + + self.multiworld.get_location(loc_name, self.player).place_locked_item(self.create_item(item_name)) + self.location_count -= 1 + + # Give starting levels right away + for i in range(len(self.starting_level_for_episode)): + if self.included_episodes[i]: + self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i])) + + # Give Computer area maps if option selected + if start_with_computer_area_maps: + for item_id, item_dict in Items.item_table.items(): + item_episode = item_dict["episode"] + if item_episode > 0: + if item_dict["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP and self.included_episodes[item_episode - 1]: + self.multiworld.push_precollected(self.create_item(item_dict["name"])) + + # Fill the rest starting with powerups, then fillers + self.create_ratioed_items("Armor", itempool) + self.create_ratioed_items("Mega Armor", itempool) + self.create_ratioed_items("Berserk", itempool) + self.create_ratioed_items("Invulnerability", itempool) + self.create_ratioed_items("Partial invisibility", itempool) + self.create_ratioed_items("Supercharge", itempool) + + while len(itempool) < self.location_count: + itempool.append(self.create_item(self.get_filler_item_name())) + + # add itempool to multiworld + self.multiworld.itempool += itempool + + def get_filler_item_name(self): + return self.multiworld.random.choice([ + "Medikit", + "Box of bullets", + "Box of rockets", + "Box of shotgun shells", + "Energy cell pack" + ]) + + def create_ratioed_items(self, item_name: str, itempool: List[DOOM2Item]): + remaining_loc = self.location_count - len(itempool) + ep_count = self.get_episode_count() + + # Was balanced based on DOOM 1993's first 3 episodes + count = min(remaining_loc, max(1, int(round(self.items_ratio[item_name] * ep_count / 3)))) + if count == 0: + logger.warning("Warning, no ", item_name, " will be placed.") + return + + for i in range(count): + itempool.append(self.create_item(item_name)) + + def fill_slot_data(self) -> Dict[str, Any]: + return self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "flip_levels", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "episode1", "episode2", "episode3", "episode4") diff --git a/worlds/doom_ii/docs/en_DOOM II.md b/worlds/doom_ii/docs/en_DOOM II.md new file mode 100644 index 000000000000..d561745b76c2 --- /dev/null +++ b/worlds/doom_ii/docs/en_DOOM II.md @@ -0,0 +1,23 @@ +# DOOM II + +## Where is the settings page? + +The [player settings page](../player-settings) contains the options needed to configure your game session. + +## What does randomization do to this game? + +Guns, keycards, and level unlocks have been randomized. Typically, you will end up playing different levels out of order to find your keycards and level unlocks and eventually complete your game. + +Maps can be selected on a level select screen. You can exit a level at any time by visiting the hub station at the beginning of each level. The state of each level is saved and restored upon re-entering the level. + +## What is the goal? + +The goal is to complete every level. + +## What is a "check" in DOOM II? + +Guns, keycards, and powerups have been replaced with Archipelago checks. The switch at the end of each level is also a check. + +## What "items" can you unlock in DOOM II? + +Keycards and level unlocks are your main progression items. Gun unlocks and some upgrades are your useful items. Temporary powerups, ammo, healing, and armor are filler items. diff --git a/worlds/doom_ii/docs/setup_en.md b/worlds/doom_ii/docs/setup_en.md new file mode 100644 index 000000000000..321d440ea68b --- /dev/null +++ b/worlds/doom_ii/docs/setup_en.md @@ -0,0 +1,51 @@ +# DOOM II Randomizer Setup + +## Required Software + +- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2300/DOOM_II/) +- [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases) + +## Optional Software + +- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases) + +## Installing AP Doom +1. Download [APDOOM.zip](https://github.com/Daivuk/apdoom/releases) and extract it. +2. Copy DOOM2.WAD from your steam install into the extracted folder. + You can find the folder in steam by finding the game in your library, + right clicking it and choosing *Manage→Browse Local Files*. + +## Joining a MultiWorld Game + +1. Launch apdoom-launcher.exe +2. Select `DOOM II` from the drop-down +3. Enter the Archipelago server address, slot name, and password (if you have one) +4. Press "Launch DOOM" +5. Enjoy! + +To continue a game, follow the same connection steps. +Connecting with a different seed won't erase your progress in other seeds. + +## Archipelago Text Client + +We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. +APDOOM has in-game messages, +but they disappear quickly and there's no reasonable way to check your message history in-game. + +### Hinting + +To hint from in-game, use the chat (Default key: 'T'). Hinting from DOOM II can be difficult because names are rather long and contain special characters. For example: +``` +!hint Underhalls (MAP02) - Red keycard +``` +The game has a hint helper implemented, where you can simply type this: +``` +!hint map02 red +``` +For this to work, include the map short name (`MAP01`), followed by one of the keywords: `map`, `blue`, `yellow`, `red`. + +## Auto-Tracking + +APDOOM has a functional map tracker integrated into the level select screen. +It tells you which levels you have unlocked, which keys you have for each level, which levels have been completed, +and how many of the checks you have completed in each level. From a18fb0a14f5c7dda82575d4e92cbdfeb95e57ea9 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 24 Nov 2023 12:11:34 -0500 Subject: [PATCH 075/104] Lingo: Move datafiles into a subdirectory (#2459) --- worlds/lingo/{ => data}/LL1.yaml | 61 ++++++++++++++++++++++++++++++++ worlds/lingo/data/__init__.py | 0 worlds/lingo/{ => data}/ids.yaml | 0 worlds/lingo/static_logic.py | 12 ++++--- 4 files changed, 68 insertions(+), 5 deletions(-) rename worlds/lingo/{ => data}/LL1.yaml (99%) create mode 100644 worlds/lingo/data/__init__.py rename worlds/lingo/{ => data}/ids.yaml (100%) diff --git a/worlds/lingo/LL1.yaml b/worlds/lingo/data/LL1.yaml similarity index 99% rename from worlds/lingo/LL1.yaml rename to worlds/lingo/data/LL1.yaml index f8b07b86514b..d46403e8daa6 100644 --- a/worlds/lingo/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -42,6 +42,8 @@ # this panel. # - non_counting: If True, this panel does not contribute to the total needed # to unlock Level 2. + # - hunt: If True, the tracker will show this panel even when it is + # not a check. Used for hunts like the Number Hunt. # # doors is an array of doors associated with this room. When door # randomization is enabled, each of these is an item. The key is a name that @@ -449,6 +451,7 @@ FOUR: id: Backside Room/Panel_four_four_3 tag: midwhite + hunt: True required_door: room: Outside The Undeterred door: Fours @@ -521,12 +524,14 @@ FOUR: id: Backside Room/Panel_four_four_2 tag: midwhite + hunt: True required_door: room: Outside The Undeterred door: Fours EIGHT: id: Backside Room/Panel_eight_eight_8 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights @@ -551,6 +556,7 @@ MASTERY: id: Master Room/Panel_mastery_mastery14 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -662,6 +668,7 @@ EIGHT: id: Backside Room/Panel_eight_eight_5 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights @@ -843,6 +850,7 @@ NINE: id: Backside Room/Panel_nine_nine_3 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines @@ -1055,17 +1063,20 @@ PURPLE: id: Color Arrow Room/Panel_purple_afar tag: midwhite + hunt: True required_door: door: Purple Barrier FIVE (1): id: Backside Room/Panel_five_five_5 tag: midwhite + hunt: True required_door: room: Outside The Undeterred door: Fives FIVE (2): id: Backside Room/Panel_five_five_4 tag: midwhite + hunt: True required_door: room: Outside The Undeterred door: Fives @@ -1296,12 +1307,14 @@ MASTERY (1): id: Master Room/Panel_mastery_mastery5 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery MASTERY (2): id: Master Room/Panel_mastery_mastery9 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -1545,6 +1558,7 @@ BACKSIDE: id: Backside Room/Panel_backside_2 tag: midwhite + hunt: True required_door: door: Backside Door STAIRS: @@ -1912,6 +1926,7 @@ RED: id: Color Arrow Room/Panel_red_afar tag: midwhite + hunt: True required_door: door: Red Barrier DEER + WREN: @@ -2013,6 +2028,7 @@ EIGHT: id: Backside Room/Panel_eight_eight_3 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights @@ -2058,6 +2074,7 @@ NINE: id: Backside Room/Panel_nine_nine_2 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines @@ -2163,6 +2180,7 @@ # accessed by jumping from the top of the tower. id: Master Room/Panel_mastery_mastery8 tag: midwhite + hunt: True required_door: door: Mastery doors: @@ -2234,36 +2252,42 @@ MASTERY (1): id: Master Room/Panel_mastery_mastery6 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery MASTERY (2): id: Master Room/Panel_mastery_mastery7 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery MASTERY (3): id: Master Room/Panel_mastery_mastery10 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery MASTERY (4): id: Master Room/Panel_mastery_mastery11 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery MASTERY (5): id: Master Room/Panel_mastery_mastery12 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery MASTERY (6): id: Master Room/Panel_mastery_mastery15 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -2279,6 +2303,7 @@ MASTERY: id: Master Room/Panel_mastery_mastery3 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -2309,9 +2334,11 @@ id: Strand Room/Panel_i_staring colors: blue tag: forbid + hunt: True GREEN: id: Color Arrow Room/Panel_green_afar tag: midwhite + hunt: True required_door: door: Green Barrier PINECONE: @@ -2356,9 +2383,11 @@ BACKSIDE: id: Backside Room/Panel_backside_3 tag: midwhite + hunt: True NINE: id: Backside Room/Panel_nine_nine_8 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines @@ -2725,35 +2754,41 @@ SEVEN (1): id: Backside Room/Panel_seven_seven_5 tag: midwhite + hunt: True required_door: room: Number Hunt door: Sevens SEVEN (2): id: Backside Room/Panel_seven_seven_6 tag: midwhite + hunt: True required_door: room: Number Hunt door: Sevens EIGHT: id: Backside Room/Panel_eight_eight_7 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights NINE: id: Backside Room/Panel_nine_nine_4 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines BLUE: id: Color Arrow Room/Panel_blue_afar tag: midwhite + hunt: True required_door: door: Blue Barrier ORANGE: id: Color Arrow Room/Panel_orange_afar tag: midwhite + hunt: True required_door: door: Orange Barrier UNCOVER: @@ -3077,6 +3112,7 @@ FOUR: id: Backside Room/Panel_four_four_4 tag: midwhite + hunt: True required_door: room: Outside The Undeterred door: Fours @@ -3135,12 +3171,14 @@ SIX: id: Backside Room/Panel_six_six_4 tag: midwhite + hunt: True required_door: room: Number Hunt door: Sixes NINE: id: Backside Room/Panel_nine_nine_5 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines @@ -3804,46 +3842,54 @@ FIVE (1): id: Backside Room/Panel_five_five_3 tag: midwhite + hunt: True required_panel: panel: LIGHT FIVE (2): id: Backside Room/Panel_five_five_2 tag: midwhite + hunt: True required_panel: panel: WARD SIX (1): id: Backside Room/Panel_six_six_3 tag: midwhite + hunt: True required_door: room: Number Hunt door: Sixes SIX (2): id: Backside Room/Panel_six_six_2 tag: midwhite + hunt: True required_door: room: Number Hunt door: Sixes SEVEN: id: Backside Room/Panel_seven_seven_2 tag: midwhite + hunt: True required_door: room: Number Hunt door: Sevens EIGHT: id: Backside Room/Panel_eight_eight_2 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights NINE: id: Backside Room/Panel_nine_nine_6 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines BACKSIDE: id: Backside Room/Panel_backside_4 tag: midwhite + hunt: True "834283054": id: Tower Room/Panel_834283054_undaunted colors: orange @@ -3864,6 +3910,7 @@ YELLOW: id: Color Arrow Room/Panel_yellow_afar tag: midwhite + hunt: True required_door: door: Yellow Barrier WADED + WEE: @@ -4100,6 +4147,7 @@ BACKSIDE: id: Backside Room/Panel_backside_5 tag: midwhite + hunt: True required_door: door: Backside Door PART: @@ -4144,6 +4192,7 @@ colors: - red - yellow + hunt: True required_door: room: Number Hunt door: Sixes @@ -4221,6 +4270,7 @@ colors: - red - yellow + hunt: True required_door: room: Number Hunt door: Sixes @@ -4547,6 +4597,7 @@ MASTERY: id: Master Room/Panel_mastery_mastery2 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -4988,18 +5039,21 @@ SEVEN (1): id: Backside Room/Panel_seven_seven_7 tag: midwhite + hunt: True required_door: - room: Number Hunt door: Sevens SEVEN (2): id: Backside Room/Panel_seven_seven_3 tag: midwhite + hunt: True required_door: - room: Number Hunt door: Sevens SEVEN (3): id: Backside Room/Panel_seven_seven_4 tag: midwhite + hunt: True required_door: - room: Number Hunt door: Sevens @@ -5598,6 +5652,7 @@ EIGHT: id: Backside Room/Panel_eight_eight_4 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights @@ -5797,6 +5852,7 @@ MASTERY: id: Master Room/Panel_mastery_mastery4 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -5915,9 +5971,11 @@ id: Strand Room/Panel_a_strands colors: blue tag: forbid + hunt: True NINE: id: Backside Room/Panel_nine_nine_7 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines @@ -5929,6 +5987,7 @@ MASTERY: id: Master Room/Panel_mastery_mastery13 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -6025,6 +6084,7 @@ EIGHT: id: Backside Room/Panel_eight_eight_6 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights @@ -6322,6 +6382,7 @@ NINE: id: Backside Room/Panel_nine_nine_9 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines diff --git a/worlds/lingo/data/__init__.py b/worlds/lingo/data/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/lingo/ids.yaml b/worlds/lingo/data/ids.yaml similarity index 100% rename from worlds/lingo/ids.yaml rename to worlds/lingo/data/ids.yaml diff --git a/worlds/lingo/static_logic.py b/worlds/lingo/static_logic.py index f6690f93a439..e9f82fb751ca 100644 --- a/worlds/lingo/static_logic.py +++ b/worlds/lingo/static_logic.py @@ -1,6 +1,6 @@ from typing import Dict, List, NamedTuple, Optional, Set -import yaml +import Utils class RoomAndDoor(NamedTuple): @@ -108,9 +108,11 @@ def load_static_data(): except ImportError: from importlib_resources import files + from . import data + # Load in all item and location IDs. These are broken up into groups based on the type of item/location. - with files("worlds.lingo").joinpath("ids.yaml").open() as file: - config = yaml.load(file, Loader=yaml.Loader) + with files(data).joinpath("ids.yaml").open() as file: + config = Utils.parse_yaml(file) if "special_items" in config: for item_name, item_id in config["special_items"].items(): @@ -144,8 +146,8 @@ def load_static_data(): PROGRESSIVE_ITEM_IDS[item_name] = item_id # Process the main world file. - with files("worlds.lingo").joinpath("LL1.yaml").open() as file: - config = yaml.load(file, Loader=yaml.Loader) + with files(data).joinpath("LL1.yaml").open() as file: + config = Utils.parse_yaml(file) for room_name, room_data in config.items(): process_room(room_name, room_data) From 4641456ba26a06c56286bd1f5b8a65505f29dfe0 Mon Sep 17 00:00:00 2001 From: digiholic Date: Fri, 24 Nov 2023 10:14:05 -0700 Subject: [PATCH 076/104] MMBN3: Small Bug Fixes (#2282) Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> Co-authored-by: Zach Parks --- MMBN3Client.py | 2 +- worlds/mmbn3/Locations.py | 2 +- worlds/mmbn3/__init__.py | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/MMBN3Client.py b/MMBN3Client.py index 3f7474a6fd50..140a98745c26 100644 --- a/MMBN3Client.py +++ b/MMBN3Client.py @@ -58,7 +58,7 @@ def _cmd_debug(self): class MMBN3Context(CommonContext): command_processor = MMBN3CommandProcessor game = "MegaMan Battle Network 3" - items_handling = 0b001 # full local + items_handling = 0b101 # full local except starting items def __init__(self, server_address, password): super().__init__(server_address, password) diff --git a/worlds/mmbn3/Locations.py b/worlds/mmbn3/Locations.py index fc5910334055..0e2a1c51d11b 100644 --- a/worlds/mmbn3/Locations.py +++ b/worlds/mmbn3/Locations.py @@ -208,7 +208,7 @@ class MMBN3Location(Location): LocationData(LocationName.ACDC_Class_5B_Bookshelf, 0xb3109e, 0x200024c, 0x40, 0x737634, 235, [5, 6]), LocationData(LocationName.SciLab_Garbage_Can, 0xb3109f, 0x200024c, 0x8, 0x73AC20, 222, [4, 5]), LocationData(LocationName.Yoka_Inn_Jars, 0xb310a0, 0x200024c, 0x80, 0x747B1C, 237, [4, 5]), - LocationData(LocationName.Yoka_Zoo_Garbage, 0xb310a1, 0x200024d, 0x8, 0x749444, 226, [4]), + LocationData(LocationName.Yoka_Zoo_Garbage, 0xb310a1, 0x200024d, 0x8, 0x749444, 226, [5]), LocationData(LocationName.Beach_Department_Store, 0xb310a2, 0x2000161, 0x40, 0x74C27C, 196, [0, 1]), LocationData(LocationName.Beach_Hospital_Plaque, 0xb310a3, 0x200024c, 0x4, 0x754394, 220, [3, 4]), LocationData(LocationName.Beach_Hospital_Pink_Door, 0xb310a4, 0x200024d, 0x4, 0x754D00, 220, [4]), diff --git a/worlds/mmbn3/__init__.py b/worlds/mmbn3/__init__.py index ec68825c2d2c..acf258a730c6 100644 --- a/worlds/mmbn3/__init__.py +++ b/worlds/mmbn3/__init__.py @@ -15,6 +15,7 @@ from .Regions import regions, RegionName from .Names.ItemName import ItemName from .Names.LocationName import LocationName +from worlds.generic.Rules import add_item_rule class MMBN3Settings(settings.Group): @@ -91,6 +92,9 @@ def create_regions(self) -> None: loc = MMBN3Location(self.player, location, self.location_name_to_id.get(location, None), region) if location in self.excluded_locations: loc.progress_type = LocationProgressType.EXCLUDED + # Do not place any progression items on WWW Island + if region_info.name == RegionName.WWW_Island: + add_item_rule(loc, lambda item: not item.advancement) region.locations.append(loc) self.multiworld.regions.append(region) for region_info in regions: From 15797175c7a4d47aee9f031aa59e14a265ee0996 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 24 Nov 2023 20:38:46 +0100 Subject: [PATCH 077/104] The Witness: New junk hints (#2495) --- worlds/witness/hints.py | 48 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 24302f0c6724..1e54ec352cb6 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -69,6 +69,12 @@ "Have you tried Undertale?\nI hope I'm not the 10th person to ask you that. But it's, like, really good.", "Have you tried Wargroove?\nI'm glad that for every abandoned series, enough people are yearning for its return that one of them will know how to code.", "Have you tried Blasphemous?\nYou haven't? Blasphemy!\n...Sorry. You should try it, though!", + "Have you tried Doom II?\nGot a good game on your hands? Just make it bigger and better.", + "Have you tried Lingo?\nIt's an open world puzzle game. It features panels with non-verbally explained mechanics.\nIf you like this game, you'll like Lingo too.", + "(Middle Yellow)\nYOU AILED OVERNIGHT\nH--- --- ----- -----?", + "Have you tried Bumper Stickers?\nMaybe after spending so much time on this island, you are longing for a simpler puzzle game.", + "Have you tried Pokemon Emerald?\nI'm going to say it: 10/10, just the right amount of water.", + "Have you tried Terraria?\nA prime example of a survival sandbox game that beats the \"Wide as an ocean, deep as a puddle\" allegations.", "One day I was fascinated by the subject of generation of waves by wind.", "I don't like sandwiches. Why would you think I like sandwiches? Have you ever seen me with a sandwich?", @@ -112,8 +118,46 @@ "Have you found a red page yet? No? Then have you found a blue page?", "And here we see the Witness player, seeking answers where there are none-\nDid someone turn on the loudspeaker?", - "Hints suggested by:\nIHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi," - "KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch.", + "Be quiet. I can't hear the elevator.", + "Witness me.\n- The famous last words of John Witness.", + "It's okay, I always have to skip the Rotated Shaper puzzles too.", + "Alan please add hint.", + "Rumor has it there's an audio log with a hint nearby.", + "In the future, war will break out between obelisk_sides and individual EP players.\nWhich side are you on?", + "Droplets: Low, High, Mid.\nAmbience: Mid, Low, Mid, High.", + "Name a better game involving lines. I'll wait.", + "\"You have to draw a line in the sand.\"\n- Arin \"Egoraptor\" Hanson", + "Have you tried?\nThe puzzles tend to get easier if you do.", + "Sorry, I accidentally left my phone in the Jungle.\nAnd also all my fragile dishes.", + "Winner of the \"Most Irrelevant PR in AP History\" award!", + "I bet you wish this was a real hint :)", + "\"This hint is an impostor.\"- Junk hint submitted by T1mshady.\n...wait, I'm not supposed to say that part?", + "Wouldn't you like to know, weather buoy?", + "Give me a few minutes, I should have better material by then.", + "Just pet the doggy! You know you want to!!!", + "ceci n'est pas une metroidvania", + "HINT is MELT\nYOU is HOT", + "Who's that behind you?", + ":3", + "^v ^^v> >>^>v\n^^v>v ^v>> v>^> v>v^", + "Statement #0162601, regarding a strange island that--\nOh, wait, sorry. I'm not supposed to be here.", + "Hollow Bastion has 6 progression items.\nOr maybe it doesn't.\nI wouldn't know.", + "Set your hint count lower so I can tell you more jokes next time.", + "A non-edge start point is similar to a cat.\nIt must be either inside or outside, it can't be both.", + "What if we kissed on the Bunker Laser Platform?\nJk... unless?", + "You don't have Boat? Invisible boat time!\nYou do have boat? Boat clipping time!", + "Cet indice est en français. Nous nous excusons de tout inconvénients engendrés par cela.", + "How many of you have personally witnessed a total solar eclipse?", + "In the Treehouse area, you will find \n[Error: Data not found] progression items.", + "Lingo\nLingoing\nLingone", + "The name of the captain was Albert Einstein.", + "Panel impossible Sigma plz fix", + "Welcome Back! (:", + "R R R U L L U L U R U R D R D R U U", + "Have you tried checking your tracker?", + + "Hints suggested by:\nIHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi, Jasper Bird, T1mshady," + "KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch, Rever, Ishigh, snolid.", ] From e64c7b1cbb7ece0c139462c47175cd7ed4f19294 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Fri, 24 Nov 2023 16:50:32 -0500 Subject: [PATCH 078/104] Fix player-options and weighted-options failing to validate settings if a payer's name is entirely numeric (#2496) --- WebHostLib/static/assets/player-options.js | 2 +- WebHostLib/static/assets/weighted-options.js | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/WebHostLib/static/assets/player-options.js b/WebHostLib/static/assets/player-options.js index 54fae2909a99..2bb4a3ba1357 100644 --- a/WebHostLib/static/assets/player-options.js +++ b/WebHostLib/static/assets/player-options.js @@ -463,7 +463,7 @@ const exportOptions = () => { options['description'] = `Generated by https://archipelago.gg with the ${preset} preset.`; } - if (!options.name || options.name.trim().length === 0) { + if (!options.name || options.name.toString().trim().length === 0) { return showUserMessage('You must enter a player name!'); } const yamlText = jsyaml.safeDump(options, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`); diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js index 34dfbae4bbee..19928327bb54 100644 --- a/WebHostLib/static/assets/weighted-options.js +++ b/WebHostLib/static/assets/weighted-options.js @@ -210,7 +210,11 @@ class WeightedSettings { let errorMessage = null; // User must choose a name for their file - if (!settings.name || settings.name.trim().length === 0 || settings.name.toLowerCase().trim() === 'player') { + if ( + !settings.name || + settings.name.toString().trim().length === 0 || + settings.name.toString().toLowerCase().trim() === 'player' + ) { userMessage.innerText = 'You forgot to set your player name at the top of the page!'; userMessage.classList.add('visible'); userMessage.scrollIntoView({ @@ -256,7 +260,7 @@ class WeightedSettings { // Remove empty arrays else if ( - ['exclude_locations', 'priority_locations', 'local_items', + ['exclude_locations', 'priority_locations', 'local_items', 'non_local_items', 'start_hints', 'start_location_hints'].includes(setting) && settings[game][setting].length === 0 ) { From c944ecf628c393ff3d296ce95355298cbce31086 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Sat, 25 Nov 2023 00:10:52 +0100 Subject: [PATCH 079/104] Core: Introduce new Option class NamedRange (#2330) Co-authored-by: Chris Wilson Co-authored-by: Zach Parks --- Options.py | 29 +- WebHostLib/options.py | 6 +- WebHostLib/static/assets/player-options.js | 62 ++-- WebHostLib/static/assets/weighted-options.js | 317 +++++++++--------- WebHostLib/static/styles/player-options.css | 6 +- docs/options api.md | 9 +- docs/world api.md | 4 +- test/webhost/test_option_presets.py | 8 +- worlds/dlcquest/Options.py | 4 +- worlds/dlcquest/test/TestOptionsLong.py | 4 +- worlds/hk/Options.py | 6 +- worlds/lufia2ac/Options.py | 17 +- worlds/pokemon_rb/options.py | 14 +- worlds/stardew_valley/options.py | 16 +- worlds/stardew_valley/test/TestOptions.py | 8 +- .../test/long/TestOptionsLong.py | 4 +- .../test/long/TestRandomWorlds.py | 4 +- worlds/zillion/options.py | 6 +- 18 files changed, 280 insertions(+), 244 deletions(-) diff --git a/Options.py b/Options.py index 9b4f9d990879..2e3927aae3f3 100644 --- a/Options.py +++ b/Options.py @@ -696,11 +696,19 @@ def triangular(lower: int, end: int, tri: typing.Optional[int] = None) -> int: return int(round(random.triangular(lower, end, tri), 0)) -class SpecialRange(Range): - special_range_cutoff = 0 +class NamedRange(Range): special_range_names: typing.Dict[str, int] = {} """Special Range names have to be all lowercase as matching is done with text.lower()""" + def __init__(self, value: int) -> None: + if value < self.range_start and value not in self.special_range_names.values(): + raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__} " + + f"and is also not one of the supported named special values: {self.special_range_names}") + elif value > self.range_end and value not in self.special_range_names.values(): + raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " + + f"and is also not one of the supported named special values: {self.special_range_names}") + self.value = value + @classmethod def from_text(cls, text: str) -> Range: text = text.lower() @@ -708,6 +716,19 @@ def from_text(cls, text: str) -> Range: return cls(cls.special_range_names[text]) return super().from_text(text) + +class SpecialRange(NamedRange): + special_range_cutoff = 0 + + # TODO: remove class SpecialRange, earliest 3 releases after 0.4.3 + def __new__(cls, value: int) -> SpecialRange: + from Utils import deprecate + deprecate(f"Option type {cls.__name__} is a subclass of SpecialRange, which is deprecated and pending removal. " + "Consider switching to NamedRange, which supports all use-cases of SpecialRange, and more. In " + "NamedRange, range_start specifies the lower end of the regular range, while special values can be " + "placed anywhere (below, inside, or above the regular range).") + return super().__new__(cls, value) + @classmethod def weighted_range(cls, text) -> Range: if text == "random-low": @@ -891,7 +912,7 @@ class Accessibility(Choice): default = 1 -class ProgressionBalancing(SpecialRange): +class ProgressionBalancing(NamedRange): """A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. A lower setting means more getting stuck. A higher setting means less getting stuck.""" default = 50 @@ -1108,7 +1129,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge if os.path.isfile(full_path) and full_path.endswith(".yaml"): os.unlink(full_path) - def dictify_range(option: typing.Union[Range, SpecialRange]): + def dictify_range(option: Range): data = {option.default: 50} for sub_option in ["random", "random-low", "random-high"]: if sub_option != option.default: diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 4d17c7fdde19..0158de7e241f 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -81,8 +81,8 @@ def get_html_doc(option_type: type(Options.Option)) -> str: "max": option.range_end, } - if issubclass(option, Options.SpecialRange): - game_options[option_name]["type"] = 'special_range' + if issubclass(option, Options.NamedRange): + game_options[option_name]["type"] = 'named_range' game_options[option_name]["value_names"] = {} for key, val in option.special_range_names.items(): game_options[option_name]["value_names"][key] = val @@ -133,7 +133,7 @@ def get_html_doc(option_type: type(Options.Option)) -> str: continue option = world.options_dataclass.type_hints[option_name].from_any(option_value) - if isinstance(option, Options.SpecialRange) and isinstance(option_value, str): + if isinstance(option, Options.NamedRange) and isinstance(option_value, str): assert option_value in option.special_range_names, \ f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. " \ f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}." diff --git a/WebHostLib/static/assets/player-options.js b/WebHostLib/static/assets/player-options.js index 2bb4a3ba1357..37ba7f98ff19 100644 --- a/WebHostLib/static/assets/player-options.js +++ b/WebHostLib/static/assets/player-options.js @@ -216,13 +216,13 @@ const buildOptionsTable = (options, romOpts = false) => { element.appendChild(randomButton); break; - case 'special_range': + case 'named_range': element = document.createElement('div'); - element.classList.add('special-range-container'); + element.classList.add('named-range-container'); // Build the select element - let specialRangeSelect = document.createElement('select'); - specialRangeSelect.setAttribute('data-key', option); + let namedRangeSelect = document.createElement('select'); + namedRangeSelect.setAttribute('data-key', option); Object.keys(options[option].value_names).forEach((presetName) => { let presetOption = document.createElement('option'); presetOption.innerText = presetName; @@ -232,58 +232,58 @@ const buildOptionsTable = (options, romOpts = false) => { words[i] = words[i][0].toUpperCase() + words[i].substring(1); } presetOption.innerText = words.join(' '); - specialRangeSelect.appendChild(presetOption); + namedRangeSelect.appendChild(presetOption); }); let customOption = document.createElement('option'); customOption.innerText = 'Custom'; customOption.value = 'custom'; customOption.selected = true; - specialRangeSelect.appendChild(customOption); + namedRangeSelect.appendChild(customOption); if (Object.values(options[option].value_names).includes(Number(currentOptions[gameName][option]))) { - specialRangeSelect.value = Number(currentOptions[gameName][option]); + namedRangeSelect.value = Number(currentOptions[gameName][option]); } // Build range element - let specialRangeWrapper = document.createElement('div'); - specialRangeWrapper.classList.add('special-range-wrapper'); - let specialRange = document.createElement('input'); - specialRange.setAttribute('type', 'range'); - specialRange.setAttribute('data-key', option); - specialRange.setAttribute('min', options[option].min); - specialRange.setAttribute('max', options[option].max); - specialRange.value = currentOptions[gameName][option]; + let namedRangeWrapper = document.createElement('div'); + namedRangeWrapper.classList.add('named-range-wrapper'); + let namedRange = document.createElement('input'); + namedRange.setAttribute('type', 'range'); + namedRange.setAttribute('data-key', option); + namedRange.setAttribute('min', options[option].min); + namedRange.setAttribute('max', options[option].max); + namedRange.value = currentOptions[gameName][option]; // Build rage value element - let specialRangeVal = document.createElement('span'); - specialRangeVal.classList.add('range-value'); - specialRangeVal.setAttribute('id', `${option}-value`); - specialRangeVal.innerText = currentOptions[gameName][option] !== 'random' ? + let namedRangeVal = document.createElement('span'); + namedRangeVal.classList.add('range-value'); + namedRangeVal.setAttribute('id', `${option}-value`); + namedRangeVal.innerText = currentOptions[gameName][option] !== 'random' ? currentOptions[gameName][option] : options[option].defaultValue; // Configure select event listener - specialRangeSelect.addEventListener('change', (event) => { + namedRangeSelect.addEventListener('change', (event) => { if (event.target.value === 'custom') { return; } // Update range slider - specialRange.value = event.target.value; + namedRange.value = event.target.value; document.getElementById(`${option}-value`).innerText = event.target.value; updateGameOption(event.target); }); // Configure range event handler - specialRange.addEventListener('change', (event) => { + namedRange.addEventListener('change', (event) => { // Update select element - specialRangeSelect.value = + namedRangeSelect.value = (Object.values(options[option].value_names).includes(parseInt(event.target.value))) ? parseInt(event.target.value) : 'custom'; document.getElementById(`${option}-value`).innerText = event.target.value; updateGameOption(event.target); }); - element.appendChild(specialRangeSelect); - specialRangeWrapper.appendChild(specialRange); - specialRangeWrapper.appendChild(specialRangeVal); - element.appendChild(specialRangeWrapper); + element.appendChild(namedRangeSelect); + namedRangeWrapper.appendChild(namedRange); + namedRangeWrapper.appendChild(namedRangeVal); + element.appendChild(namedRangeWrapper); // Randomize button randomButton.innerText = '🎲'; @@ -291,15 +291,15 @@ const buildOptionsTable = (options, romOpts = false) => { randomButton.setAttribute('data-key', option); randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!'); randomButton.addEventListener('click', (event) => toggleRandomize( - event, specialRange, specialRangeSelect) + event, namedRange, namedRangeSelect) ); if (currentOptions[gameName][option] === 'random') { randomButton.classList.add('active'); - specialRange.disabled = true; - specialRangeSelect.disabled = true; + namedRange.disabled = true; + namedRangeSelect.disabled = true; } - specialRangeWrapper.appendChild(randomButton); + namedRangeWrapper.appendChild(randomButton); break; default: diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js index 19928327bb54..a2fedb5383b7 100644 --- a/WebHostLib/static/assets/weighted-options.js +++ b/WebHostLib/static/assets/weighted-options.js @@ -93,9 +93,10 @@ class WeightedSettings { }); break; case 'range': - case 'special_range': + case 'named_range': this.current[game][gameSetting]['random'] = 0; this.current[game][gameSetting]['random-low'] = 0; + this.current[game][gameSetting]['random-middle'] = 0; this.current[game][gameSetting]['random-high'] = 0; if (setting.hasOwnProperty('defaultValue')) { this.current[game][gameSetting][setting.defaultValue] = 25; @@ -522,178 +523,185 @@ class GameSettings { break; case 'range': - case 'special_range': + case 'named_range': const rangeTable = document.createElement('table'); const rangeTbody = document.createElement('tbody'); - if (((setting.max - setting.min) + 1) < 11) { - for (let i=setting.min; i <= setting.max; ++i) { - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - tdLeft.innerText = i; - tr.appendChild(tdLeft); + const hintText = document.createElement('p'); + hintText.classList.add('hint-text'); + hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' + + `below, then press the "Add" button to add a weight for it.

Accepted values:
` + + `Normal range: ${setting.min} - ${setting.max}`; + + const acceptedValuesOutsideRange = []; + if (setting.hasOwnProperty('value_names')) { + Object.keys(setting.value_names).forEach((specialName) => { + if ( + (setting.value_names[specialName] < setting.min) || + (setting.value_names[specialName] > setting.max) + ) { + hintText.innerHTML += `
${specialName}: ${setting.value_names[specialName]}`; + acceptedValuesOutsideRange.push(setting.value_names[specialName]); + } + }); - const tdMiddle = document.createElement('td'); - tdMiddle.classList.add('td-middle'); - const range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('id', `${this.name}-${settingName}-${i}-range`); - range.setAttribute('data-game', this.name); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', i); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); - range.value = this.current[settingName][i] || 0; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); + hintText.innerHTML += '

Certain values have special meaning:'; + Object.keys(setting.value_names).forEach((specialName) => { + hintText.innerHTML += `
${specialName}: ${setting.value_names[specialName]}`; + }); + } - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${this.name}-${settingName}-${i}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); + settingWrapper.appendChild(hintText); + + const addOptionDiv = document.createElement('div'); + addOptionDiv.classList.add('add-option-div'); + const optionInput = document.createElement('input'); + optionInput.setAttribute('id', `${this.name}-${settingName}-option`); + let placeholderText = `${setting.min} - ${setting.max}`; + acceptedValuesOutsideRange.forEach((aVal) => placeholderText += `, ${aVal}`); + optionInput.setAttribute('placeholder', placeholderText); + addOptionDiv.appendChild(optionInput); + const addOptionButton = document.createElement('button'); + addOptionButton.innerText = 'Add'; + addOptionDiv.appendChild(addOptionButton); + settingWrapper.appendChild(addOptionDiv); + optionInput.addEventListener('keydown', (evt) => { + if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); } + }); - rangeTbody.appendChild(tr); + addOptionButton.addEventListener('click', () => { + const optionInput = document.getElementById(`${this.name}-${settingName}-option`); + let option = optionInput.value; + if (!option || !option.trim()) { return; } + option = parseInt(option, 10); + + let optionAcceptable = false; + if ((option > setting.min) && (option < setting.max)) { + optionAcceptable = true; } - } else { - const hintText = document.createElement('p'); - hintText.classList.add('hint-text'); - hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' + - `below, then press the "Add" button to add a weight for it.
Minimum value: ${setting.min}
` + - `Maximum value: ${setting.max}`; - - if (setting.hasOwnProperty('value_names')) { - hintText.innerHTML += '

Certain values have special meaning:'; - Object.keys(setting.value_names).forEach((specialName) => { - hintText.innerHTML += `
${specialName}: ${setting.value_names[specialName]}`; - }); + if (setting.hasOwnProperty('value_names') && Object.values(setting.value_names).includes(option)){ + optionAcceptable = true; } + if (!optionAcceptable) { return; } + + optionInput.value = ''; + if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; } + + const tr = document.createElement('tr'); + const tdLeft = document.createElement('td'); + tdLeft.classList.add('td-left'); + tdLeft.innerText = option; + if ( + setting.hasOwnProperty('value_names') && + Object.values(setting.value_names).includes(parseInt(option, 10)) + ) { + const optionName = Object.keys(setting.value_names).find( + (key) => setting.value_names[key] === parseInt(option, 10) + ); + tdLeft.innerText += ` [${optionName}]`; + } + tr.appendChild(tdLeft); + + const tdMiddle = document.createElement('td'); + tdMiddle.classList.add('td-middle'); + const range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('id', `${this.name}-${settingName}-${option}-range`); + range.setAttribute('data-game', this.name); + range.setAttribute('data-setting', settingName); + range.setAttribute('data-option', option); + range.setAttribute('min', 0); + range.setAttribute('max', 50); + range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); + range.value = this.current[settingName][parseInt(option, 10)]; + tdMiddle.appendChild(range); + tr.appendChild(tdMiddle); + + const tdRight = document.createElement('td'); + tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`) + tdRight.classList.add('td-right'); + tdRight.innerText = range.value; + tr.appendChild(tdRight); - settingWrapper.appendChild(hintText); - - const addOptionDiv = document.createElement('div'); - addOptionDiv.classList.add('add-option-div'); - const optionInput = document.createElement('input'); - optionInput.setAttribute('id', `${this.name}-${settingName}-option`); - optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`); - addOptionDiv.appendChild(optionInput); - const addOptionButton = document.createElement('button'); - addOptionButton.innerText = 'Add'; - addOptionDiv.appendChild(addOptionButton); - settingWrapper.appendChild(addOptionDiv); - optionInput.addEventListener('keydown', (evt) => { - if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); } + const tdDelete = document.createElement('td'); + tdDelete.classList.add('td-delete'); + const deleteButton = document.createElement('span'); + deleteButton.classList.add('range-option-delete'); + deleteButton.innerText = '❌'; + deleteButton.addEventListener('click', () => { + range.value = 0; + range.dispatchEvent(new Event('change')); + rangeTbody.removeChild(tr); }); + tdDelete.appendChild(deleteButton); + tr.appendChild(tdDelete); - addOptionButton.addEventListener('click', () => { - const optionInput = document.getElementById(`${this.name}-${settingName}-option`); - let option = optionInput.value; - if (!option || !option.trim()) { return; } - option = parseInt(option, 10); - if ((option < setting.min) || (option > setting.max)) { return; } - optionInput.value = ''; - if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; } + rangeTbody.appendChild(tr); - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - tdLeft.innerText = option; - tr.appendChild(tdLeft); + // Save new option to settings + range.dispatchEvent(new Event('change')); + }); - const tdMiddle = document.createElement('td'); - tdMiddle.classList.add('td-middle'); - const range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('id', `${this.name}-${settingName}-${option}-range`); - range.setAttribute('data-game', this.name); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', option); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); - range.value = this.current[settingName][parseInt(option, 10)]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); + Object.keys(this.current[settingName]).forEach((option) => { + // These options are statically generated below, and should always appear even if they are deleted + // from localStorage + if (['random', 'random-low', 'random-middle', 'random-high'].includes(option)) { return; } - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); + const tr = document.createElement('tr'); + const tdLeft = document.createElement('td'); + tdLeft.classList.add('td-left'); + tdLeft.innerText = option; + if ( + setting.hasOwnProperty('value_names') && + Object.values(setting.value_names).includes(parseInt(option, 10)) + ) { + const optionName = Object.keys(setting.value_names).find( + (key) => setting.value_names[key] === parseInt(option, 10) + ); + tdLeft.innerText += ` [${optionName}]`; + } + tr.appendChild(tdLeft); - const tdDelete = document.createElement('td'); - tdDelete.classList.add('td-delete'); - const deleteButton = document.createElement('span'); - deleteButton.classList.add('range-option-delete'); - deleteButton.innerText = '❌'; - deleteButton.addEventListener('click', () => { - range.value = 0; - range.dispatchEvent(new Event('change')); - rangeTbody.removeChild(tr); - }); - tdDelete.appendChild(deleteButton); - tr.appendChild(tdDelete); + const tdMiddle = document.createElement('td'); + tdMiddle.classList.add('td-middle'); + const range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('id', `${this.name}-${settingName}-${option}-range`); + range.setAttribute('data-game', this.name); + range.setAttribute('data-setting', settingName); + range.setAttribute('data-option', option); + range.setAttribute('min', 0); + range.setAttribute('max', 50); + range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); + range.value = this.current[settingName][parseInt(option, 10)]; + tdMiddle.appendChild(range); + tr.appendChild(tdMiddle); - rangeTbody.appendChild(tr); + const tdRight = document.createElement('td'); + tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`) + tdRight.classList.add('td-right'); + tdRight.innerText = range.value; + tr.appendChild(tdRight); - // Save new option to settings - range.dispatchEvent(new Event('change')); + const tdDelete = document.createElement('td'); + tdDelete.classList.add('td-delete'); + const deleteButton = document.createElement('span'); + deleteButton.classList.add('range-option-delete'); + deleteButton.innerText = '❌'; + deleteButton.addEventListener('click', () => { + range.value = 0; + const changeEvent = new Event('change'); + changeEvent.action = 'rangeDelete'; + range.dispatchEvent(changeEvent); + rangeTbody.removeChild(tr); }); + tdDelete.appendChild(deleteButton); + tr.appendChild(tdDelete); - Object.keys(this.current[settingName]).forEach((option) => { - // These options are statically generated below, and should always appear even if they are deleted - // from localStorage - if (['random-low', 'random', 'random-high'].includes(option)) { return; } - - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - tdLeft.innerText = option; - tr.appendChild(tdLeft); - - const tdMiddle = document.createElement('td'); - tdMiddle.classList.add('td-middle'); - const range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('id', `${this.name}-${settingName}-${option}-range`); - range.setAttribute('data-game', this.name); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', option); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); - range.value = this.current[settingName][parseInt(option, 10)]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); - - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); - - const tdDelete = document.createElement('td'); - tdDelete.classList.add('td-delete'); - const deleteButton = document.createElement('span'); - deleteButton.classList.add('range-option-delete'); - deleteButton.innerText = '❌'; - deleteButton.addEventListener('click', () => { - range.value = 0; - const changeEvent = new Event('change'); - changeEvent.action = 'rangeDelete'; - range.dispatchEvent(changeEvent); - rangeTbody.removeChild(tr); - }); - tdDelete.appendChild(deleteButton); - tr.appendChild(tdDelete); - - rangeTbody.appendChild(tr); - }); - } + rangeTbody.appendChild(tr); + }); - ['random', 'random-low', 'random-high'].forEach((option) => { + ['random', 'random-low', 'random-middle', 'random-high'].forEach((option) => { const tr = document.createElement('tr'); const tdLeft = document.createElement('td'); tdLeft.classList.add('td-left'); @@ -704,6 +712,9 @@ class GameSettings { case 'random-low': tdLeft.innerText = "Random (Low)"; break; + case 'random-middle': + tdLeft.innerText = 'Random (Middle)'; + break; case 'random-high': tdLeft.innerText = "Random (High)"; break; diff --git a/WebHostLib/static/styles/player-options.css b/WebHostLib/static/styles/player-options.css index 7d6a19709a93..cc2d5e2de5ce 100644 --- a/WebHostLib/static/styles/player-options.css +++ b/WebHostLib/static/styles/player-options.css @@ -160,18 +160,18 @@ html{ margin-left: 0.25rem; } -#player-options table .special-range-container{ +#player-options table .named-range-container{ display: flex; flex-direction: column; } -#player-options table .special-range-wrapper{ +#player-options table .named-range-wrapper{ display: flex; flex-direction: row; margin-top: 0.25rem; } -#player-options table .special-range-wrapper input[type=range]{ +#player-options table .named-range-wrapper input[type=range]{ flex-grow: 1; } diff --git a/docs/options api.md b/docs/options api.md index 622d0a7ec79f..80d0737e3a7f 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -144,13 +144,20 @@ A numeric option allowing a variety of integers including the endpoints. Has a d `range_end` of 1. Allows for negative values as well. This will always be an integer and has no methods for string comparisons. -### SpecialRange +### NamedRange Like range but also allows you to define a dictionary of special names the user can use to equate to a specific value. +`special_range_names` can be used to +- give descriptive names to certain values from within the range +- add option values above or below the regular range, to be associated with a special meaning + For example: ```python +range_start = 1 +range_end = 99 special_range_names: { "normal": 20, "extreme": 99, + "unlimited": -1, } ``` diff --git a/docs/world api.md b/docs/world api.md index 71710ac2932e..6393f245ba68 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -79,9 +79,9 @@ the options and the values are the values to be set for that option. These prese Note: The values must be a non-aliased value for the option type and can only include the following option types: - - If you have a `Range`/`SpecialRange` option, the value should be an `int` between the `range_start` and `range_end` + - If you have a `Range`/`NamedRange` option, the value should be an `int` between the `range_start` and `range_end` values. - - If you have a `SpecialRange` option, the value can alternatively be a `str` that is one of the + - If you have a `NamedRange` option, the value can alternatively be a `str` that is one of the `special_range_names` keys. - If you have a `Choice` option, the value should be a `str` that is one of the `option_` values. - If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`. diff --git a/test/webhost/test_option_presets.py b/test/webhost/test_option_presets.py index 8c6ebea2088f..0c88b6c2ee6f 100644 --- a/test/webhost/test_option_presets.py +++ b/test/webhost/test_option_presets.py @@ -1,7 +1,7 @@ import unittest from worlds import AutoWorldRegister -from Options import Choice, SpecialRange, Toggle, Range +from Options import Choice, NamedRange, Toggle, Range class TestOptionPresets(unittest.TestCase): @@ -14,7 +14,7 @@ def test_option_presets_have_valid_options(self): with self.subTest(game=game_name, preset=preset_name, option=option_name): try: option = world_type.options_dataclass.type_hints[option_name].from_any(option_value) - supported_types = [Choice, Toggle, Range, SpecialRange] + supported_types = [Choice, Toggle, Range, NamedRange] if not any([issubclass(option.__class__, t) for t in supported_types]): self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' " f"is not a supported type for webhost. " @@ -46,8 +46,8 @@ def test_option_preset_values_are_explicitly_defined(self): # Check for from_text resolving to a different value. ("random" is allowed though.) if option_value != "random" and isinstance(option_value, str): - # Allow special named values for SpecialRange option presets. - if isinstance(option, SpecialRange): + # Allow special named values for NamedRange option presets. + if isinstance(option, NamedRange): self.assertTrue( option_value in option.special_range_names, f"Invalid preset '{option_name}': '{option_value}' in preset '{preset_name}' " diff --git a/worlds/dlcquest/Options.py b/worlds/dlcquest/Options.py index ce728b4e9244..769acbec1566 100644 --- a/worlds/dlcquest/Options.py +++ b/worlds/dlcquest/Options.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from Options import Choice, DeathLink, PerGameCommonOptions, SpecialRange +from Options import Choice, DeathLink, NamedRange, PerGameCommonOptions class DoubleJumpGlitch(Choice): @@ -33,7 +33,7 @@ class CoinSanity(Choice): default = 0 -class CoinSanityRange(SpecialRange): +class CoinSanityRange(NamedRange): """This is the amount of coins in a coin bundle You need to collect that number of coins to get a location check, and when receiving coin items, you will get bundles of this size It is highly recommended to not set this value below 10, as it generates a very large number of boring locations and items. diff --git a/worlds/dlcquest/test/TestOptionsLong.py b/worlds/dlcquest/test/TestOptionsLong.py index d0a5c0ed7dfb..3e9acac7e791 100644 --- a/worlds/dlcquest/test/TestOptionsLong.py +++ b/worlds/dlcquest/test/TestOptionsLong.py @@ -1,7 +1,7 @@ from typing import Dict from BaseClasses import MultiWorld -from Options import SpecialRange +from Options import NamedRange from .option_names import options_to_include from .checks.world_checks import assert_can_win, assert_same_number_items_locations from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld @@ -14,7 +14,7 @@ def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld): def get_option_choices(option) -> Dict[str, int]: - if issubclass(option, SpecialRange): + if issubclass(option, NamedRange): return option.special_range_names elif option.options: return option.options diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 2a19ffd3e7c3..fcc938474d0c 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -2,7 +2,7 @@ from .ExtractedData import logic_options, starts, pool_options from .Rules import cost_terms -from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, SpecialRange +from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange from .Charms import vanilla_costs, names as charm_names if typing.TYPE_CHECKING: @@ -242,7 +242,7 @@ class MaximumGeoPrice(Range): default = 400 -class RandomCharmCosts(SpecialRange): +class RandomCharmCosts(NamedRange): """Total Notch Cost of all Charms together. Vanilla sums to 90. This value is distributed among all charms in a random fashion. Special Cases: @@ -250,7 +250,7 @@ class RandomCharmCosts(SpecialRange): Set to -2 or shuffle to shuffle around the vanilla costs to different charms.""" display_name = "Randomize Charm Notch Costs" - range_start = -2 + range_start = 0 range_end = 240 default = -1 vanilla_costs: typing.List[int] = vanilla_costs diff --git a/worlds/lufia2ac/Options.py b/worlds/lufia2ac/Options.py index 419532cded6b..5f33d0bd5d13 100644 --- a/worlds/lufia2ac/Options.py +++ b/worlds/lufia2ac/Options.py @@ -7,8 +7,8 @@ from itertools import accumulate, chain, combinations from typing import Any, cast, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Type, TYPE_CHECKING, Union -from Options import AssembleOptions, Choice, DeathLink, ItemDict, OptionDict, PerGameCommonOptions, Range, \ - SpecialRange, TextChoice, Toggle +from Options import AssembleOptions, Choice, DeathLink, ItemDict, NamedRange, OptionDict, PerGameCommonOptions, Range, \ + TextChoice, Toggle from .Enemies import enemy_name_to_sprite from .Items import ItemType, l2ac_item_table @@ -255,7 +255,7 @@ class CapsuleCravingsJPStyle(Toggle): display_name = "Capsule cravings JP style" -class CapsuleStartingForm(SpecialRange): +class CapsuleStartingForm(NamedRange): """The starting form of your capsule monsters. Supported values: 1 – 4, m @@ -266,7 +266,6 @@ class CapsuleStartingForm(SpecialRange): range_start = 1 range_end = 5 default = 1 - special_range_cutoff = 1 special_range_names = { "default": 1, "m": 5, @@ -280,7 +279,7 @@ def unlock(self) -> int: return self.value - 1 -class CapsuleStartingLevel(LevelMixin, SpecialRange): +class CapsuleStartingLevel(LevelMixin, NamedRange): """The starting level of your capsule monsters. Can be set to the special value party_starting_level to make it the same value as the party_starting_level option. @@ -289,10 +288,9 @@ class CapsuleStartingLevel(LevelMixin, SpecialRange): """ display_name = "Capsule monster starting level" - range_start = 0 + range_start = 1 range_end = 99 default = 1 - special_range_cutoff = 1 special_range_names = { "default": 1, "party_starting_level": 0, @@ -685,7 +683,7 @@ class RunSpeed(Choice): default = option_disabled -class ShopInterval(SpecialRange): +class ShopInterval(NamedRange): """Place shops after a certain number of floors. E.g., if you set this to 5, then you will be given the opportunity to shop after completing B5, B10, B15, etc., @@ -698,10 +696,9 @@ class ShopInterval(SpecialRange): """ display_name = "Shop interval" - range_start = 0 + range_start = 1 range_end = 10 default = 0 - special_range_cutoff = 1 special_range_names = { "disabled": 0, } diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index 794977d32d36..8afe91b86741 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -1,4 +1,4 @@ -from Options import Toggle, Choice, Range, SpecialRange, TextChoice, DeathLink +from Options import Toggle, Choice, Range, NamedRange, TextChoice, DeathLink class GameVersion(Choice): @@ -285,7 +285,7 @@ class AllPokemonSeen(Toggle): display_name = "All Pokemon Seen" -class DexSanity(SpecialRange): +class DexSanity(NamedRange): """Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify a percentage of Pokemon to have checks added. If Accessibility is set to locations, this will be the percentage of all logically reachable Pokemon that will get a location check added to it. With items or minimal Accessibility, it will be the percentage @@ -412,7 +412,7 @@ class LevelScaling(Choice): default = 1 -class ExpModifier(SpecialRange): +class ExpModifier(NamedRange): """Modifier for EXP gained. When specifying a number, exp is multiplied by this amount and divided by 16.""" display_name = "Exp Modifier" default = 16 @@ -607,8 +607,8 @@ class RandomizeTMMoves(Toggle): display_name = "Randomize TM Moves" -class TMHMCompatibility(SpecialRange): - range_start = -1 +class TMHMCompatibility(NamedRange): + range_start = 0 range_end = 100 special_range_names = { "vanilla": -1, @@ -675,12 +675,12 @@ class RandomizeMoveTypes(Toggle): default = 0 -class SecondaryTypeChance(SpecialRange): +class SecondaryTypeChance(NamedRange): """If randomize_pokemon_types is on, this is the chance each Pokemon will have a secondary type. If follow_evolutions is selected, it is the chance a second type will be added at each evolution stage. vanilla will give secondary types to Pokemon that normally have a secondary type.""" display_name = "Secondary Type Chance" - range_start = -1 + range_start = 0 range_end = 100 default = -1 special_range_names = { diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index d85bbf06f6ee..267ebd7a63de 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Dict -from Options import Range, SpecialRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, Option +from Options import Range, NamedRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, Option from .mods.mod_data import ModNames @@ -48,12 +48,12 @@ def get_option_name(cls, value) -> str: return super().get_option_name(value) -class StartingMoney(SpecialRange): +class StartingMoney(NamedRange): """Amount of gold when arriving at the farm. Set to -1 or unlimited for infinite money""" internal_name = "starting_money" display_name = "Starting Gold" - range_start = -1 + range_start = 0 range_end = 50000 default = 5000 @@ -67,7 +67,7 @@ class StartingMoney(SpecialRange): } -class ProfitMargin(SpecialRange): +class ProfitMargin(NamedRange): """Multiplier over all gold earned in-game by the player.""" internal_name = "profit_margin" display_name = "Profit Margin" @@ -283,7 +283,7 @@ class SpecialOrderLocations(Choice): option_board_qi = 2 -class HelpWantedLocations(SpecialRange): +class HelpWantedLocations(NamedRange): """Include location checks for Help Wanted quests Out of every 7 quests, 4 will be item deliveries, and then 1 of each for: Fishing, Gathering and Slaying Monsters. Choosing a multiple of 7 is recommended.""" @@ -429,7 +429,7 @@ class MultipleDaySleepEnabled(Toggle): default = 1 -class MultipleDaySleepCost(SpecialRange): +class MultipleDaySleepCost(NamedRange): """How much gold it will cost to use MultiSleep. You will have to pay that amount for each day skipped.""" internal_name = "multiple_day_sleep_cost" display_name = "Multiple Day Sleep Cost" @@ -446,7 +446,7 @@ class MultipleDaySleepCost(SpecialRange): } -class ExperienceMultiplier(SpecialRange): +class ExperienceMultiplier(NamedRange): """How fast you want to earn skill experience. A lower setting mean less experience. A higher setting means more experience.""" @@ -466,7 +466,7 @@ class ExperienceMultiplier(SpecialRange): } -class FriendshipMultiplier(SpecialRange): +class FriendshipMultiplier(NamedRange): """How fast you want to earn friendship points with villagers. A lower setting mean less friendship per action. A higher setting means more friendship per action.""" diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 02b1ebf64373..ccffc2848a80 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -4,7 +4,7 @@ from typing import Dict from BaseClasses import ItemClassification, MultiWorld -from Options import SpecialRange +from Options import NamedRange from . import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods from .. import StardewItem, items_by_group, Group, StardewValleyWorld from ..locations import locations_by_tag, LocationTags, location_table @@ -42,7 +42,7 @@ def check_no_ginger_island(tester: unittest.TestCase, multiworld: MultiWorld): def get_option_choices(option) -> Dict[str, int]: - if issubclass(option, SpecialRange): + if issubclass(option, NamedRange): return option.special_range_names elif option.options: return option.options @@ -53,7 +53,7 @@ class TestGenerateDynamicOptions(SVTestCase): def test_given_special_range_when_generate_then_basic_checks(self): options = StardewValleyWorld.options_dataclass.type_hints for option_name, option in options.items(): - if not isinstance(option, SpecialRange): + if not isinstance(option, NamedRange): continue for value in option.special_range_names: with self.subTest(f"{option_name}: {value}"): @@ -152,7 +152,7 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestCase): def test_given_special_range_when_generate_exclude_ginger_island(self): options = StardewValleyWorld.options_dataclass.type_hints for option_name, option in options.items(): - if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name: + if not isinstance(option, NamedRange) or option_name == ExcludeGingerIsland.internal_name: continue for value in option.special_range_names: with self.subTest(f"{option_name}: {value}"): diff --git a/worlds/stardew_valley/test/long/TestOptionsLong.py b/worlds/stardew_valley/test/long/TestOptionsLong.py index 3634dc5fd169..e3da6968ed43 100644 --- a/worlds/stardew_valley/test/long/TestOptionsLong.py +++ b/worlds/stardew_valley/test/long/TestOptionsLong.py @@ -2,7 +2,7 @@ from typing import Dict from BaseClasses import MultiWorld -from Options import SpecialRange +from Options import NamedRange from .option_names import options_to_include from worlds.stardew_valley.test.checks.world_checks import assert_can_win, assert_same_number_items_locations from .. import setup_solo_multiworld, SVTestCase @@ -14,7 +14,7 @@ def basic_checks(tester: unittest.TestCase, multiworld: MultiWorld): def get_option_choices(option) -> Dict[str, int]: - if issubclass(option, SpecialRange): + if issubclass(option, NamedRange): return option.special_range_names elif option.options: return option.options diff --git a/worlds/stardew_valley/test/long/TestRandomWorlds.py b/worlds/stardew_valley/test/long/TestRandomWorlds.py index e22c6c3564e5..1f1d59652c5e 100644 --- a/worlds/stardew_valley/test/long/TestRandomWorlds.py +++ b/worlds/stardew_valley/test/long/TestRandomWorlds.py @@ -2,7 +2,7 @@ import random from BaseClasses import MultiWorld -from Options import SpecialRange, Range +from Options import NamedRange, Range from .option_names import options_to_include from .. import setup_solo_multiworld, SVTestCase from ..checks.goal_checks import assert_perfection_world_is_valid, assert_goal_world_is_valid @@ -12,7 +12,7 @@ def get_option_choices(option) -> Dict[str, int]: - if issubclass(option, SpecialRange): + if issubclass(option, NamedRange): return option.special_range_names if issubclass(option, Range): return {f"{val}": val for val in range(option.range_start, option.range_end + 1)} diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 80f9469ec8c0..cb861e962128 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -3,7 +3,7 @@ from typing import Dict, Tuple from typing_extensions import TypeGuard # remove when Python >= 3.10 -from Options import DefaultOnToggle, PerGameCommonOptions, Range, SpecialRange, Toggle, Choice +from Options import DefaultOnToggle, NamedRange, PerGameCommonOptions, Range, Toggle, Choice from zilliandomizer.options import \ Options as ZzOptions, char_to_gun, char_to_jump, ID, \ @@ -11,7 +11,7 @@ from zilliandomizer.options.parsing import validate as zz_validate -class ZillionContinues(SpecialRange): +class ZillionContinues(NamedRange): """ number of continues before game over @@ -218,7 +218,7 @@ class ZillionSkill(Range): default = 2 -class ZillionStartingCards(SpecialRange): +class ZillionStartingCards(NamedRange): """ how many ID Cards to start the game with From e46420f4a9f0676a5f93db573515f73b0e4c3778 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Fri, 24 Nov 2023 17:14:07 -0600 Subject: [PATCH 080/104] MultiServer: Create read-only data storage key for client statuses. (#2412) --- MultiServer.py | 11 ++++++++++- docs/network protocol.md | 13 +++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index bd9d2446af65..9d2e9b564e75 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -2,8 +2,8 @@ import argparse import asyncio -import copy import collections +import copy import datetime import functools import hashlib @@ -417,6 +417,8 @@ def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.A self.player_name_lookup[slot_info.name] = 0, slot_id self.read_data[f"hints_{0}_{slot_id}"] = lambda local_team=0, local_player=slot_id: \ list(self.get_rechecked_hints(local_team, local_player)) + self.read_data[f"client_status_{0}_{slot_id}"] = lambda local_team=0, local_player=slot_id: \ + self.client_game_state[local_team, local_player] self.seed_name = decoded_obj["seed_name"] self.random.seed(self.seed_name) @@ -712,6 +714,12 @@ def on_new_hint(self, team: int, slot: int): "hint_points": get_slot_points(self, team, slot) }]) + def on_client_status_change(self, team: int, slot: int): + key: str = f"_read_client_status_{team}_{slot}" + targets: typing.Set[Client] = set(self.stored_data_notification_clients[key]) + if targets: + self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.client_game_state[team, slot]}]) + def update_aliases(ctx: Context, team: int): cmd = ctx.dumper([{"cmd": "RoomUpdate", @@ -1819,6 +1827,7 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus) ctx.on_goal_achieved(client) ctx.client_game_state[client.team, client.slot] = new_status + ctx.on_client_status_change(client.team, client.slot) ctx.save() diff --git a/docs/network protocol.md b/docs/network protocol.md index c17cc74a8ac7..199f96f48131 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -380,11 +380,12 @@ Additional arguments sent in this package will also be added to the [Retrieved]( Some special keys exist with specific return data, all of them have the prefix `_read_`, so `hints_{team}_{slot}` is `_read_hints_{team}_{slot}`. -| Name | Type | Notes | -|-------------------------------|--------------------------|---------------------------------------------------| -| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. | -| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. | -| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | +| Name | Type | Notes | +|------------------------------|-------------------------------|---------------------------------------------------| +| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. | +| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. | +| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | +| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. | ### Set Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package. @@ -558,7 +559,7 @@ Color options: `player` marks owning player id for location/item, `flags` contains the [NetworkItem](#NetworkItem) flags that belong to the item -### Client States +### ClientStatus An enumeration containing the possible client states that may be used to inform the server in [StatusUpdate](#StatusUpdate). The MultiServer automatically sets the client state to `ClientStatus.CLIENT_CONNECTED` on the first active connection From 8173fd54e7ceead44a20ac7ddf32b3944733bf52 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Fri, 24 Nov 2023 17:16:19 -0600 Subject: [PATCH 081/104] DOOM II: Add to `CODEOWNERS` (#2492) --- docs/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 83f47235323a..b40078dc3440 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -46,6 +46,9 @@ # DOOM 1993 /worlds/doom_1993/ @Daivuk +# DOOM II +/worlds/doom_ii/ @Daivuk + # Factorio /worlds/factorio/ @Berserker66 From 8d41430cc8d3509cd86805af8b4924fe3484ae91 Mon Sep 17 00:00:00 2001 From: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Fri, 24 Nov 2023 16:23:45 -0700 Subject: [PATCH 082/104] Shivers: Implement New Game (#1836) Co-authored-by: Mathx2 Co-authored-by: Zach Parks --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/shivers/Constants.py | 17 + worlds/shivers/Items.py | 112 +++++++ worlds/shivers/Options.py | 50 +++ worlds/shivers/Rules.py | 228 ++++++++++++++ worlds/shivers/__init__.py | 178 +++++++++++ worlds/shivers/data/excluded_locations.json | 52 ++++ worlds/shivers/data/locations.json | 325 ++++++++++++++++++++ worlds/shivers/data/regions.json | 145 +++++++++ worlds/shivers/docs/en_Shivers.md | 31 ++ worlds/shivers/docs/setup_en.md | 60 ++++ 12 files changed, 1202 insertions(+) create mode 100644 worlds/shivers/Constants.py create mode 100644 worlds/shivers/Items.py create mode 100644 worlds/shivers/Options.py create mode 100644 worlds/shivers/Rules.py create mode 100644 worlds/shivers/__init__.py create mode 100644 worlds/shivers/data/excluded_locations.json create mode 100644 worlds/shivers/data/locations.json create mode 100644 worlds/shivers/data/regions.json create mode 100644 worlds/shivers/docs/en_Shivers.md create mode 100644 worlds/shivers/docs/setup_en.md diff --git a/README.md b/README.md index a57f0f9802d4..2bca422fea90 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ Currently, the following games are supported: * Lingo * Pokémon Emerald * DOOM II +* Shivers For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index b40078dc3440..6231da823234 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -113,6 +113,9 @@ # Risk of Rain 2 /worlds/ror2/ @kindasneaki +# Shivers +/worlds/shivers/ @GodlFire + # Sonic Adventure 2 Battle /worlds/sa2b/ @PoryGone @RaspberrySpace diff --git a/worlds/shivers/Constants.py b/worlds/shivers/Constants.py new file mode 100644 index 000000000000..0b00cecec3ec --- /dev/null +++ b/worlds/shivers/Constants.py @@ -0,0 +1,17 @@ +import os +import json +import pkgutil + +def load_data_file(*args) -> dict: + fname = os.path.join("data", *args) + return json.loads(pkgutil.get_data(__name__, fname).decode()) + +location_id_offset: int = 27000 + +location_info = load_data_file("locations.json") +location_name_to_id = {name: location_id_offset + index \ + for index, name in enumerate(location_info["all_locations"])} + +exclusion_info = load_data_file("excluded_locations.json") + +region_info = load_data_file("regions.json") diff --git a/worlds/shivers/Items.py b/worlds/shivers/Items.py new file mode 100644 index 000000000000..caf24ded2987 --- /dev/null +++ b/worlds/shivers/Items.py @@ -0,0 +1,112 @@ +from BaseClasses import Item, ItemClassification +import typing + +class ShiversItem(Item): + game: str = "Shivers" + +class ItemData(typing.NamedTuple): + code: int + type: str + classification: ItemClassification = ItemClassification.progression + +SHIVERS_ITEM_ID_OFFSET = 27000 + +item_table = { + #Pot Pieces + "Water Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 0, "pot"), + "Wax Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 1, "pot"), + "Ash Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 2, "pot"), + "Oil Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 3, "pot"), + "Cloth Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 4, "pot"), + "Wood Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 5, "pot"), + "Crystal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 6, "pot"), + "Lightning Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 7, "pot"), + "Sand Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 8, "pot"), + "Metal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 9, "pot"), + "Water Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 10, "pot"), + "Wax Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 11, "pot"), + "Ash Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 12, "pot"), + "Oil Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 13, "pot"), + "Cloth Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 14, "pot"), + "Wood Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 15, "pot"), + "Crystal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 16, "pot"), + "Lightning Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 17, "pot"), + "Sand Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 18, "pot"), + "Metal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 19, "pot"), + + #Keys + "Key for Office Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 20, "key"), + "Key for Bedroom Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 21, "key"), + "Key for Three Floor Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 22, "key"), + "Key for Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 23, "key"), + "Key for Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 24, "key"), + "Key for Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 25, "key"), + "Key for Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 26, "key"), + "Key for Ocean Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 27, "key"), + "Key for Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 28, "key"), + "Key for Generator Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 29, "key"), + "Key for Egypt Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 30, "key"), + "Key for Library Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 31, "key"), + "Key for Tiki Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 32, "key"), + "Key for UFO Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 33, "key"), + "Key for Torture Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 34, "key"), + "Key for Puzzle Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 35, "key"), + "Key for Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 36, "key"), + "Key for Underground Lake Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 37, "key"), + "Key for Janitor Closet": ItemData(SHIVERS_ITEM_ID_OFFSET + 38, "key"), + "Key for Front Door": ItemData(SHIVERS_ITEM_ID_OFFSET + 39, "key-optional"), + + #Abilities + "Crawling": ItemData(SHIVERS_ITEM_ID_OFFSET + 50, "ability"), + + #Event Items + "Victory": ItemData(SHIVERS_ITEM_ID_OFFSET + 60, "victory"), + + #Duplicate pot pieces for fill_Restrictive + "Water Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 70, "potduplicate"), + "Wax Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 71, "potduplicate"), + "Ash Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 72, "potduplicate"), + "Oil Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 73, "potduplicate"), + "Cloth Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 74, "potduplicate"), + "Wood Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 75, "potduplicate"), + "Crystal Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 76, "potduplicate"), + "Lightning Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 77, "potduplicate"), + "Sand Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 78, "potduplicate"), + "Metal Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 79, "potduplicate"), + "Water Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 80, "potduplicate"), + "Wax Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 81, "potduplicate"), + "Ash Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 82, "potduplicate"), + "Oil Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 83, "potduplicate"), + "Cloth Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 84, "potduplicate"), + "Wood Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 85, "potduplicate"), + "Crystal Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 86, "potduplicate"), + "Lightning Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 87, "potduplicate"), + "Sand Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 88, "potduplicate"), + "Metal Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 89, "potduplicate"), + + #Filler + "Empty": ItemData(SHIVERS_ITEM_ID_OFFSET + 90, "filler"), + "Easier Lyre": ItemData(SHIVERS_ITEM_ID_OFFSET + 91, "filler", ItemClassification.filler), + "Water Always Available in Lobby": ItemData(SHIVERS_ITEM_ID_OFFSET + 92, "filler2", ItemClassification.filler), + "Wax Always Available in Library": ItemData(SHIVERS_ITEM_ID_OFFSET + 93, "filler2", ItemClassification.filler), + "Wax Always Available in Anansi Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 94, "filler2", ItemClassification.filler), + "Wax Always Available in Tiki Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 95, "filler2", ItemClassification.filler), + "Ash Always Available in Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 96, "filler2", ItemClassification.filler), + "Ash Always Available in Burial Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 97, "filler2", ItemClassification.filler), + "Oil Always Available in Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 98, "filler2", ItemClassification.filler), + "Cloth Always Available in Egypt": ItemData(SHIVERS_ITEM_ID_OFFSET + 99, "filler2", ItemClassification.filler), + "Cloth Always Available in Burial Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 100, "filler2", ItemClassification.filler), + "Wood Always Available in Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 101, "filler2", ItemClassification.filler), + "Wood Always Available in Blue Maze": ItemData(SHIVERS_ITEM_ID_OFFSET + 102, "filler2", ItemClassification.filler), + "Wood Always Available in Pegasus Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 103, "filler2", ItemClassification.filler), + "Wood Always Available in Gods Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 104, "filler2", ItemClassification.filler), + "Crystal Always Available in Lobby": ItemData(SHIVERS_ITEM_ID_OFFSET + 105, "filler2", ItemClassification.filler), + "Crystal Always Available in Ocean": ItemData(SHIVERS_ITEM_ID_OFFSET + 106, "filler2", ItemClassification.filler), + "Sand Always Available in Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 107, "filler2", ItemClassification.filler), + "Sand Always Available in Ocean": ItemData(SHIVERS_ITEM_ID_OFFSET + 108, "filler2", ItemClassification.filler), + "Metal Always Available in Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 109, "filler2", ItemClassification.filler), + "Metal Always Available in Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 110, "filler2", ItemClassification.filler), + "Metal Always Available in Prehistoric": ItemData(SHIVERS_ITEM_ID_OFFSET + 111, "filler2", ItemClassification.filler), + "Heal": ItemData(SHIVERS_ITEM_ID_OFFSET + 112, "filler3", ItemClassification.filler) + +} diff --git a/worlds/shivers/Options.py b/worlds/shivers/Options.py new file mode 100644 index 000000000000..6d1880406910 --- /dev/null +++ b/worlds/shivers/Options.py @@ -0,0 +1,50 @@ +from Options import Choice, DefaultOnToggle, Toggle, PerGameCommonOptions +from dataclasses import dataclass + + +class LobbyAccess(Choice): + """Chooses how keys needed to reach the lobby are placed. + - Normal: Keys are placed anywhere + - Early: Keys are placed early + - Local: Keys are placed locally""" + display_name = "Lobby Access" + option_normal = 0 + option_early = 1 + option_local = 2 + +class PuzzleHintsRequired(DefaultOnToggle): + """If turned on puzzle hints will be available before the corresponding puzzle is required. For example: The Tiki + Drums puzzle will be placed after access to the security cameras which give you the solution. Turning this off + allows for greater randomization.""" + display_name = "Puzzle Hints Required" + +class InformationPlaques(Toggle): + """Adds Information Plaques as checks.""" + display_name = "Include Information Plaques" + +class FrontDoorUsable(Toggle): + """Adds a key to unlock the front door of the museum.""" + display_name = "Front Door Usable" + +class ElevatorsStaySolved(DefaultOnToggle): + """Adds elevators as checks and will remain open upon solving them.""" + display_name = "Elevators Stay Solved" + +class EarlyBeth(DefaultOnToggle): + """Beth's body is open at the start of the game. This allows any pot piece to be placed in the slide and early checks on the second half of the final riddle.""" + display_name = "Early Beth" + +class EarlyLightning(Toggle): + """Allows lightning to be captured at any point in the game. You will still need to capture all ten Ixupi for victory.""" + display_name = "Early Lightning" + + +@dataclass +class ShiversOptions(PerGameCommonOptions): + lobby_access: LobbyAccess + puzzle_hints_required: PuzzleHintsRequired + include_information_plaques: InformationPlaques + front_door_usable: FrontDoorUsable + elevators_stay_solved: ElevatorsStaySolved + early_beth: EarlyBeth + early_lightning: EarlyLightning diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py new file mode 100644 index 000000000000..fdd260ca91aa --- /dev/null +++ b/worlds/shivers/Rules.py @@ -0,0 +1,228 @@ +from typing import Dict, List, TYPE_CHECKING +from collections.abc import Callable +from BaseClasses import CollectionState +from worlds.generic.Rules import forbid_item + +if TYPE_CHECKING: + from . import ShiversWorld + + +def water_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Lobby", "Region", player) or (state.can_reach("Janitor Closet", "Region", player) and cloth_capturable(state, player))) \ + and state.has_all({"Water Pot Bottom", "Water Pot Top", "Water Pot Bottom DUPE", "Water Pot Top DUPE"}, player) + + +def wax_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Library", "Region", player) or state.can_reach("Anansi", "Region", player)) \ + and state.has_all({"Wax Pot Bottom", "Wax Pot Top", "Wax Pot Bottom DUPE", "Wax Pot Top DUPE"}, player) + + +def ash_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Office", "Region", player) or state.can_reach("Burial", "Region", player)) \ + and state.has_all({"Ash Pot Bottom", "Ash Pot Top", "Ash Pot Bottom DUPE", "Ash Pot Top DUPE"}, player) + + +def oil_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Prehistoric", "Region", player) or state.can_reach("Tar River", "Region", player)) \ + and state.has_all({"Oil Pot Bottom", "Oil Pot Top", "Oil Pot Bottom DUPE", "Oil Pot Top DUPE"}, player) + + +def cloth_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Egypt", "Region", player) or state.can_reach("Burial", "Region", player) or state.can_reach("Janitor Closet", "Region", player)) \ + and state.has_all({"Cloth Pot Bottom", "Cloth Pot Top", "Cloth Pot Bottom DUPE", "Cloth Pot Top DUPE"}, player) + + +def wood_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Workshop", "Region", player) or state.can_reach("Blue Maze", "Region", player) or state.can_reach("Gods Room", "Region", player) or state.can_reach("Anansi", "Region", player)) \ + and state.has_all({"Wood Pot Bottom", "Wood Pot Top", "Wood Pot Bottom DUPE", "Wood Pot Top DUPE"}, player) + + +def crystal_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Lobby", "Region", player) or state.can_reach("Ocean", "Region", player)) \ + and state.has_all({"Crystal Pot Bottom", "Crystal Pot Top", "Crystal Pot Bottom DUPE", "Crystal Pot Top DUPE"}, player) + + +def sand_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Greenhouse", "Region", player) or state.can_reach("Ocean", "Region", player)) \ + and state.has_all({"Sand Pot Bottom", "Sand Pot Top", "Sand Pot Bottom DUPE", "Sand Pot Top DUPE"}, player) + + +def metal_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Projector Room", "Region", player) or state.can_reach("Prehistoric", "Region", player) or state.can_reach("Bedroom", "Region", player)) \ + and state.has_all({"Metal Pot Bottom", "Metal Pot Top", "Metal Pot Bottom DUPE", "Metal Pot Top DUPE"}, player) + + +def lightning_capturable(state: CollectionState, player: int) -> bool: + return (first_nine_ixupi_capturable or state.multiworld.early_lightning[player].value) \ + and state.can_reach("Generator", "Region", player) \ + and state.has_all({"Lightning Pot Bottom", "Lightning Pot Top", "Lightning Pot Bottom DUPE", "Lightning Pot Top DUPE"}, player) + + +def beths_body_available(state: CollectionState, player: int) -> bool: + return (first_nine_ixupi_capturable(state, player) or state.multiworld.early_beth[player].value) \ + and state.can_reach("Generator", "Region", player) + + +def first_nine_ixupi_capturable(state: CollectionState, player: int) -> bool: + return water_capturable(state, player) and wax_capturable(state, player) \ + and ash_capturable(state, player) and oil_capturable(state, player) \ + and cloth_capturable(state, player) and wood_capturable(state, player) \ + and crystal_capturable(state, player) and sand_capturable(state, player) \ + and metal_capturable(state, player) + + +def get_rules_lookup(player: int): + rules_lookup: Dict[str, List[Callable[[CollectionState], bool]]] = { + "entrances": { + "To Office Elevator From Underground Blue Tunnels": lambda state: state.has("Key for Office Elevator", player), + "To Office Elevator From Office": lambda state: state.has("Key for Office Elevator", player), + "To Bedroom Elevator From Office": lambda state: state.has_all({"Key for Bedroom Elevator", "Crawling"}, player), + "To Office From Bedroom Elevator": lambda state: state.has_all({"Key for Bedroom Elevator", "Crawling"}, player), + "To Three Floor Elevator From Maintenance Tunnels": lambda state: state.has("Key for Three Floor Elevator", player), + "To Three Floor Elevator From Blue Maze Bottom": lambda state: state.has("Key for Three Floor Elevator", player), + "To Three Floor Elevator From Blue Maze Top": lambda state: state.has("Key for Three Floor Elevator", player), + "To Workshop": lambda state: state.has("Key for Workshop", player), + "To Lobby From Office": lambda state: state.has("Key for Office", player), + "To Office From Lobby": lambda state: state.has("Key for Office", player), + "To Library From Lobby": lambda state: state.has("Key for Library Room", player), + "To Lobby From Library": lambda state: state.has("Key for Library Room", player), + "To Prehistoric From Lobby": lambda state: state.has("Key for Prehistoric Room", player), + "To Lobby From Prehistoric": lambda state: state.has("Key for Prehistoric Room", player), + "To Greenhouse": lambda state: state.has("Key for Greenhouse Room", player), + "To Ocean From Prehistoric": lambda state: state.has("Key for Ocean Room", player), + "To Prehistoric From Ocean": lambda state: state.has("Key for Ocean Room", player), + "To Projector Room": lambda state: state.has("Key for Projector Room", player), + "To Generator": lambda state: state.has("Key for Generator Room", player), + "To Lobby From Egypt": lambda state: state.has("Key for Egypt Room", player), + "To Egypt From Lobby": lambda state: state.has("Key for Egypt Room", player), + "To Janitor Closet": lambda state: state.has("Key for Janitor Closet", player), + "To Tiki From Burial": lambda state: state.has("Key for Tiki Room", player), + "To Burial From Tiki": lambda state: state.has("Key for Tiki Room", player), + "To Inventions From UFO": lambda state: state.has("Key for UFO Room", player), + "To UFO From Inventions": lambda state: state.has("Key for UFO Room", player), + "To Torture From Inventions": lambda state: state.has("Key for Torture Room", player), + "To Inventions From Torture": lambda state: state.has("Key for Torture Room", player), + "To Torture": lambda state: state.has("Key for Puzzle Room", player), + "To Puzzle Room Mastermind From Torture": lambda state: state.has("Key for Puzzle Room", player), + "To Bedroom": lambda state: state.has("Key for Bedroom", player), + "To Underground Lake From Underground Tunnels": lambda state: state.has("Key for Underground Lake Room", player), + "To Underground Tunnels From Underground Lake": lambda state: state.has("Key for Underground Lake Room", player), + "To Outside From Lobby": lambda state: state.has("Key for Front Door", player), + "To Lobby From Outside": lambda state: state.has("Key for Front Door", player), + "To Maintenance Tunnels From Theater Back Hallways": lambda state: state.has("Crawling", player), + "To Blue Maze From Egypt": lambda state: state.has("Crawling", player), + "To Egypt From Blue Maze": lambda state: state.has("Crawling", player), + "To Lobby From Tar River": lambda state: (state.has("Crawling", player) and oil_capturable(state, player)), + "To Tar River From Lobby": lambda state: (state.has("Crawling", player) and oil_capturable(state, player) and state.can_reach("Tar River", "Region", player)), + "To Burial From Egypt": lambda state: state.can_reach("Egypt", "Region", player), + "To Gods Room From Anansi": lambda state: state.can_reach("Gods Room", "Region", player), + "To Slide Room": lambda state: ( + state.can_reach("Prehistoric", "Region", player) and state.can_reach("Tar River", "Region",player) and + state.can_reach("Egypt", "Region", player) and state.can_reach("Burial", "Region", player) and + state.can_reach("Gods Room", "Region", player) and state.can_reach("Werewolf", "Region", player)), + "To Lobby From Slide Room": lambda state: (beths_body_available(state, player)) + }, + "locations_required": { + "Puzzle Solved Anansi Musicbox": lambda state: state.can_reach("Clock Tower", "Region", player), + "Accessible: Storage: Janitor Closet": lambda state: cloth_capturable(state, player), + "Accessible: Storage: Tar River": lambda state: oil_capturable(state, player), + "Accessible: Storage: Theater": lambda state: state.can_reach("Projector Room", "Region", player), + "Accessible: Storage: Slide": lambda state: beths_body_available(state, player) and state.can_reach("Slide Room", "Region", player), + "Ixupi Captured Water": lambda state: water_capturable(state, player), + "Ixupi Captured Wax": lambda state: wax_capturable(state, player), + "Ixupi Captured Ash": lambda state: ash_capturable(state, player), + "Ixupi Captured Oil": lambda state: oil_capturable(state, player), + "Ixupi Captured Cloth": lambda state: cloth_capturable(state, player), + "Ixupi Captured Wood": lambda state: wood_capturable(state, player), + "Ixupi Captured Crystal": lambda state: crystal_capturable(state, player), + "Ixupi Captured Sand": lambda state: sand_capturable(state, player), + "Ixupi Captured Metal": lambda state: metal_capturable(state, player), + "Final Riddle: Planets Aligned": lambda state: state.can_reach("Fortune Teller", "Region", player), + "Final Riddle: Norse God Stone Message": lambda state: (state.can_reach("Fortune Teller", "Region", player) and state.can_reach("UFO", "Region", player)), + "Final Riddle: Beth's Body Page 17": lambda state: beths_body_available(state, player), + "Final Riddle: Guillotine Dropped": lambda state: beths_body_available(state, player), + }, + "locations_puzzle_hints": { + "Puzzle Solved Clock Tower Door": lambda state: state.can_reach("Three Floor Elevator", "Region", player), + "Puzzle Solved Clock Chains": lambda state: state.can_reach("Bedroom", "Region", player), + "Puzzle Solved Tiki Drums": lambda state: state.can_reach("Clock Tower", "Region", player), + "Puzzle Solved Red Door": lambda state: state.can_reach("Maintenance Tunnels", "Region", player), + "Puzzle Solved UFO Symbols": lambda state: state.can_reach("Library", "Region", player), + "Puzzle Solved Maze Door": lambda state: state.can_reach("Projector Room", "Region", player), + "Puzzle Solved Theater Door": lambda state: state.can_reach("Underground Lake", "Region", player), + "Puzzle Solved Columns of RA": lambda state: state.can_reach("Underground Lake", "Region", player), + "Final Riddle: Guillotine Dropped": lambda state: state.can_reach("Underground Lake", "Region", player) + }, + "elevators": { + "Puzzle Solved Underground Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player) + and state.has("Key for Office Elevator", player))), + "Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)), + "Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player) + and state.has("Key for Three Floor Elevator", player))) + }, + "lightning": { + "Ixupi Captured Lightning": lambda state: lightning_capturable(state, player) + } + } + return rules_lookup + + +def set_rules(world: "ShiversWorld") -> None: + multiworld = world.multiworld + player = world.player + + rules_lookup = get_rules_lookup(player) + # Set required entrance rules + for entrance_name, rule in rules_lookup["entrances"].items(): + multiworld.get_entrance(entrance_name, player).access_rule = rule + + # Set required location rules + for location_name, rule in rules_lookup["locations_required"].items(): + multiworld.get_location(location_name, player).access_rule = rule + + # Set option location rules + if world.options.puzzle_hints_required.value: + for location_name, rule in rules_lookup["locations_puzzle_hints"].items(): + multiworld.get_location(location_name, player).access_rule = rule + if world.options.elevators_stay_solved.value: + for location_name, rule in rules_lookup["elevators"].items(): + multiworld.get_location(location_name, player).access_rule = rule + if world.options.early_lightning.value: + for location_name, rule in rules_lookup["lightning"].items(): + multiworld.get_location(location_name, player).access_rule = rule + + # forbid cloth in janitor closet and oil in tar river + forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Bottom DUPE", player) + forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Top DUPE", player) + forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Bottom DUPE", player) + forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Top DUPE", player) + + # Filler Item Forbids + forbid_item(multiworld.get_location("Puzzle Solved Lyre", player), "Easier Lyre", player) + forbid_item(multiworld.get_location("Ixupi Captured Water", player), "Water Always Available in Lobby", player) + forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Library", player) + forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Anansi Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Tiki Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Ash", player), "Ash Always Available in Office", player) + forbid_item(multiworld.get_location("Ixupi Captured Ash", player), "Ash Always Available in Burial Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Oil", player), "Oil Always Available in Prehistoric Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Cloth", player), "Cloth Always Available in Egypt", player) + forbid_item(multiworld.get_location("Ixupi Captured Cloth", player), "Cloth Always Available in Burial Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Workshop", player) + forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Blue Maze", player) + forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Pegasus Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Gods Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Crystal", player), "Crystal Always Available in Lobby", player) + forbid_item(multiworld.get_location("Ixupi Captured Crystal", player), "Crystal Always Available in Ocean", player) + forbid_item(multiworld.get_location("Ixupi Captured Sand", player), "Sand Always Available in Plants Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Sand", player), "Sand Always Available in Ocean", player) + forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Projector Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Bedroom", player) + forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Prehistoric", player) + + # Set completion condition + multiworld.completion_condition[player] = lambda state: (first_nine_ixupi_capturable(state, player) and lightning_capturable(state, player)) + + + + diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py new file mode 100644 index 000000000000..e43e91fb5ae3 --- /dev/null +++ b/worlds/shivers/__init__.py @@ -0,0 +1,178 @@ +from .Items import item_table, ShiversItem +from .Rules import set_rules +from BaseClasses import Item, Tutorial, Region, Location +from Fill import fill_restrictive +from worlds.AutoWorld import WebWorld, World +from . import Constants, Rules +from .Options import ShiversOptions + + +class ShiversWeb(WebWorld): + tutorials = [Tutorial( + "Shivers Setup Guide", + "A guide to setting up Shivers for Multiworld.", + "English", + "setup_en.md", + "setup/en", + ["GodlFire", "Mathx2"] + )] + +class ShiversWorld(World): + """ + Shivers is a horror themed point and click adventure. Explore the mysteries of Windlenot's Museum of the Strange and Unusual. + """ + + game: str = "Shivers" + topology_present = False + web = ShiversWeb() + options_dataclass = ShiversOptions + options: ShiversOptions + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = Constants.location_name_to_id + + def create_item(self, name: str) -> Item: + data = item_table[name] + return ShiversItem(name, data.classification, data.code, self.player) + + def create_event(self, region_name: str, event_name: str) -> None: + region = self.multiworld.get_region(region_name, self.player) + loc = ShiversLocation(self.player, event_name, None, region) + loc.place_locked_item(self.create_event_item(event_name)) + region.locations.append(loc) + + def create_regions(self) -> None: + # Create regions + for region_name, exits in Constants.region_info["regions"]: + r = Region(region_name, self.player, self.multiworld) + self.multiworld.regions.append(r) + for exit_name in exits: + r.create_exit(exit_name) + + + # Bind mandatory connections + for entr_name, region_name in Constants.region_info["mandatory_connections"]: + e = self.multiworld.get_entrance(entr_name, self.player) + r = self.multiworld.get_region(region_name, self.player) + e.connect(r) + + # Locations + # Build exclusion list + self.removed_locations = set() + if not self.options.include_information_plaques: + self.removed_locations.update(Constants.exclusion_info["plaques"]) + if not self.options.elevators_stay_solved: + self.removed_locations.update(Constants.exclusion_info["elevators"]) + if not self.options.early_lightning: + self.removed_locations.update(Constants.exclusion_info["lightning"]) + + # Add locations + for region_name, locations in Constants.location_info["locations_by_region"].items(): + region = self.multiworld.get_region(region_name, self.player) + for loc_name in locations: + if loc_name not in self.removed_locations: + loc = ShiversLocation(self.player, loc_name, self.location_name_to_id.get(loc_name, None), region) + region.locations.append(loc) + + def create_items(self) -> None: + #Add items to item pool + itempool = [] + for name, data in item_table.items(): + if data.type in {"pot", "key", "ability", "filler2"}: + itempool.append(self.create_item(name)) + + #Add Filler + itempool += [self.create_item("Easier Lyre") for i in range(9)] + + #Extra filler is random between Heals and Easier Lyre. Heals weighted 95%. + filler_needed = len(self.multiworld.get_unfilled_locations(self.player)) - 24 - len(itempool) + itempool += [self.random.choices([self.create_item("Heal"), self.create_item("Easier Lyre")], weights=[95, 5])[0] for i in range(filler_needed)] + + + #Place library escape items. Choose a location to place the escape item + library_region = self.multiworld.get_region("Library", self.player) + librarylocation = self.random.choice([loc for loc in library_region.locations if not loc.name.startswith("Accessible:")]) + + #Roll for which escape items will be placed in the Library + library_random = self.random.randint(1, 3) + if library_random == 1: + librarylocation.place_locked_item(self.create_item("Crawling")) + + itempool = [item for item in itempool if item.name != "Crawling"] + + elif library_random == 2: + librarylocation.place_locked_item(self.create_item("Key for Library Room")) + + itempool = [item for item in itempool if item.name != "Key for Library Room"] + elif library_random == 3: + librarylocation.place_locked_item(self.create_item("Key for Three Floor Elevator")) + + librarylocationkeytwo = self.random.choice([loc for loc in library_region.locations if not loc.name.startswith("Accessible:") and loc != librarylocation]) + librarylocationkeytwo.place_locked_item(self.create_item("Key for Egypt Room")) + + itempool = [item for item in itempool if item.name not in ["Key for Three Floor Elevator", "Key for Egypt Room"]] + + #If front door option is on, determine which set of keys will be used for lobby access and add front door key to item pool + lobby_access_keys = 1 + if self.options.front_door_usable: + lobby_access_keys = self.random.randint(1, 2) + itempool += [self.create_item("Key for Front Door")] + else: + itempool += [self.create_item("Heal")] + + self.multiworld.itempool += itempool + + #Lobby acess: + if self.options.lobby_access == 1: + if lobby_access_keys == 1: + self.multiworld.early_items[self.player]["Key for Underground Lake Room"] = 1 + self.multiworld.early_items[self.player]["Key for Office Elevator"] = 1 + self.multiworld.early_items[self.player]["Key for Office"] = 1 + elif lobby_access_keys == 2: + self.multiworld.early_items[self.player]["Key for Front Door"] = 1 + if self.options.lobby_access == 2: + if lobby_access_keys == 1: + self.multiworld.local_early_items[self.player]["Key for Underground Lake Room"] = 1 + self.multiworld.local_early_items[self.player]["Key for Office Elevator"] = 1 + self.multiworld.local_early_items[self.player]["Key for Office"] = 1 + elif lobby_access_keys == 2: + self.multiworld.local_early_items[self.player]["Key for Front Door"] = 1 + + def pre_fill(self) -> None: + # Prefills event storage locations with duplicate pots + storagelocs = [] + storageitems = [] + self.storage_placements = [] + + for locations in Constants.location_info["locations_by_region"].values(): + for loc_name in locations: + if loc_name.startswith("Accessible: "): + storagelocs.append(self.multiworld.get_location(loc_name, self.player)) + + storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate'] + storageitems += [self.create_item("Empty") for i in range(3)] + + state = self.multiworld.get_all_state(True) + + self.random.shuffle(storagelocs) + self.random.shuffle(storageitems) + + fill_restrictive(self.multiworld, state, storagelocs.copy(), storageitems, True, True) + + self.storage_placements = {location.name: location.item.name for location in storagelocs} + + set_rules = set_rules + + def fill_slot_data(self) -> dict: + + return { + "storageplacements": self.storage_placements, + "excludedlocations": {str(excluded_location).replace('ExcludeLocations(', '').replace(')', '') for excluded_location in self.multiworld.exclude_locations.values()}, + "elevatorsstaysolved": {self.options.elevators_stay_solved.value}, + "earlybeth": {self.options.early_beth.value}, + "earlylightning": {self.options.early_lightning.value}, + } + + +class ShiversLocation(Location): + game = "Shivers" diff --git a/worlds/shivers/data/excluded_locations.json b/worlds/shivers/data/excluded_locations.json new file mode 100644 index 000000000000..6ed625077af8 --- /dev/null +++ b/worlds/shivers/data/excluded_locations.json @@ -0,0 +1,52 @@ +{ + "plaques": [ + "Information Plaque: Transforming Masks (Lobby)", + "Information Plaque: Jade Skull (Lobby)", + "Information Plaque: Bronze Unicorn (Prehistoric)", + "Information Plaque: Griffin (Prehistoric)", + "Information Plaque: Eagles Nest (Prehistoric)", + "Information Plaque: Large Spider (Prehistoric)", + "Information Plaque: Starfish (Prehistoric)", + "Information Plaque: Quartz Crystal (Ocean)", + "Information Plaque: Poseidon (Ocean)", + "Information Plaque: Colossus of Rhodes (Ocean)", + "Information Plaque: Poseidon's Temple (Ocean)", + "Information Plaque: Subterranean World (Underground Maze)", + "Information Plaque: Dero (Underground Maze)", + "Information Plaque: Tomb of the Ixupi (Egypt)", + "Information Plaque: The Sphinx (Egypt)", + "Information Plaque: Curse of Anubis (Egypt)", + "Information Plaque: Norse Burial Ship (Burial)", + "Information Plaque: Paracas Burial Bundles (Burial)", + "Information Plaque: Spectacular Coffins of Ghana (Burial)", + "Information Plaque: Cremation (Burial)", + "Information Plaque: Animal Crematorium (Burial)", + "Information Plaque: Witch Doctors of the Congo (Tiki)", + "Information Plaque: Sarombe doctor of Mozambique (Tiki)", + "Information Plaque: Fisherman's Canoe God (Gods)", + "Information Plaque: Mayan Gods (Gods)", + "Information Plaque: Thor (Gods)", + "Information Plaque: Celtic Janus Sculpture (Gods)", + "Information Plaque: Sumerian Bull God - An (Gods)", + "Information Plaque: Sumerian Lyre (Gods)", + "Information Plaque: Chuen (Gods)", + "Information Plaque: African Creation Myth (Anansi)", + "Information Plaque: Apophis the Serpent (Anansi)", + "Information Plaque: Death (Anansi)", + "Information Plaque: Cyclops (Pegasus)", + "Information Plaque: Lycanthropy (Werewolf)", + "Information Plaque: Coincidence or Extraterrestrial Visits? (UFO)", + "Information Plaque: Planets (UFO)", + "Information Plaque: Astronomical Construction (UFO)", + "Information Plaque: Guillotine (Torture)", + "Information Plaque: Aliens (UFO)" + ], + "elevators": [ + "Puzzle Solved Underground Elevator", + "Puzzle Solved Bedroom Elevator", + "Puzzle Solved Three Floor Elevator" + ], + "lightning": [ + "Ixupi Captured Lightning" + ] +} \ No newline at end of file diff --git a/worlds/shivers/data/locations.json b/worlds/shivers/data/locations.json new file mode 100644 index 000000000000..7d031b886bff --- /dev/null +++ b/worlds/shivers/data/locations.json @@ -0,0 +1,325 @@ +{ + "all_locations": [ + "Puzzle Solved Gears", + "Puzzle Solved Stone Henge", + "Puzzle Solved Workshop Drawers", + "Puzzle Solved Library Statue", + "Puzzle Solved Theater Door", + "Puzzle Solved Clock Tower Door", + "Puzzle Solved Clock Chains", + "Puzzle Solved Atlantis", + "Puzzle Solved Organ", + "Puzzle Solved Maze Door", + "Puzzle Solved Columns of RA", + "Puzzle Solved Burial Door", + "Puzzle Solved Chinese Solitaire", + "Puzzle Solved Tiki Drums", + "Puzzle Solved Lyre", + "Puzzle Solved Red Door", + "Puzzle Solved Fortune Teller Door", + "Puzzle Solved Alchemy", + "Puzzle Solved UFO Symbols", + "Puzzle Solved Anansi Musicbox", + "Puzzle Solved Gallows", + "Puzzle Solved Mastermind", + "Puzzle Solved Marble Flipper", + "Puzzle Solved Skull Dial Door", + "Flashback Memory Obtained Beth's Ghost", + "Flashback Memory Obtained Merrick's Ghost", + "Flashback Memory Obtained Windlenot's Ghost", + "Flashback Memory Obtained Ancient Astrology", + "Flashback Memory Obtained Scrapbook", + "Flashback Memory Obtained Museum Brochure", + "Flashback Memory Obtained In Search of the Unexplained", + "Flashback Memory Obtained Egyptian Hieroglyphics Explained", + "Flashback Memory Obtained South American Pictographs", + "Flashback Memory Obtained Mythology of the Stars", + "Flashback Memory Obtained Black Book", + "Flashback Memory Obtained Theater Movie", + "Flashback Memory Obtained Museum Blueprints", + "Flashback Memory Obtained Beth's Address Book", + "Flashback Memory Obtained Merick's Notebook", + "Flashback Memory Obtained Professor Windlenot's Diary", + "Ixupi Captured Water", + "Ixupi Captured Wax", + "Ixupi Captured Ash", + "Ixupi Captured Oil", + "Ixupi Captured Cloth", + "Ixupi Captured Wood", + "Ixupi Captured Crystal", + "Ixupi Captured Sand", + "Ixupi Captured Metal", + "Final Riddle: Fortune Teller", + "Final Riddle: Planets Aligned", + "Final Riddle: Norse God Stone Message", + "Final Riddle: Beth's Body Page 17", + "Final Riddle: Guillotine Dropped", + "Puzzle Hint Found: Combo Lock in Mailbox", + "Puzzle Hint Found: Orange Symbol", + "Puzzle Hint Found: Silver Symbol", + "Puzzle Hint Found: Green Symbol", + "Puzzle Hint Found: White Symbol", + "Puzzle Hint Found: Brown Symbol", + "Puzzle Hint Found: Tan Symbol", + "Puzzle Hint Found: Basilisk Bone Fragments", + "Puzzle Hint Found: Atlantis Map", + "Puzzle Hint Found: Sirens Song Heard", + "Puzzle Hint Found: Egyptian Sphinx Heard", + "Puzzle Hint Found: Gallows Information Plaque", + "Puzzle Hint Found: Mastermind Information Plaque", + "Puzzle Hint Found: Elevator Writing", + "Puzzle Hint Found: Tiki Security Camera", + "Puzzle Hint Found: Tape Recorder Heard", + "Information Plaque: Transforming Masks (Lobby)", + "Information Plaque: Jade Skull (Lobby)", + "Information Plaque: Bronze Unicorn (Prehistoric)", + "Information Plaque: Griffin (Prehistoric)", + "Information Plaque: Eagles Nest (Prehistoric)", + "Information Plaque: Large Spider (Prehistoric)", + "Information Plaque: Starfish (Prehistoric)", + "Information Plaque: Quartz Crystal (Ocean)", + "Information Plaque: Poseidon (Ocean)", + "Information Plaque: Colossus of Rhodes (Ocean)", + "Information Plaque: Poseidon's Temple (Ocean)", + "Information Plaque: Subterranean World (Underground Maze)", + "Information Plaque: Dero (Underground Maze)", + "Information Plaque: Tomb of the Ixupi (Egypt)", + "Information Plaque: The Sphinx (Egypt)", + "Information Plaque: Curse of Anubis (Egypt)", + "Information Plaque: Norse Burial Ship (Burial)", + "Information Plaque: Paracas Burial Bundles (Burial)", + "Information Plaque: Spectacular Coffins of Ghana (Burial)", + "Information Plaque: Cremation (Burial)", + "Information Plaque: Animal Crematorium (Burial)", + "Information Plaque: Witch Doctors of the Congo (Tiki)", + "Information Plaque: Sarombe doctor of Mozambique (Tiki)", + "Information Plaque: Fisherman's Canoe God (Gods)", + "Information Plaque: Mayan Gods (Gods)", + "Information Plaque: Thor (Gods)", + "Information Plaque: Celtic Janus Sculpture (Gods)", + "Information Plaque: Sumerian Bull God - An (Gods)", + "Information Plaque: Sumerian Lyre (Gods)", + "Information Plaque: Chuen (Gods)", + "Information Plaque: African Creation Myth (Anansi)", + "Information Plaque: Apophis the Serpent (Anansi)", + "Information Plaque: Death (Anansi)", + "Information Plaque: Cyclops (Pegasus)", + "Information Plaque: Lycanthropy (Werewolf)", + "Information Plaque: Coincidence or Extraterrestrial Visits? (UFO)", + "Information Plaque: Planets (UFO)", + "Information Plaque: Astronomical Construction (UFO)", + "Information Plaque: Guillotine (Torture)", + "Information Plaque: Aliens (UFO)", + "Puzzle Solved Underground Elevator", + "Puzzle Solved Bedroom Elevator", + "Puzzle Solved Three Floor Elevator", + "Ixupi Captured Lightning" + ], + "locations_by_region": { + "Outside": [ + "Puzzle Solved Gears", + "Puzzle Solved Stone Henge", + "Ixupi Captured Water", + "Ixupi Captured Wax", + "Ixupi Captured Ash", + "Ixupi Captured Oil", + "Ixupi Captured Cloth", + "Ixupi Captured Wood", + "Ixupi Captured Crystal", + "Ixupi Captured Sand", + "Ixupi Captured Metal", + "Ixupi Captured Lightning", + "Puzzle Solved Underground Elevator", + "Puzzle Solved Three Floor Elevator", + "Puzzle Hint Found: Combo Lock in Mailbox", + "Puzzle Hint Found: Orange Symbol", + "Puzzle Hint Found: Silver Symbol", + "Puzzle Hint Found: Green Symbol", + "Puzzle Hint Found: White Symbol", + "Puzzle Hint Found: Brown Symbol", + "Puzzle Hint Found: Tan Symbol" + ], + "Underground Lake": [ + "Flashback Memory Obtained Windlenot's Ghost", + "Flashback Memory Obtained Egyptian Hieroglyphics Explained" + ], + "Office": [ + "Flashback Memory Obtained Scrapbook", + "Accessible: Storage: Desk Drawer", + "Puzzle Hint Found: Atlantis Map", + "Puzzle Hint Found: Tape Recorder Heard", + "Puzzle Solved Bedroom Elevator" + ], + "Workshop": [ + "Puzzle Solved Workshop Drawers", + "Accessible: Storage: Workshop Drawers", + "Puzzle Hint Found: Basilisk Bone Fragments" + ], + "Bedroom": [ + "Flashback Memory Obtained Professor Windlenot's Diary" + ], + "Library": [ + "Puzzle Solved Library Statue", + "Flashback Memory Obtained In Search of the Unexplained", + "Flashback Memory Obtained South American Pictographs", + "Flashback Memory Obtained Mythology of the Stars", + "Flashback Memory Obtained Black Book", + "Accessible: Storage: Library Cabinet", + "Accessible: Storage: Library Statue" + ], + "Maintenance Tunnels": [ + "Flashback Memory Obtained Beth's Address Book" + ], + "Three Floor Elevator": [ + "Puzzle Hint Found: Elevator Writing" + ], + "Lobby": [ + "Puzzle Solved Theater Door", + "Flashback Memory Obtained Museum Brochure", + "Information Plaque: Jade Skull (Lobby)", + "Information Plaque: Transforming Masks (Lobby)", + "Accessible: Storage: Slide", + "Accessible: Storage: Eagles Head" + ], + "Generator": [ + "Final Riddle: Beth's Body Page 17" + ], + "Theater Back Hallways": [ + "Puzzle Solved Clock Tower Door" + ], + "Clock Tower Staircase": [ + "Puzzle Solved Clock Chains" + ], + "Clock Tower": [ + "Flashback Memory Obtained Beth's Ghost", + "Accessible: Storage: Clock Tower", + "Puzzle Hint Found: Tiki Security Camera" + ], + "Projector Room": [ + "Flashback Memory Obtained Theater Movie" + ], + "Ocean": [ + "Puzzle Solved Atlantis", + "Puzzle Solved Organ", + "Flashback Memory Obtained Museum Blueprints", + "Accessible: Storage: Ocean", + "Puzzle Hint Found: Sirens Song Heard", + "Information Plaque: Quartz Crystal (Ocean)", + "Information Plaque: Poseidon (Ocean)", + "Information Plaque: Colossus of Rhodes (Ocean)", + "Information Plaque: Poseidon's Temple (Ocean)" + ], + "Maze Staircase": [ + "Puzzle Solved Maze Door" + ], + "Egypt": [ + "Puzzle Solved Columns of RA", + "Puzzle Solved Burial Door", + "Accessible: Storage: Egypt", + "Puzzle Hint Found: Egyptian Sphinx Heard", + "Information Plaque: Tomb of the Ixupi (Egypt)", + "Information Plaque: The Sphinx (Egypt)", + "Information Plaque: Curse of Anubis (Egypt)" + ], + "Burial": [ + "Puzzle Solved Chinese Solitaire", + "Flashback Memory Obtained Merick's Notebook", + "Accessible: Storage: Chinese Solitaire", + "Information Plaque: Norse Burial Ship (Burial)", + "Information Plaque: Paracas Burial Bundles (Burial)", + "Information Plaque: Spectacular Coffins of Ghana (Burial)", + "Information Plaque: Animal Crematorium (Burial)", + "Information Plaque: Cremation (Burial)" + ], + "Tiki": [ + "Puzzle Solved Tiki Drums", + "Accessible: Storage: Tiki Hut", + "Information Plaque: Witch Doctors of the Congo (Tiki)", + "Information Plaque: Sarombe doctor of Mozambique (Tiki)" + ], + "Gods Room": [ + "Puzzle Solved Lyre", + "Puzzle Solved Red Door", + "Accessible: Storage: Lyre", + "Final Riddle: Norse God Stone Message", + "Information Plaque: Fisherman's Canoe God (Gods)", + "Information Plaque: Mayan Gods (Gods)", + "Information Plaque: Thor (Gods)", + "Information Plaque: Celtic Janus Sculpture (Gods)", + "Information Plaque: Sumerian Bull God - An (Gods)", + "Information Plaque: Sumerian Lyre (Gods)", + "Information Plaque: Chuen (Gods)" + ], + "Blue Maze": [ + "Puzzle Solved Fortune Teller Door" + ], + "Fortune Teller": [ + "Flashback Memory Obtained Merrick's Ghost", + "Final Riddle: Fortune Teller" + ], + "Inventions": [ + "Puzzle Solved Alchemy", + "Accessible: Storage: Alchemy" + ], + "UFO": [ + "Puzzle Solved UFO Symbols", + "Accessible: Storage: UFO", + "Final Riddle: Planets Aligned", + "Information Plaque: Coincidence or Extraterrestrial Visits? (UFO)", + "Information Plaque: Planets (UFO)", + "Information Plaque: Astronomical Construction (UFO)", + "Information Plaque: Aliens (UFO)" + ], + "Anansi": [ + "Puzzle Solved Anansi Musicbox", + "Flashback Memory Obtained Ancient Astrology", + "Accessible: Storage: Skeleton", + "Accessible: Storage: Anansi", + "Information Plaque: African Creation Myth (Anansi)", + "Information Plaque: Apophis the Serpent (Anansi)", + "Information Plaque: Death (Anansi)", + "Information Plaque: Cyclops (Pegasus)", + "Information Plaque: Lycanthropy (Werewolf)" + ], + "Torture": [ + "Puzzle Solved Gallows", + "Accessible: Storage: Hanging", + "Final Riddle: Guillotine Dropped", + "Puzzle Hint Found: Gallows Information Plaque", + "Information Plaque: Guillotine (Torture)" + ], + "Puzzle Room Mastermind": [ + "Puzzle Solved Mastermind", + "Puzzle Hint Found: Mastermind Information Plaque" + ], + "Puzzle Room Marbles": [ + "Puzzle Solved Marble Flipper" + ], + "Prehistoric": [ + "Information Plaque: Bronze Unicorn (Prehistoric)", + "Information Plaque: Griffin (Prehistoric)", + "Information Plaque: Eagles Nest (Prehistoric)", + "Information Plaque: Large Spider (Prehistoric)", + "Information Plaque: Starfish (Prehistoric)", + "Accessible: Storage: Eagles Nest" + ], + "Tar River": [ + "Accessible: Storage: Tar River", + "Information Plaque: Subterranean World (Underground Maze)", + "Information Plaque: Dero (Underground Maze)" + ], + "Theater": [ + "Accessible: Storage: Theater" + ], + "Greenhouse": [ + "Accessible: Storage: Greenhouse" + ], + "Janitor Closet": [ + "Accessible: Storage: Janitor Closet" + ], + "Skull Dial Bridge": [ + "Accessible: Storage: Skull Bridge", + "Puzzle Solved Skull Dial Door" + ] + } +} diff --git a/worlds/shivers/data/regions.json b/worlds/shivers/data/regions.json new file mode 100644 index 000000000000..3e81136c45f8 --- /dev/null +++ b/worlds/shivers/data/regions.json @@ -0,0 +1,145 @@ +{ + "regions": [ + ["Menu", ["To Registry"]], + ["Registry", ["To Outside From Registry"]], + ["Outside", ["To Underground Tunnels From Outside", "To Lobby From Outside"]], + ["Underground Tunnels", ["To Underground Lake From Underground Tunnels", "To Outside From Underground"]], + ["Underground Lake", ["To Underground Tunnels From Underground Lake", "To Underground Blue Tunnels From Underground Lake"]], + ["Underground Blue Tunnels", ["To Underground Lake From Underground Blue Tunnels", "To Office Elevator From Underground Blue Tunnels"]], + ["Office Elevator", ["To Underground Blue Tunnels From Office Elevator","To Office From Office Elevator"]], + ["Office", ["To Office Elevator From Office", "To Workshop", "To Lobby From Office", "To Bedroom Elevator From Office"]], + ["Workshop", ["To Office From Workshop"]], + ["Bedroom Elevator", ["To Office From Bedroom Elevator", "To Bedroom"]], + ["Bedroom", ["To Bedroom Elevator From Bedroom"]], + ["Lobby", ["To Office From Lobby", "To Library From Lobby", "To Theater From Lobby", "To Prehistoric From Lobby", "To Egypt From Lobby", "To Tar River From Lobby", "To Outside From Lobby"]], + ["Library", ["To Lobby From Library", "To Maintenance Tunnels From Library"]], + ["Maintenance Tunnels", ["To Library From Maintenance Tunnels", "To Three Floor Elevator From Maintenance Tunnels", "To Generator"]], + ["Generator", ["To Maintenance Tunnels From Generator"]], + ["Theater", ["To Lobby From Theater", "To Theater Back Hallways From Theater"]], + ["Theater Back Hallways", ["To Theater From Theater Back Hallways", "To Clock Tower Staircase From Theater Back Hallways", "To Maintenance Tunnels From Theater Back Hallways", "To Projector Room"]], + ["Clock Tower Staircase", ["To Theater Back Hallways From Clock Tower Staircase", "To Clock Tower"]], + ["Clock Tower", ["To Clock Tower Staircase From Clock Tower"]], + ["Projector Room", ["To Theater Back Hallways From Projector Room"]], + ["Prehistoric", ["To Lobby From Prehistoric", "To Greenhouse", "To Ocean From Prehistoric"]], + ["Greenhouse", ["To Prehistoric From Greenhouse"]], + ["Ocean", ["To Prehistoric From Ocean", "To Maze Staircase From Ocean"]], + ["Maze Staircase", ["To Ocean From Maze Staircase", "To Maze From Maze Staircase"]], + ["Maze", ["To Maze Staircase From Maze", "To Tar River"]], + ["Tar River", ["To Maze From Tar River", "To Lobby From Tar River"]], + ["Egypt", ["To Lobby From Egypt", "To Burial From Egypt", "To Blue Maze From Egypt"]], + ["Burial", ["To Egypt From Burial", "To Tiki From Burial"]], + ["Tiki", ["To Burial From Tiki", "To Gods Room"]], + ["Gods Room", ["To Tiki From Gods Room", "To Anansi From Gods Room"]], + ["Anansi", ["To Gods Room From Anansi", "To Werewolf From Anansi"]], + ["Werewolf", ["To Anansi From Werewolf", "To Night Staircase From Werewolf"]], + ["Night Staircase", ["To Werewolf From Night Staircase", "To Janitor Closet", "To UFO"]], + ["Janitor Closet", ["To Night Staircase From Janitor Closet"]], + ["UFO", ["To Night Staircase From UFO", "To Inventions From UFO"]], + ["Blue Maze", ["To Egypt From Blue Maze", "To Three Floor Elevator From Blue Maze Bottom", "To Three Floor Elevator From Blue Maze Top", "To Fortune Teller", "To Inventions From Blue Maze"]], + ["Three Floor Elevator", ["To Maintenance Tunnels From Three Floor Elevator", "To Blue Maze From Three Floor Elevator"]], + ["Fortune Teller", ["To Blue Maze From Fortune Teller"]], + ["Inventions", ["To Blue Maze From Inventions", "To UFO From Inventions", "To Torture From Inventions"]], + ["Torture", ["To Inventions From Torture", "To Puzzle Room Mastermind From Torture"]], + ["Puzzle Room Mastermind", ["To Torture", "To Puzzle Room Marbles From Puzzle Room Mastermind"]], + ["Puzzle Room Marbles", ["To Puzzle Room Mastermind From Puzzle Room Marbles", "To Skull Dial Bridge From Puzzle Room Marbles"]], + ["Skull Dial Bridge", ["To Puzzle Room Marbles From Skull Dial Bridge", "To Slide Room"]], + ["Slide Room", ["To Skull Dial Bridge From Slide Room", "To Lobby From Slide Room"]] + ], + "mandatory_connections": [ + ["To Registry", "Registry"], + ["To Outside From Registry", "Outside"], + ["To Outside From Underground", "Outside"], + ["To Outside From Lobby", "Outside"], + ["To Underground Tunnels From Outside", "Underground Tunnels"], + ["To Underground Tunnels From Underground Lake", "Underground Tunnels"], + ["To Underground Lake From Underground Tunnels", "Underground Lake"], + ["To Underground Lake From Underground Blue Tunnels", "Underground Lake"], + ["To Underground Blue Tunnels From Underground Lake", "Underground Blue Tunnels"], + ["To Underground Blue Tunnels From Office Elevator", "Underground Blue Tunnels"], + ["To Office Elevator From Underground Blue Tunnels", "Office Elevator"], + ["To Office Elevator From Office", "Office Elevator"], + ["To Office From Office Elevator", "Office"], + ["To Office From Workshop", "Office"], + ["To Office From Bedroom Elevator", "Office"], + ["To Office From Lobby", "Office"], + ["To Workshop", "Workshop"], + ["To Lobby From Office", "Lobby"], + ["To Lobby From Library", "Lobby"], + ["To Lobby From Tar River", "Lobby"], + ["To Lobby From Slide Room", "Lobby"], + ["To Lobby From Egypt", "Lobby"], + ["To Lobby From Theater", "Lobby"], + ["To Lobby From Prehistoric", "Lobby"], + ["To Lobby From Outside", "Lobby"], + ["To Bedroom Elevator From Office", "Bedroom Elevator"], + ["To Bedroom Elevator From Bedroom", "Bedroom Elevator"], + ["To Bedroom", "Bedroom"], + ["To Library From Lobby", "Library"], + ["To Library From Maintenance Tunnels", "Library"], + ["To Theater From Lobby", "Theater" ], + ["To Theater From Theater Back Hallways", "Theater"], + ["To Prehistoric From Lobby", "Prehistoric"], + ["To Prehistoric From Greenhouse", "Prehistoric"], + ["To Prehistoric From Ocean", "Prehistoric"], + ["To Egypt From Lobby", "Egypt"], + ["To Egypt From Burial", "Egypt"], + ["To Egypt From Blue Maze", "Egypt"], + ["To Maintenance Tunnels From Generator", "Maintenance Tunnels"], + ["To Maintenance Tunnels From Three Floor Elevator", "Maintenance Tunnels"], + ["To Maintenance Tunnels From Library", "Maintenance Tunnels"], + ["To Maintenance Tunnels From Theater Back Hallways", "Maintenance Tunnels"], + ["To Three Floor Elevator From Maintenance Tunnels", "Three Floor Elevator"], + ["To Three Floor Elevator From Blue Maze Bottom", "Three Floor Elevator"], + ["To Three Floor Elevator From Blue Maze Top", "Three Floor Elevator"], + ["To Generator", "Generator"], + ["To Theater Back Hallways From Theater", "Theater Back Hallways"], + ["To Theater Back Hallways From Clock Tower Staircase", "Theater Back Hallways"], + ["To Theater Back Hallways From Projector Room", "Theater Back Hallways"], + ["To Clock Tower Staircase From Theater Back Hallways", "Clock Tower Staircase"], + ["To Clock Tower Staircase From Clock Tower", "Clock Tower Staircase"], + ["To Projector Room", "Projector Room"], + ["To Clock Tower", "Clock Tower"], + ["To Greenhouse", "Greenhouse"], + ["To Ocean From Prehistoric", "Ocean"], + ["To Ocean From Maze Staircase", "Ocean"], + ["To Maze Staircase From Ocean", "Maze Staircase"], + ["To Maze Staircase From Maze", "Maze Staircase"], + ["To Maze From Maze Staircase", "Maze"], + ["To Maze From Tar River", "Maze"], + ["To Tar River", "Tar River"], + ["To Tar River From Lobby", "Tar River"], + ["To Burial From Egypt", "Burial"], + ["To Burial From Tiki", "Burial"], + ["To Blue Maze From Three Floor Elevator", "Blue Maze"], + ["To Blue Maze From Fortune Teller", "Blue Maze"], + ["To Blue Maze From Inventions", "Blue Maze"], + ["To Blue Maze From Egypt", "Blue Maze"], + ["To Tiki From Burial", "Tiki"], + ["To Tiki From Gods Room", "Tiki"], + ["To Gods Room", "Gods Room" ], + ["To Gods Room From Anansi", "Gods Room"], + ["To Anansi From Gods Room", "Anansi"], + ["To Anansi From Werewolf", "Anansi"], + ["To Werewolf From Anansi", "Werewolf"], + ["To Werewolf From Night Staircase", "Werewolf"], + ["To Night Staircase From Werewolf", "Night Staircase"], + ["To Night Staircase From Janitor Closet", "Night Staircase"], + ["To Night Staircase From UFO", "Night Staircase"], + ["To Janitor Closet", "Janitor Closet"], + ["To UFO", "UFO"], + ["To UFO From Inventions", "UFO"], + ["To Inventions From UFO", "Inventions"], + ["To Inventions From Blue Maze", "Inventions"], + ["To Inventions From Torture", "Inventions"], + ["To Fortune Teller", "Fortune Teller"], + ["To Torture", "Torture"], + ["To Torture From Inventions", "Torture"], + ["To Puzzle Room Mastermind From Torture", "Puzzle Room Mastermind"], + ["To Puzzle Room Mastermind From Puzzle Room Marbles", "Puzzle Room Mastermind"], + ["To Puzzle Room Marbles From Puzzle Room Mastermind", "Puzzle Room Marbles"], + ["To Puzzle Room Marbles From Skull Dial Bridge", "Puzzle Room Marbles"], + ["To Skull Dial Bridge From Puzzle Room Marbles", "Skull Dial Bridge"], + ["To Skull Dial Bridge From Slide Room", "Skull Dial Bridge"], + ["To Slide Room", "Slide Room"] + ] +} \ No newline at end of file diff --git a/worlds/shivers/docs/en_Shivers.md b/worlds/shivers/docs/en_Shivers.md new file mode 100644 index 000000000000..51730057b034 --- /dev/null +++ b/worlds/shivers/docs/en_Shivers.md @@ -0,0 +1,31 @@ +# Shivers + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +configuration file. + +## What does randomization do to this game? + +All Ixupi pot pieces are randomized. Keys have been added to the game to lock off different rooms in the museum, +these are randomized. Crawling has been added and is required to use any crawl space. + +## What is considered a location check in Shivers? + +1. All puzzle solves are location checks excluding elevator puzzles. +2. All Ixupi captures are location checks excluding Lightning. +3. Puzzle hints/solutions are location checks. For example, looking at the Atlantis map. +4. Optionally information plaques are location checks. + +## When the player receives an item, what happens? + +If the player receives a key then the corresponding door will be unlocked. If the player receives a pot piece, it is placed into a pot piece storage location. + +## What is the victory condition? + +Victory is achieved when the player captures Lightning in the generator room. + +## Encountered a bug? + +Please contact GodlFire on Discord for bugs related to Shivers world generation.\ +Please contact GodlFire or mouse on Discord for bugs related to the Shivers Randomizer. diff --git a/worlds/shivers/docs/setup_en.md b/worlds/shivers/docs/setup_en.md new file mode 100644 index 000000000000..ee33bb70408e --- /dev/null +++ b/worlds/shivers/docs/setup_en.md @@ -0,0 +1,60 @@ +# Shivers Randomizer Setup Guide + + +## Required Software + +- [Shivers (GOG version)](https://www.gog.com/en/game/shivers) or original disc +- [ScummVM](https://www.scummvm.org/downloads/) version 2.7.0 or later +- [Shivers Randomizer](https://www.speedrun.com/shivers/resources) + +## Setup ScummVM for Shivers + +### GOG version of Shivers + +1. Launch ScummVM +2. Click Add Game... +3. Locate the folder for Shivers (typically in GOG Galaxy\Games\Shivers) +4. Click OK + +### Disc copy of Shivers + +1. Copy contents of Shivers disc to a desired location on your computer +2. Launch ScummVM +3. Click Add Game... +4. Locate the folder for Shivers and click Choose +5. Click OK + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The Player Settings page on the website allows you to configure your personal settings and export a config file from +them. Player settings page: [Shivers Player Settings Page](/games/Shivers/player-settings) + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/mysterycheck) + +## Joining a MultiWorld Game + +1. Launch ScummVM +2. Highlight Shivers and click "Start" +3. Launch the Shivers Randomizer +4. Click "Attach" +5. Click "Archipelago" +6. Enter the Archipelago server address, slot name, and password +7. Click "Connect" +8. In Shivers click "New Game" + +## What is a check + +- Every puzzle +- Every puzzle hint/solution +- Every document that is considered a Flashback +- Optionally information plaques. From edb62004ef22cf3f2a8d4c5917c4e2cd185e88ec Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 25 Nov 2023 11:12:13 +0100 Subject: [PATCH 083/104] LttP: remove extra default = False (#2497) * LttP: remove extra default = False --- worlds/alttp/Options.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 0f35be7459a3..a89a9adb83a7 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -102,9 +102,10 @@ class map_shuffle(DungeonItem): class key_drop_shuffle(Toggle): - """Shuffle keys found in pots and dropped from killed enemies.""" + """Shuffle keys found in pots and dropped from killed enemies, + respects the small key and big key shuffle options.""" display_name = "Key Drop Shuffle" - default = False + class Crystals(Range): range_start = 0 From 8a852abdc4e36d3ea3fe8b4a5154ae97e78bd17d Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sat, 25 Nov 2023 05:57:02 -0500 Subject: [PATCH 084/104] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Migrate=20support?= =?UTF-8?q?=20into=20Bizhawk=20Client=20(#2466)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removes the Pokémon Client, adding support for Red and Blue to the Bizhawk Client. - Adds `/bank` commands that mirror SDV's, allowing transferring money into and out of the EnergyLink storage. - Adds a fix to the base patch so that the progressive card key counter will not increment beyond 10, which would lead to receiving glitch items. This value is checked against and verified that it is not > 10 as part of crash detection by the client, to prevent erroneous location checks when the game crashes, so this is relevant to the new client (although shouldn't happen unless you're using !getitem, or putting progressive card keys as item link replacement items) --- PokemonClient.py | 382 ------------------ data/lua/connector_pkmn_rb.lua | 224 ---------- inno_setup.iss | 8 +- worlds/LauncherComponents.py | 2 - worlds/pokemon_rb/__init__.py | 21 +- worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 45893 -> 45946 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 45875 -> 45892 bytes worlds/pokemon_rb/client.py | 277 +++++++++++++ .../docs/en_Pokemon Red and Blue.md | 7 +- worlds/pokemon_rb/docs/setup_en.md | 43 +- worlds/pokemon_rb/rom.py | 4 + worlds/pokemon_rb/rom_addresses.py | 222 +++++----- 12 files changed, 439 insertions(+), 751 deletions(-) delete mode 100644 PokemonClient.py delete mode 100644 data/lua/connector_pkmn_rb.lua create mode 100644 worlds/pokemon_rb/client.py diff --git a/PokemonClient.py b/PokemonClient.py deleted file mode 100644 index 6b43a53b8ff7..000000000000 --- a/PokemonClient.py +++ /dev/null @@ -1,382 +0,0 @@ -import asyncio -import json -import time -import os -import bsdiff4 -import subprocess -import zipfile -from asyncio import StreamReader, StreamWriter -from typing import List - - -import Utils -from Utils import async_start -from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \ - get_base_parser - -from worlds.pokemon_rb.locations import location_data -from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch - -location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}} -location_bytes_bits = {} -for location in location_data: - if location.ram_address is not None: - if type(location.ram_address) == list: - location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address - location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit}, - {'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}] - else: - location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address - location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit} - -location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item" - and location.address is not None} - -SYSTEM_MESSAGE_ID = 0 - -CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua" -CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure pkmn_rb.lua is running" -CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart pkmn_rb.lua" -CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" -CONNECTION_CONNECTED_STATUS = "Connected" -CONNECTION_INITIAL_STATUS = "Connection has not been initiated" - -DISPLAY_MSGS = True - -SCRIPT_VERSION = 3 - - -class GBCommandProcessor(ClientCommandProcessor): - def __init__(self, ctx: CommonContext): - super().__init__(ctx) - - def _cmd_gb(self): - """Check Gameboy Connection State""" - if isinstance(self.ctx, GBContext): - logger.info(f"Gameboy Status: {self.ctx.gb_status}") - - -class GBContext(CommonContext): - command_processor = GBCommandProcessor - game = 'Pokemon Red and Blue' - - def __init__(self, server_address, password): - super().__init__(server_address, password) - self.gb_streams: (StreamReader, StreamWriter) = None - self.gb_sync_task = None - self.messages = {} - self.locations_array = None - self.gb_status = CONNECTION_INITIAL_STATUS - self.awaiting_rom = False - self.display_msgs = True - self.deathlink_pending = False - self.set_deathlink = False - self.client_compatibility_mode = 0 - self.items_handling = 0b001 - self.sent_release = False - self.sent_collect = False - self.auto_hints = set() - - async def server_auth(self, password_requested: bool = False): - if password_requested and not self.password: - await super(GBContext, self).server_auth(password_requested) - if not self.auth: - self.awaiting_rom = True - logger.info('Awaiting connection to EmuHawk to get Player information') - return - - await self.send_connect() - - def _set_message(self, msg: str, msg_id: int): - if DISPLAY_MSGS: - self.messages[(time.time(), msg_id)] = msg - - def on_package(self, cmd: str, args: dict): - if cmd == 'Connected': - self.locations_array = None - if 'death_link' in args['slot_data'] and args['slot_data']['death_link']: - self.set_deathlink = True - elif cmd == "RoomInfo": - self.seed_name = args['seed_name'] - elif cmd == 'Print': - msg = args['text'] - if ': !' not in msg: - self._set_message(msg, SYSTEM_MESSAGE_ID) - elif cmd == "ReceivedItems": - msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" - self._set_message(msg, SYSTEM_MESSAGE_ID) - - def on_deathlink(self, data: dict): - self.deathlink_pending = True - super().on_deathlink(data) - - def run_gui(self): - from kvui import GameManager - - class GBManager(GameManager): - logging_pairs = [ - ("Client", "Archipelago") - ] - base_title = "Archipelago Pokémon Client" - - self.ui = GBManager(self) - self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") - - -def get_payload(ctx: GBContext): - current_time = time.time() - ret = json.dumps( - { - "items": [item.item for item in ctx.items_received], - "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items() - if key[0] > current_time - 10}, - "deathlink": ctx.deathlink_pending, - "options": ((ctx.permissions['release'] in ('goal', 'enabled')) * 2) + (ctx.permissions['collect'] in ('goal', 'enabled')) - } - ) - ctx.deathlink_pending = False - return ret - - -async def parse_locations(data: List, ctx: GBContext): - locations = [] - flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20], - "Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], - "Rod": data[0x140 + 0x20 + 0x0E:0x140 + 0x20 + 0x0E + 0x01]} - - if len(data) > 0x140 + 0x20 + 0x0E + 0x01: - flags["DexSanityFlag"] = data[0x140 + 0x20 + 0x0E + 0x01:] - else: - flags["DexSanityFlag"] = [0] * 19 - - for flag_type, loc_map in location_map.items(): - for flag, loc_id in loc_map.items(): - if flag_type == "list": - if (flags["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 << location_bytes_bits[loc_id][0]['bit'] - and flags["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 << location_bytes_bits[loc_id][1]['bit']): - locations.append(loc_id) - elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']: - locations.append(loc_id) - - hints = [] - if flags["EventFlag"][280] & 16: - hints.append("Cerulean Bicycle Shop") - if flags["EventFlag"][280] & 32: - hints.append("Route 2 Gate - Oak's Aide") - if flags["EventFlag"][280] & 64: - hints.append("Route 11 Gate 2F - Oak's Aide") - if flags["EventFlag"][280] & 128: - hints.append("Route 15 Gate 2F - Oak's Aide") - if flags["EventFlag"][281] & 1: - hints += ["Celadon Prize Corner - Item Prize 1", "Celadon Prize Corner - Item Prize 2", - "Celadon Prize Corner - Item Prize 3"] - if (location_name_to_id["Fossil - Choice A"] in ctx.checked_locations and location_name_to_id["Fossil - Choice B"] - not in ctx.checked_locations): - hints.append("Fossil - Choice B") - elif (location_name_to_id["Fossil - Choice B"] in ctx.checked_locations and location_name_to_id["Fossil - Choice A"] - not in ctx.checked_locations): - hints.append("Fossil - Choice A") - hints = [ - location_name_to_id[loc] for loc in hints if location_name_to_id[loc] not in ctx.auto_hints and - location_name_to_id[loc] in ctx.missing_locations and location_name_to_id[loc] not in ctx.locations_checked - ] - if hints: - await ctx.send_msgs([{"cmd": "LocationScouts", "locations": hints, "create_as_hint": 2}]) - ctx.auto_hints.update(hints) - - if flags["EventFlag"][280] & 1 and not ctx.finished_game: - await ctx.send_msgs([ - {"cmd": "StatusUpdate", - "status": 30} - ]) - ctx.finished_game = True - if locations == ctx.locations_array: - return - ctx.locations_array = locations - if locations is not None: - await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}]) - - -async def gb_sync_task(ctx: GBContext): - logger.info("Starting GB connector. Use /gb for status information") - while not ctx.exit_event.is_set(): - error_status = None - if ctx.gb_streams: - (reader, writer) = ctx.gb_streams - msg = get_payload(ctx).encode() - writer.write(msg) - writer.write(b'\n') - try: - await asyncio.wait_for(writer.drain(), timeout=1.5) - try: - # Data will return a dict with up to two fields: - # 1. A keepalive response of the Players Name (always) - # 2. An array representing the memory values of the locations area (if in game) - data = await asyncio.wait_for(reader.readline(), timeout=5) - data_decoded = json.loads(data.decode()) - if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION: - msg = "You are connecting with an incompatible Lua script version. Ensure your connector Lua " \ - "and PokemonClient are from the same Archipelago installation." - logger.info(msg, extra={'compact_gui': True}) - ctx.gui_error('Error', msg) - error_status = CONNECTION_RESET_STATUS - ctx.client_compatibility_mode = data_decoded['clientCompatibilityVersion'] - if ctx.client_compatibility_mode == 0: - ctx.items_handling = 0b101 # old patches will not have local start inventory, must be requested - if ctx.seed_name and ctx.seed_name != ''.join([chr(i) for i in data_decoded['seedName'] if i != 0]): - msg = "The server is running a different multiworld than your client is. (invalid seed_name)" - logger.info(msg, extra={'compact_gui': True}) - ctx.gui_error('Error', msg) - error_status = CONNECTION_RESET_STATUS - ctx.seed_name = ''.join([chr(i) for i in data_decoded['seedName'] if i != 0]) - if not ctx.auth: - ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0]) - if ctx.auth == '': - msg = "Invalid ROM detected. No player name built into the ROM." - logger.info(msg, extra={'compact_gui': True}) - ctx.gui_error('Error', msg) - error_status = CONNECTION_RESET_STATUS - if ctx.awaiting_rom: - await ctx.server_auth(False) - if 'locations' in data_decoded and ctx.game and ctx.gb_status == CONNECTION_CONNECTED_STATUS \ - and not error_status and ctx.auth: - # Not just a keep alive ping, parse - async_start(parse_locations(data_decoded['locations'], ctx)) - if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags: - await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!") - if 'options' in data_decoded: - msgs = [] - if data_decoded['options'] & 4 and not ctx.sent_release: - ctx.sent_release = True - msgs.append({"cmd": "Say", "text": "!release"}) - if data_decoded['options'] & 8 and not ctx.sent_collect: - ctx.sent_collect = True - msgs.append({"cmd": "Say", "text": "!collect"}) - if msgs: - await ctx.send_msgs(msgs) - if ctx.set_deathlink: - await ctx.update_death_link(True) - except asyncio.TimeoutError: - logger.debug("Read Timed Out, Reconnecting") - error_status = CONNECTION_TIMING_OUT_STATUS - writer.close() - ctx.gb_streams = None - except ConnectionResetError as e: - logger.debug("Read failed due to Connection Lost, Reconnecting") - error_status = CONNECTION_RESET_STATUS - writer.close() - ctx.gb_streams = None - except TimeoutError: - logger.debug("Connection Timed Out, Reconnecting") - error_status = CONNECTION_TIMING_OUT_STATUS - writer.close() - ctx.gb_streams = None - except ConnectionResetError: - logger.debug("Connection Lost, Reconnecting") - error_status = CONNECTION_RESET_STATUS - writer.close() - ctx.gb_streams = None - if ctx.gb_status == CONNECTION_TENTATIVE_STATUS: - if not error_status: - logger.info("Successfully Connected to Gameboy") - ctx.gb_status = CONNECTION_CONNECTED_STATUS - else: - ctx.gb_status = f"Was tentatively connected but error occured: {error_status}" - elif error_status: - ctx.gb_status = error_status - logger.info("Lost connection to Gameboy and attempting to reconnect. Use /gb for status updates") - else: - try: - logger.debug("Attempting to connect to Gameboy") - ctx.gb_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 17242), timeout=10) - ctx.gb_status = CONNECTION_TENTATIVE_STATUS - except TimeoutError: - logger.debug("Connection Timed Out, Trying Again") - ctx.gb_status = CONNECTION_TIMING_OUT_STATUS - continue - except ConnectionRefusedError: - logger.debug("Connection Refused, Trying Again") - ctx.gb_status = CONNECTION_REFUSED_STATUS - continue - - -async def run_game(romfile): - auto_start = Utils.get_options()["pokemon_rb_options"].get("rom_start", True) - if auto_start is True: - import webbrowser - webbrowser.open(romfile) - elif os.path.isfile(auto_start): - subprocess.Popen([auto_start, romfile], - stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - - -async def patch_and_run_game(game_version, patch_file, ctx): - base_name = os.path.splitext(patch_file)[0] - comp_path = base_name + '.gb' - if game_version == "blue": - delta_patch = BlueDeltaPatch - else: - delta_patch = RedDeltaPatch - - try: - base_rom = delta_patch.get_source_data() - except Exception as msg: - logger.info(msg, extra={'compact_gui': True}) - ctx.gui_error('Error', msg) - - with zipfile.ZipFile(patch_file, 'r') as patch_archive: - with patch_archive.open('delta.bsdiff4', 'r') as stream: - patch = stream.read() - patched_rom_data = bsdiff4.patch(base_rom, patch) - - with open(comp_path, "wb") as patched_rom_file: - patched_rom_file.write(patched_rom_data) - - async_start(run_game(comp_path)) - - -if __name__ == '__main__': - - Utils.init_logging("PokemonClient") - - options = Utils.get_options() - - async def main(): - parser = get_base_parser() - parser.add_argument('patch_file', default="", type=str, nargs="?", - help='Path to an APRED or APBLUE patch file') - args = parser.parse_args() - - ctx = GBContext(args.connect, args.password) - ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") - if gui_enabled: - ctx.run_gui() - ctx.run_cli() - ctx.gb_sync_task = asyncio.create_task(gb_sync_task(ctx), name="GB Sync") - - if args.patch_file: - ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower() - if ext == "apred": - logger.info("APRED file supplied, beginning patching process...") - async_start(patch_and_run_game("red", args.patch_file, ctx)) - elif ext == "apblue": - logger.info("APBLUE file supplied, beginning patching process...") - async_start(patch_and_run_game("blue", args.patch_file, ctx)) - else: - logger.warning(f"Unknown patch file extension {ext}") - - await ctx.exit_event.wait() - ctx.server_address = None - - await ctx.shutdown() - - if ctx.gb_sync_task: - await ctx.gb_sync_task - - - import colorama - - colorama.init() - - asyncio.run(main()) - colorama.deinit() diff --git a/data/lua/connector_pkmn_rb.lua b/data/lua/connector_pkmn_rb.lua deleted file mode 100644 index 3f56435bdbee..000000000000 --- a/data/lua/connector_pkmn_rb.lua +++ /dev/null @@ -1,224 +0,0 @@ -local socket = require("socket") -local json = require('json') -local math = require('math') -require("common") -local STATE_OK = "Ok" -local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" -local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" -local STATE_UNINITIALIZED = "Uninitialized" - -local SCRIPT_VERSION = 3 - -local APIndex = 0x1A6E -local APDeathLinkAddress = 0x00FD -local APItemAddress = 0x00FF -local EventFlagAddress = 0x1735 -local MissableAddress = 0x161A -local HiddenItemsAddress = 0x16DE -local RodAddress = 0x1716 -local DexSanityAddress = 0x1A71 -local InGameAddress = 0x1A84 -local ClientCompatibilityAddress = 0xFF00 - -local ItemsReceived = nil -local playerName = nil -local seedName = nil - -local deathlink_rec = nil -local deathlink_send = false - -local prevstate = "" -local curstate = STATE_UNINITIALIZED -local gbSocket = nil -local frame = 0 - -local compat = nil - -local function defineMemoryFunctions() - local memDomain = {} - local domains = memory.getmemorydomainlist() - memDomain["rom"] = function() memory.usememorydomain("ROM") end - memDomain["wram"] = function() memory.usememorydomain("WRAM") end - return memDomain -end - -local memDomain = defineMemoryFunctions() -u8 = memory.read_u8 -wU8 = memory.write_u8 -u16 = memory.read_u16_le -function uRange(address, bytes) - data = memory.readbyterange(address - 1, bytes + 1) - data[0] = nil - return data -end - -function generateLocationsChecked() - memDomain.wram() - events = uRange(EventFlagAddress, 0x140) - missables = uRange(MissableAddress, 0x20) - hiddenitems = uRange(HiddenItemsAddress, 0x0E) - rod = {u8(RodAddress)} - dexsanity = uRange(DexSanityAddress, 19) - - - data = {} - - categories = {events, missables, hiddenitems, rod} - if compat > 1 then - table.insert(categories, dexsanity) - end - for _, category in ipairs(categories) do - for _, v in ipairs(category) do - table.insert(data, v) - end - end - - return data -end - -local function arrayEqual(a1, a2) - if #a1 ~= #a2 then - return false - end - - for i, v in ipairs(a1) do - if v ~= a2[i] then - return false - end - end - - return true -end - -function receive() - l, e = gbSocket:receive() - if e == 'closed' then - if curstate == STATE_OK then - print("Connection closed") - end - curstate = STATE_UNINITIALIZED - return - elseif e == 'timeout' then - return - elseif e ~= nil then - print(e) - curstate = STATE_UNINITIALIZED - return - end - if l ~= nil then - block = json.decode(l) - if block ~= nil then - local itemsBlock = block["items"] - if itemsBlock ~= nil then - ItemsReceived = itemsBlock - end - deathlink_rec = block["deathlink"] - - end - end - -- Determine Message to send back - memDomain.rom() - newPlayerName = uRange(0xFFF0, 0x10) - newSeedName = uRange(0xFFDB, 21) - if (playerName ~= nil and not arrayEqual(playerName, newPlayerName)) or (seedName ~= nil and not arrayEqual(seedName, newSeedName)) then - print("ROM changed, quitting") - curstate = STATE_UNINITIALIZED - return - end - playerName = newPlayerName - seedName = newSeedName - local retTable = {} - retTable["scriptVersion"] = SCRIPT_VERSION - - if compat == nil then - compat = u8(ClientCompatibilityAddress) - if compat < 2 then - InGameAddress = 0x1A71 - end - end - - retTable["clientCompatibilityVersion"] = compat - retTable["playerName"] = playerName - retTable["seedName"] = seedName - memDomain.wram() - - in_game = u8(InGameAddress) - if in_game == 0x2A or in_game == 0xAC then - retTable["locations"] = generateLocationsChecked() - elseif in_game ~= 0 then - print("Game may have crashed") - curstate = STATE_UNINITIALIZED - return - end - - retTable["deathLink"] = deathlink_send - deathlink_send = false - - msg = json.encode(retTable).."\n" - local ret, error = gbSocket:send(msg) - if ret == nil then - print(error) - elseif curstate == STATE_INITIAL_CONNECTION_MADE then - curstate = STATE_TENTATIVELY_CONNECTED - elseif curstate == STATE_TENTATIVELY_CONNECTED then - print("Connected!") - curstate = STATE_OK - end -end - -function main() - if not checkBizHawkVersion() then - return - end - server, error = socket.bind('localhost', 17242) - - while true do - frame = frame + 1 - if not (curstate == prevstate) then - print("Current state: "..curstate) - prevstate = curstate - end - if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then - if (frame % 5 == 0) then - receive() - in_game = u8(InGameAddress) - if in_game == 0x2A or in_game == 0xAC then - if u8(APItemAddress) == 0x00 then - ItemIndex = u16(APIndex) - if deathlink_rec == true then - wU8(APDeathLinkAddress, 1) - elseif u8(APDeathLinkAddress) == 3 then - wU8(APDeathLinkAddress, 0) - deathlink_send = true - end - if ItemsReceived[ItemIndex + 1] ~= nil then - item_id = ItemsReceived[ItemIndex + 1] - 172000000 - if item_id > 255 then - item_id = item_id - 256 - end - wU8(APItemAddress, item_id) - end - end - end - end - elseif (curstate == STATE_UNINITIALIZED) then - if (frame % 60 == 0) then - - print("Waiting for client.") - - emu.frameadvance() - server:settimeout(2) - print("Attempting to connect") - local client, timeout = server:accept() - if timeout == nil then - curstate = STATE_INITIAL_CONNECTION_MADE - gbSocket = client - gbSocket:settimeout(0) - end - end - end - emu.frameadvance() - end -end - -main() diff --git a/inno_setup.iss b/inno_setup.iss index b4779b1067b7..4744fa2b724d 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -140,13 +140,13 @@ Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{ Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Root: HKCR; Subkey: ".apbn3"; ValueData: "{#MyAppName}bn3bpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Archipelago MegaMan Battle Network 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index c3ae2b0495b0..31739bb24606 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -101,8 +101,6 @@ def launch_textclient(): Component('OoT Adjuster', 'OoTAdjuster'), # FF1 Component('FF1 Client', 'FF1Client'), - # Pokémon - Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), # TLoZ Component('Zelda 1 Client', 'Zelda1Client', file_identifier=SuffixIdentifier('.aptloz')), # ChecksFinder diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index b2ee0702c91e..d9bd6dde76e5 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -2,9 +2,11 @@ import settings import typing import threading +import base64 from copy import deepcopy from typing import TextIO +from Utils import __version__ from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification, LocationProgressType from Fill import fill_restrictive, FillError, sweep_from_pool from worlds.AutoWorld import World, WebWorld @@ -22,6 +24,7 @@ from .level_scaling import level_scaling from . import logic from . import poke_data +from . import client class PokemonSettings(settings.Group): @@ -36,16 +39,8 @@ class BlueRomFile(settings.UserFilePath): copy_to = "Pokemon Blue (UE) [S][!].gb" md5s = [BlueDeltaPatch.hash] - class RomStart(str): - """ - Set this to false to never autostart a rom (such as after patching) - True for operating system default program - Alternatively, a path to a program to open the .gb file with - """ - red_rom_file: RedRomFile = RedRomFile(RedRomFile.copy_to) blue_rom_file: BlueRomFile = BlueRomFile(BlueRomFile.copy_to) - rom_start: typing.Union[RomStart, bool] = True class PokemonWebWorld(WebWorld): @@ -141,9 +136,6 @@ def encode_name(name, t): else: self.rival_name = encode_name(self.multiworld.rival_name[self.player].value, "Rival") - if len(self.multiworld.player_name[self.player].encode()) > 16: - raise Exception(f"Player name too long for {self.multiworld.get_player_name(self.player)}. Player name cannot exceed 16 bytes for Pokémon Red and Blue.") - if not self.multiworld.badgesanity[self.player]: self.multiworld.non_local_items[self.player].value -= self.item_name_groups["Badges"] @@ -621,6 +613,13 @@ def stage_generate_output(cls, multiworld, output_directory): def generate_output(self, output_directory: str): generate_output(self, output_directory) + def modify_multidata(self, multidata: dict): + rom_name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0', + 'utf8')[:21] + rom_name.extend([0] * (21 - len(rom_name))) + new_name = base64.b64encode(bytes(rom_name)).decode() + multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] + def write_spoiler_header(self, spoiler_handle: TextIO): spoiler_handle.write(f"Cerulean Cave Total Key Items: {self.multiworld.cerulean_cave_key_items_condition[self.player].total}\n") spoiler_handle.write(f"Elite Four Total Key Items: {self.multiworld.elite_four_key_items_condition[self.player].total}\n") diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index eb4d83360cd854c12ec7f0983dd3944d6bfa7fd1..bee5a8d2f4996ee1db54d2b7812938cc66dcb922 100644 GIT binary patch literal 45946 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

^aKX)H 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 UboM_(lt27k$rRy2K>Qy9z{TtP>;M1& literal 45875 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 diff --git a/worlds/pokemon_rb/client.py b/worlds/pokemon_rb/client.py new file mode 100644 index 000000000000..fb29045cf4e8 --- /dev/null +++ b/worlds/pokemon_rb/client.py @@ -0,0 +1,277 @@ +import base64 +import logging +import time + +from NetUtils import ClientStatus +from worlds._bizhawk.client import BizHawkClient +from worlds._bizhawk import read, write, guarded_write + +from worlds.pokemon_rb.locations import location_data + +logger = logging.getLogger("Client") + +BANK_EXCHANGE_RATE = 100000000 + +DATA_LOCATIONS = { + "ItemIndex": (0x1A6E, 0x02), + "Deathlink": (0x00FD, 0x01), + "APItem": (0x00FF, 0x01), + "EventFlag": (0x1735, 0x140), + "Missable": (0x161A, 0x20), + "Hidden": (0x16DE, 0x0E), + "Rod": (0x1716, 0x01), + "DexSanityFlag": (0x1A71, 19), + "GameStatus": (0x1A84, 0x01), + "Money": (0x141F, 3), + "ResetCheck": (0x0100, 4), + # First and second Vermilion Gym trash can selection. Second is not used, so should always be 0. + # First should never be above 0x0F. This is just before Event Flags. + "CrashCheck1": (0x1731, 2), + # Unused, should always be 0. This is just before Missables flags. + "CrashCheck2": (0x1617, 1), + # Progressive keys, should never be above 10. Just before Dexsanity flags. + "CrashCheck3": (0x1A70, 1), + # Route 18 script value. Should never be above 2. Just before Hidden items flags. + "CrashCheck4": (0x16DD, 1), +} + +location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}} +location_bytes_bits = {} +for location in location_data: + if location.ram_address is not None: + if type(location.ram_address) == list: + location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address + location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit}, + {'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}] + else: + location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address + location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit} + +location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item" + and location.address is not None} + + +class PokemonRBClient(BizHawkClient): + system = ("GB", "SGB") + patch_suffix = (".apred", ".apblue") + game = "Pokemon Red and Blue" + + def __init__(self): + super().__init__() + self.auto_hints = set() + self.locations_array = None + self.disconnect_pending = False + self.set_deathlink = False + self.banking_command = None + self.game_state = False + self.last_death_link = 0 + + async def validate_rom(self, ctx): + game_name = await read(ctx.bizhawk_ctx, [(0x134, 12, "ROM")]) + game_name = game_name[0].decode("ascii") + if game_name in ("POKEMON RED\00", "POKEMON BLUE"): + ctx.game = self.game + ctx.items_handling = 0b001 + ctx.command_processor.commands["bank"] = cmd_bank + seed_name = await read(ctx.bizhawk_ctx, [(0xFFDB, 21, "ROM")]) + ctx.seed_name = seed_name[0].split(b"\0")[0].decode("ascii") + self.set_deathlink = False + self.banking_command = None + self.locations_array = None + self.disconnect_pending = False + return True + return False + + async def set_auth(self, ctx): + auth_name = await read(ctx.bizhawk_ctx, [(0xFFC6, 21, "ROM")]) + if auth_name[0] == bytes([0] * 21): + # rom was patched before rom names implemented, use player name + auth_name = await read(ctx.bizhawk_ctx, [(0xFFF0, 16, "ROM")]) + auth_name = auth_name[0].decode("ascii").split("\x00")[0] + else: + auth_name = base64.b64encode(auth_name[0]).decode() + ctx.auth = auth_name + + async def game_watcher(self, ctx): + if not ctx.server or not ctx.server.socket.open or ctx.server.socket.closed: + return + + data = await read(ctx.bizhawk_ctx, [(loc_data[0], loc_data[1], "WRAM") + for loc_data in DATA_LOCATIONS.values()]) + data = {data_set_name: data_name for data_set_name, data_name in zip(DATA_LOCATIONS.keys(), data)} + + if self.set_deathlink: + self.set_deathlink = False + await ctx.update_death_link(True) + + if self.disconnect_pending: + self.disconnect_pending = False + await ctx.disconnect() + + if data["GameStatus"][0] == 0 or data["ResetCheck"] == b'\xff\xff\xff\x7f': + # Do not handle anything before game save is loaded + self.game_state = False + return + elif (data["GameStatus"][0] not in (0x2A, 0xAC) + or data["CrashCheck1"][0] & 0xF0 or data["CrashCheck1"][1] & 0xFF + or data["CrashCheck2"][0] + or data["CrashCheck3"][0] > 10 + or data["CrashCheck4"][0] > 2): + # Should mean game crashed + logger.warning("Pokémon Red/Blue game may have crashed. Disconnecting from server.") + self.game_state = False + await ctx.disconnect() + return + self.game_state = True + + # SEND ITEMS TO CLIENT + + if data["APItem"][0] == 0: + item_index = int.from_bytes(data["ItemIndex"], "little") + if len(ctx.items_received) > item_index: + item_code = ctx.items_received[item_index].item - 172000000 + if item_code > 255: + item_code -= 256 + await write(ctx.bizhawk_ctx, [(DATA_LOCATIONS["APItem"][0], + [item_code], "WRAM")]) + + # LOCATION CHECKS + + locations = set() + + for flag_type, loc_map in location_map.items(): + for flag, loc_id in loc_map.items(): + if flag_type == "list": + if (data["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 << + location_bytes_bits[loc_id][0]['bit'] + and data["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 << + location_bytes_bits[loc_id][1]['bit']): + locations.add(loc_id) + elif data[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']: + locations.add(loc_id) + + if locations != self.locations_array: + if locations: + self.locations_array = locations + await ctx.send_msgs([{"cmd": "LocationChecks", "locations": list(locations)}]) + + # AUTO HINTS + + hints = [] + if data["EventFlag"][280] & 16: + hints.append("Cerulean Bicycle Shop") + if data["EventFlag"][280] & 32: + hints.append("Route 2 Gate - Oak's Aide") + if data["EventFlag"][280] & 64: + hints.append("Route 11 Gate 2F - Oak's Aide") + if data["EventFlag"][280] & 128: + hints.append("Route 15 Gate 2F - Oak's Aide") + if data["EventFlag"][281] & 1: + hints += ["Celadon Prize Corner - Item Prize 1", "Celadon Prize Corner - Item Prize 2", + "Celadon Prize Corner - Item Prize 3"] + if (location_name_to_id["Fossil - Choice A"] in ctx.checked_locations and location_name_to_id[ + "Fossil - Choice B"] + not in ctx.checked_locations): + hints.append("Fossil - Choice B") + elif (location_name_to_id["Fossil - Choice B"] in ctx.checked_locations and location_name_to_id[ + "Fossil - Choice A"] + not in ctx.checked_locations): + hints.append("Fossil - Choice A") + hints = [ + location_name_to_id[loc] for loc in hints if location_name_to_id[loc] not in self.auto_hints and + location_name_to_id[loc] in ctx.missing_locations and + location_name_to_id[loc] not in ctx.locations_checked + ] + if hints: + await ctx.send_msgs([{"cmd": "LocationScouts", "locations": hints, "create_as_hint": 2}]) + self.auto_hints.update(hints) + + # DEATHLINK + + if "DeathLink" in ctx.tags: + if data["Deathlink"][0] == 3: + await ctx.send_death(ctx.player_names[ctx.slot] + " is out of usable Pokémon! " + + ctx.player_names[ctx.slot] + " blacked out!") + await write(ctx.bizhawk_ctx, [(DATA_LOCATIONS["Deathlink"][0], [0], "WRAM")]) + self.last_death_link = ctx.last_death_link + elif ctx.last_death_link > self.last_death_link: + self.last_death_link = ctx.last_death_link + await write(ctx.bizhawk_ctx, [(DATA_LOCATIONS["Deathlink"][0], [1], "WRAM")]) + + # BANK + + if self.banking_command: + original_money = data["Money"] + # Money is stored as binary-coded decimal. + money = int(original_money.hex()) + if self.banking_command > money: + logger.warning(f"You do not have ${self.banking_command} to deposit!") + elif (-self.banking_command * BANK_EXCHANGE_RATE) > ctx.stored_data[f"EnergyLink{ctx.team}"]: + logger.warning("Not enough money in the EnergyLink storage!") + else: + if self.banking_command + money > 999999: + self.banking_command = 999999 - money + money = str(money - self.banking_command).zfill(6) + money = [int(money[:2], 16), int(money[2:4], 16), int(money[4:], 16)] + money_written = await guarded_write(ctx.bizhawk_ctx, [(0x141F, money, "WRAM")], + [(0x141F, original_money, "WRAM")]) + if money_written: + if self.banking_command >= 0: + deposit = self.banking_command - int(self.banking_command / 4) + tax = self.banking_command - deposit + logger.info(f"Deposited ${deposit}, and charged a tax of ${tax}.") + self.banking_command = deposit + else: + logger.info(f"Withdrew ${-self.banking_command}.") + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": + [{"operation": "add", "value": self.banking_command * BANK_EXCHANGE_RATE}, + {"operation": "max", "value": 0}], + }]) + self.banking_command = None + + # VICTORY + + if data["EventFlag"][280] & 1 and not ctx.finished_game: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + + def on_package(self, ctx, cmd, args): + if cmd == 'Connected': + if 'death_link' in args['slot_data'] and args['slot_data']['death_link']: + self.set_deathlink = True + self.last_death_link = time.time() + ctx.set_notify(f"EnergyLink{ctx.team}") + elif cmd == 'RoomInfo': + if ctx.seed_name and ctx.seed_name != args["seed_name"]: + # CommonClient's on_package displays an error to the user in this case, but connection is not cancelled. + self.game_state = False + self.disconnect_pending = True + super().on_package(ctx, cmd, args) + + +def cmd_bank(self, cmd: str = "", amount: str = ""): + """Deposit or withdraw money with the server's EnergyLink storage. + /bank - check server balance. + /bank deposit # - deposit money. One quarter of the amount will be lost to taxation. + /bank withdraw # - withdraw money.""" + if self.ctx.game != "Pokemon Red and Blue": + logger.warning("This command can only be used while playing Pokémon Red and Blue") + return + if not cmd: + logger.info(f"Money available: {int(self.ctx.stored_data[f'EnergyLink{self.ctx.team}'] / BANK_EXCHANGE_RATE)}") + return + elif (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: + logger.info(f"Must be connected to server and in game.") + elif not amount: + logger.warning("You must specify an amount.") + elif cmd == "withdraw": + self.ctx.client_handler.banking_command = -int(amount) + elif cmd == "deposit": + if int(amount) < 4: + logger.warning("You must deposit at least $4, for tax purposes.") + return + self.ctx.client_handler.banking_command = int(amount) + else: + logger.warning(f"Invalid bank command {cmd}") + return diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md index 086ec347f34f..b164d4b0fef6 100644 --- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -83,6 +83,9 @@ you until these have ended. ## Unique Local Commands -The following command is only available when using the PokemonClient to play with Archipelago. +You can use `/bank` commands to deposit and withdraw money from the server's EnergyLink storage. This can be accessed by +any players playing games that use the EnergyLink feature. -- `/gb` Check Gameboy Connection State +- `/bank` - check the amount of money available on the server. +- `/bank withdraw #` - withdraw money from the server. +- `/bank deposit #` - deposit money into the server. 25% of the amount will be lost to taxation. \ No newline at end of file diff --git a/worlds/pokemon_rb/docs/setup_en.md b/worlds/pokemon_rb/docs/setup_en.md index 7ba9b3aa09e3..c9344959f6b9 100644 --- a/worlds/pokemon_rb/docs/setup_en.md +++ b/worlds/pokemon_rb/docs/setup_en.md @@ -11,7 +11,6 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst - Detailed installation instructions for BizHawk can be found at the above link. - Windows users must run the prereq installer first, which can also be found at the above link. - The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases) - (select `Pokemon Client` during installation). - Pokémon Red and/or Blue ROM files. The Archipelago community cannot provide these. ## Optional Software @@ -71,28 +70,41 @@ And the following special characters (these each count as one character): ## Joining a MultiWorld Game -### Obtain your Pokémon patch file +### Generating and Patching a Game -When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done, -the host will provide you with either a link to download your data file, or with a zip file containing everyone's data -files. Your data file should have a `.apred` or `.apblue` extension. +1. Create your settings file (YAML). +2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game). +This will generate an output file for you. Your patch file will have a `.apred` or `.apblue` file extension. +3. Open `ArchipelagoLauncher.exe` +4. Select "Open Patch" on the left side and select your patch file. +5. If this is your first time patching, you will be prompted to locate your vanilla ROM. +6. A patched `.gb` file will be created in the same place as the patch file. +7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your +BizHawk install. -Double-click on your patch file to start your client and start the ROM patch process. Once the process is finished -(this can take a while), the client and the emulator will be started automatically (if you associated the extension -to the emulator as recommended). +If you're playing a single-player seed and you don't care about autotracking or hints, you can stop here, close the +client, and load the patched ROM in any emulator. However, for multiworlds and other Archipelago features, continue +below using BizHawk as your emulator. ### Connect to the Multiserver -Once both the client and the emulator are started, you must connect them. Navigate to your Archipelago install folder, -then to `data/lua`, and drag+drop the `connector_pkmn_rb.lua` script onto the main EmuHawk window. (You could instead -open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `connector_pkmn_rb.lua` with the file -picker.) +By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just +in case you have to close and reopen a window mid-game for some reason. + +1. Pokémon Red and Blue use Archipelago's BizHawk Client. If the client isn't still open from when you patched your +game, you can re-open it from the launcher. +2. Ensure EmuHawk is running the patched ROM. +3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing. +4. In the Lua Console window, go to `Script > Open Script…`. +5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`. +6. The emulator may freeze every few seconds until it manages to connect to the client. This is expected. The BizHawk +Client window should indicate that it connected and recognized Pokémon Red/Blue. +7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the +top text field of the client and click Connect. To connect the client to the multiserver simply put `

:` on the textfield on top and press enter (if the server uses password, type in the bottom textfield `/connect
: [password]`) -Now you are ready to start your adventure in Kanto. - ## Auto-Tracking Pokémon Red and Blue has a fully functional map tracker that supports auto-tracking. @@ -102,4 +114,5 @@ Pokémon Red and Blue has a fully functional map tracker that supports auto-trac 3. Click on the "AP" symbol at the top. 4. Enter the AP address, slot name and password. -The rest should take care of itself! Items and checks will be marked automatically, and it even knows your settings - It will hide checks & adjust logic accordingly. +The rest should take care of itself! Items and checks will be marked automatically, and it even knows your settings - It +will hide checks & adjust logic accordingly. diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index 096ab8e0a1f6..81ab6648dd19 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -539,6 +539,10 @@ def set_trade_mon(address, loc): write_bytes(data, self.rival_name, rom_addresses['Rival_Name']) data[0xFF00] = 2 # client compatibility version + rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0', + 'utf8')[:21] + rom_name.extend([0] * (21 - len(rom_name))) + write_bytes(data, rom_name, 0xFFC6) write_bytes(data, self.multiworld.seed_name.encode(), 0xFFDB) write_bytes(data, self.multiworld.player_name[self.player].encode(), 0xFFF0) diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index 97faf7bff205..cd57e317bdeb 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -12,101 +12,101 @@ "Player_Name": 0x4568, "Rival_Name": 0x4570, "Price_Master_Ball": 0x45c8, - "Title_Seed": 0x5f1b, - "Title_Slot_Name": 0x5f3b, - "PC_Item": 0x6309, - "PC_Item_Quantity": 0x630e, - "Fly_Location": 0x631c, - "Skip_Player_Name": 0x6335, - "Skip_Rival_Name": 0x6343, - "Pallet_Fly_Coords": 0x666e, - "Option_Old_Man": 0xcb0e, - "Option_Old_Man_Lying": 0xcb11, - "Option_Route3_Guard_A": 0xcb17, - "Option_Trashed_House_Guard_A": 0xcb20, - "Option_Trashed_House_Guard_B": 0xcb26, - "Option_Boulders": 0xcdb7, - "Option_Rock_Tunnel_Extra_Items": 0xcdc0, - "Wild_Route1": 0xd13b, - "Wild_Route2": 0xd151, - "Wild_Route22": 0xd167, - "Wild_ViridianForest": 0xd17d, - "Wild_Route3": 0xd193, - "Wild_MtMoon1F": 0xd1a9, - "Wild_MtMoonB1F": 0xd1bf, - "Wild_MtMoonB2F": 0xd1d5, - "Wild_Route4": 0xd1eb, - "Wild_Route24": 0xd201, - "Wild_Route25": 0xd217, - "Wild_Route9": 0xd22d, - "Wild_Route5": 0xd243, - "Wild_Route6": 0xd259, - "Wild_Route11": 0xd26f, - "Wild_RockTunnel1F": 0xd285, - "Wild_RockTunnelB1F": 0xd29b, - "Wild_Route10": 0xd2b1, - "Wild_Route12": 0xd2c7, - "Wild_Route8": 0xd2dd, - "Wild_Route7": 0xd2f3, - "Wild_PokemonTower3F": 0xd30d, - "Wild_PokemonTower4F": 0xd323, - "Wild_PokemonTower5F": 0xd339, - "Wild_PokemonTower6F": 0xd34f, - "Wild_PokemonTower7F": 0xd365, - "Wild_Route13": 0xd37b, - "Wild_Route14": 0xd391, - "Wild_Route15": 0xd3a7, - "Wild_Route16": 0xd3bd, - "Wild_Route17": 0xd3d3, - "Wild_Route18": 0xd3e9, - "Wild_SafariZoneCenter": 0xd3ff, - "Wild_SafariZoneEast": 0xd415, - "Wild_SafariZoneNorth": 0xd42b, - "Wild_SafariZoneWest": 0xd441, - "Wild_SeaRoutes": 0xd458, - "Wild_SeafoamIslands1F": 0xd46d, - "Wild_SeafoamIslandsB1F": 0xd483, - "Wild_SeafoamIslandsB2F": 0xd499, - "Wild_SeafoamIslandsB3F": 0xd4af, - "Wild_SeafoamIslandsB4F": 0xd4c5, - "Wild_PokemonMansion1F": 0xd4db, - "Wild_PokemonMansion2F": 0xd4f1, - "Wild_PokemonMansion3F": 0xd507, - "Wild_PokemonMansionB1F": 0xd51d, - "Wild_Route21": 0xd533, - "Wild_Surf_Route21": 0xd548, - "Wild_CeruleanCave1F": 0xd55d, - "Wild_CeruleanCave2F": 0xd573, - "Wild_CeruleanCaveB1F": 0xd589, - "Wild_PowerPlant": 0xd59f, - "Wild_Route23": 0xd5b5, - "Wild_VictoryRoad2F": 0xd5cb, - "Wild_VictoryRoad3F": 0xd5e1, - "Wild_VictoryRoad1F": 0xd5f7, - "Wild_DiglettsCave": 0xd60d, - "Ghost_Battle5": 0xd781, - "HM_Surf_Badge_a": 0xda73, - "HM_Surf_Badge_b": 0xda78, - "Option_Fix_Combat_Bugs_Heal_Stat_Modifiers": 0xdcc2, - "Option_Silph_Scope_Skip": 0xe207, - "Wild_Old_Rod": 0xe382, - "Wild_Good_Rod": 0xe3af, - "Option_Fix_Combat_Bugs_PP_Restore": 0xe541, - "Option_Reusable_TMs": 0xe675, - "Wild_Super_Rod_A": 0xeaa9, - "Wild_Super_Rod_B": 0xeaae, - "Wild_Super_Rod_C": 0xeab3, - "Wild_Super_Rod_D": 0xeaba, - "Wild_Super_Rod_E": 0xeabf, - "Wild_Super_Rod_F": 0xeac4, - "Wild_Super_Rod_G": 0xeacd, - "Wild_Super_Rod_H": 0xead6, - "Wild_Super_Rod_I": 0xeadf, - "Wild_Super_Rod_J": 0xeae8, - "Starting_Money_High": 0xf9aa, - "Starting_Money_Middle": 0xf9ad, - "Starting_Money_Low": 0xf9b0, - "Option_Pokedex_Seen": 0xf9cb, + "Title_Seed": 0x5f22, + "Title_Slot_Name": 0x5f42, + "PC_Item": 0x6310, + "PC_Item_Quantity": 0x6315, + "Fly_Location": 0x6323, + "Skip_Player_Name": 0x633c, + "Skip_Rival_Name": 0x634a, + "Pallet_Fly_Coords": 0x6675, + "Option_Old_Man": 0xcb0b, + "Option_Old_Man_Lying": 0xcb0e, + "Option_Route3_Guard_A": 0xcb14, + "Option_Trashed_House_Guard_A": 0xcb1d, + "Option_Trashed_House_Guard_B": 0xcb23, + "Option_Boulders": 0xcdb4, + "Option_Rock_Tunnel_Extra_Items": 0xcdbd, + "Wild_Route1": 0xd138, + "Wild_Route2": 0xd14e, + "Wild_Route22": 0xd164, + "Wild_ViridianForest": 0xd17a, + "Wild_Route3": 0xd190, + "Wild_MtMoon1F": 0xd1a6, + "Wild_MtMoonB1F": 0xd1bc, + "Wild_MtMoonB2F": 0xd1d2, + "Wild_Route4": 0xd1e8, + "Wild_Route24": 0xd1fe, + "Wild_Route25": 0xd214, + "Wild_Route9": 0xd22a, + "Wild_Route5": 0xd240, + "Wild_Route6": 0xd256, + "Wild_Route11": 0xd26c, + "Wild_RockTunnel1F": 0xd282, + "Wild_RockTunnelB1F": 0xd298, + "Wild_Route10": 0xd2ae, + "Wild_Route12": 0xd2c4, + "Wild_Route8": 0xd2da, + "Wild_Route7": 0xd2f0, + "Wild_PokemonTower3F": 0xd30a, + "Wild_PokemonTower4F": 0xd320, + "Wild_PokemonTower5F": 0xd336, + "Wild_PokemonTower6F": 0xd34c, + "Wild_PokemonTower7F": 0xd362, + "Wild_Route13": 0xd378, + "Wild_Route14": 0xd38e, + "Wild_Route15": 0xd3a4, + "Wild_Route16": 0xd3ba, + "Wild_Route17": 0xd3d0, + "Wild_Route18": 0xd3e6, + "Wild_SafariZoneCenter": 0xd3fc, + "Wild_SafariZoneEast": 0xd412, + "Wild_SafariZoneNorth": 0xd428, + "Wild_SafariZoneWest": 0xd43e, + "Wild_SeaRoutes": 0xd455, + "Wild_SeafoamIslands1F": 0xd46a, + "Wild_SeafoamIslandsB1F": 0xd480, + "Wild_SeafoamIslandsB2F": 0xd496, + "Wild_SeafoamIslandsB3F": 0xd4ac, + "Wild_SeafoamIslandsB4F": 0xd4c2, + "Wild_PokemonMansion1F": 0xd4d8, + "Wild_PokemonMansion2F": 0xd4ee, + "Wild_PokemonMansion3F": 0xd504, + "Wild_PokemonMansionB1F": 0xd51a, + "Wild_Route21": 0xd530, + "Wild_Surf_Route21": 0xd545, + "Wild_CeruleanCave1F": 0xd55a, + "Wild_CeruleanCave2F": 0xd570, + "Wild_CeruleanCaveB1F": 0xd586, + "Wild_PowerPlant": 0xd59c, + "Wild_Route23": 0xd5b2, + "Wild_VictoryRoad2F": 0xd5c8, + "Wild_VictoryRoad3F": 0xd5de, + "Wild_VictoryRoad1F": 0xd5f4, + "Wild_DiglettsCave": 0xd60a, + "Ghost_Battle5": 0xd77e, + "HM_Surf_Badge_a": 0xda70, + "HM_Surf_Badge_b": 0xda75, + "Option_Fix_Combat_Bugs_Heal_Stat_Modifiers": 0xdcbf, + "Option_Silph_Scope_Skip": 0xe204, + "Wild_Old_Rod": 0xe37f, + "Wild_Good_Rod": 0xe3ac, + "Option_Fix_Combat_Bugs_PP_Restore": 0xe53e, + "Option_Reusable_TMs": 0xe672, + "Wild_Super_Rod_A": 0xeaa6, + "Wild_Super_Rod_B": 0xeaab, + "Wild_Super_Rod_C": 0xeab0, + "Wild_Super_Rod_D": 0xeab7, + "Wild_Super_Rod_E": 0xeabc, + "Wild_Super_Rod_F": 0xeac1, + "Wild_Super_Rod_G": 0xeaca, + "Wild_Super_Rod_H": 0xead3, + "Wild_Super_Rod_I": 0xeadc, + "Wild_Super_Rod_J": 0xeae5, + "Starting_Money_High": 0xf9a7, + "Starting_Money_Middle": 0xf9aa, + "Starting_Money_Low": 0xf9ad, + "Option_Pokedex_Seen": 0xf9c8, "HM_Fly_Badge_a": 0x13182, "HM_Fly_Badge_b": 0x13187, "HM_Cut_Badge_a": 0x131b8, @@ -1164,22 +1164,22 @@ "Prize_Mon_E": 0x52944, "Prize_Mon_F": 0x52946, "Start_Inventory": 0x52a7b, - "Map_Fly_Location": 0x52c6f, - "Reset_A": 0x52d1b, - "Reset_B": 0x52d47, - "Reset_C": 0x52d73, - "Reset_D": 0x52d9f, - "Reset_E": 0x52dcb, - "Reset_F": 0x52df7, - "Reset_G": 0x52e23, - "Reset_H": 0x52e4f, - "Reset_I": 0x52e7b, - "Reset_J": 0x52ea7, - "Reset_K": 0x52ed3, - "Reset_L": 0x52eff, - "Reset_M": 0x52f2b, - "Reset_N": 0x52f57, - "Reset_O": 0x52f83, + "Map_Fly_Location": 0x52c75, + "Reset_A": 0x52d21, + "Reset_B": 0x52d4d, + "Reset_C": 0x52d79, + "Reset_D": 0x52da5, + "Reset_E": 0x52dd1, + "Reset_F": 0x52dfd, + "Reset_G": 0x52e29, + "Reset_H": 0x52e55, + "Reset_I": 0x52e81, + "Reset_J": 0x52ead, + "Reset_K": 0x52ed9, + "Reset_L": 0x52f05, + "Reset_M": 0x52f31, + "Reset_N": 0x52f5d, + "Reset_O": 0x52f89, "Warps_Route2": 0x54026, "Missable_Route_2_Item_1": 0x5404a, "Missable_Route_2_Item_2": 0x54051, From 6dccf36f8853abdbf07df1bec2f22cfc69cfba71 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sat, 25 Nov 2023 07:09:08 -0500 Subject: [PATCH 085/104] Lingo: Various generation optimizations (#2479) Almost all of the events have been eradicated, which significantly improves both generation speed and playthrough calculation. Previously, checking for access to a location involved checking for access to each panel in the location, as well as recursively checking for access to any panels required by those panels. This potentially performed the same check multiple times. The access requirements for locations are now calculated and flattened in generate_early, so that the access function can directly check for the required rooms, doors, and colors. These flattened access requirements are also used for Entrance checking, and register_indirect_condition is used to make sure that can_reach(Region) is safe to use. The Mastery and Level 2 rules now just run a bunch of access rules and count the number of them that succeed, instead of relying on event items. Finally: the Level 2 panel hunt is now enabled even when Level 2 is not the victory condition, as I feel that generation is fast enough now for that to be acceptable. --- worlds/lingo/__init__.py | 12 +- worlds/lingo/data/LL1.yaml | 2 - worlds/lingo/options.py | 8 +- worlds/lingo/player_logic.py | 298 +++++++++++++++++++-------- worlds/lingo/regions.py | 48 +++-- worlds/lingo/rules.py | 112 +++++----- worlds/lingo/test/TestPanelsanity.py | 19 ++ 7 files changed, 330 insertions(+), 169 deletions(-) create mode 100644 worlds/lingo/test/TestPanelsanity.py diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 3d98ae91834f..da8a246e79c0 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -55,14 +55,14 @@ def create_regions(self): create_regions(self, self.player_logic) def create_items(self): - pool = [self.create_item(name) for name in self.player_logic.REAL_ITEMS] + pool = [self.create_item(name) for name in self.player_logic.real_items] - if self.player_logic.FORCED_GOOD_ITEM != "": - new_item = self.create_item(self.player_logic.FORCED_GOOD_ITEM) + if self.player_logic.forced_good_item != "": + new_item = self.create_item(self.player_logic.forced_good_item) location_obj = self.multiworld.get_location("Second Room - Good Luck", self.player) location_obj.place_locked_item(new_item) - item_difference = len(self.player_logic.REAL_LOCATIONS) - len(pool) + item_difference = len(self.player_logic.real_locations) - len(pool) if item_difference: trap_percentage = self.options.trap_percentage traps = int(item_difference * trap_percentage / 100.0) @@ -93,7 +93,7 @@ def create_item(self, name: str) -> Item: classification = item.classification if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\ - and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.PAINTING_MAPPING + and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.painting_mapping for painting_id in item.painting_ids): # If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings # go nowhere, then this item should not be progression. @@ -116,6 +116,6 @@ def fill_slot_data(self): } if self.options.shuffle_paintings: - slot_data["painting_entrance_to_exit"] = self.player_logic.PAINTING_MAPPING + slot_data["painting_entrance_to_exit"] = self.player_logic.painting_mapping return slot_data diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index d46403e8daa6..8a4f831f94cf 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -379,8 +379,6 @@ tag: forbid non_counting: True check: True - required_panel: - - panel: ANOTHER TRY doors: Exit Door: id: Entry Room Area Doors/Door_hi_high diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index 7dc6a1389c0c..fc9ddee0e0e9 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -52,7 +52,10 @@ class ShufflePaintings(Toggle): class VictoryCondition(Choice): - """Change the victory condition.""" + """Change the victory condition. + On "the_end", the goal is to solve THE END at the top of the tower. + On "the_master", the goal is to solve THE MASTER at the top of the tower, after getting the number of achievements specified in the Mastery Achievements option. + On "level_2", the goal is to solve LEVEL 2 in the second room, after solving the number of panels specified in the Level 2 Requirement option.""" display_name = "Victory Condition" option_the_end = 0 option_the_master = 1 @@ -75,9 +78,10 @@ class Level2Requirement(Range): """The number of panel solves required to unlock LEVEL 2. In the base game, 223 are needed. Note that this count includes ANOTHER TRY. + When set to 1, the panel hunt is disabled, and you can access LEVEL 2 for free. """ display_name = "Level 2 Requirement" - range_start = 2 + range_start = 1 range_end = 800 default = 223 diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index abb975e020ae..a0b33d1dbe58 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -1,10 +1,10 @@ -from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING +from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING from .items import ALL_ITEM_TABLE from .locations import ALL_LOCATION_TABLE, LocationClassification from .options import LocationChecks, ShuffleDoors, VictoryCondition from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \ - PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, ROOMS, \ + PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, RoomAndDoor, \ RoomAndPanel from .testing import LingoTestOptions @@ -12,10 +12,29 @@ from . import LingoWorld +class AccessRequirements: + rooms: Set[str] + doors: Set[RoomAndDoor] + colors: Set[str] + + def __init__(self): + self.rooms = set() + self.doors = set() + self.colors = set() + + def merge(self, other: "AccessRequirements"): + self.rooms |= other.rooms + self.doors |= other.doors + self.colors |= other.colors + + def __str__(self): + return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})" + + class PlayerLocation(NamedTuple): name: str - code: Optional[int] = None - panels: List[RoomAndPanel] = [] + code: Optional[int] + access: AccessRequirements class LingoPlayerLogic: @@ -23,27 +42,45 @@ class LingoPlayerLogic: Defines logic after a player's options have been applied """ - ITEM_BY_DOOR: Dict[str, Dict[str, str]] + item_by_door: Dict[str, Dict[str, str]] - LOCATIONS_BY_ROOM: Dict[str, List[PlayerLocation]] - REAL_LOCATIONS: List[str] + locations_by_room: Dict[str, List[PlayerLocation]] + real_locations: List[str] - EVENT_LOC_TO_ITEM: Dict[str, str] - REAL_ITEMS: List[str] + event_loc_to_item: Dict[str, str] + real_items: List[str] - VICTORY_CONDITION: str - MASTERY_LOCATION: str - LEVEL_2_LOCATION: str + victory_condition: str + mastery_location: str + level_2_location: str - PAINTING_MAPPING: Dict[str, str] + painting_mapping: Dict[str, str] - FORCED_GOOD_ITEM: str + forced_good_item: str - def add_location(self, room: str, loc: PlayerLocation): - self.LOCATIONS_BY_ROOM.setdefault(room, []).append(loc) + panel_reqs: Dict[str, Dict[str, AccessRequirements]] + door_reqs: Dict[str, Dict[str, AccessRequirements]] + mastery_reqs: List[AccessRequirements] + counting_panel_reqs: Dict[str, List[Tuple[AccessRequirements, int]]] + + def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel], world: "LingoWorld"): + """ + Creates a location. This function determines the access requirements for the location by combining and + flattening the requirements for each of the given panels. + """ + access_reqs = AccessRequirements() + for panel in panels: + if panel.room is not None and panel.room != room: + access_reqs.rooms.add(panel.room) + + panel_room = room if panel.room is None else panel.room + sub_access_reqs = self.calculate_panel_requirements(panel_room, panel.panel, world) + access_reqs.merge(sub_access_reqs) + + self.locations_by_room.setdefault(room, []).append(PlayerLocation(name, code, access_reqs)) def set_door_item(self, room: str, door: str, item: str): - self.ITEM_BY_DOOR.setdefault(room, {})[door] = item + self.item_by_door.setdefault(room, {})[door] = item def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"): if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]: @@ -52,21 +89,25 @@ def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "Lingo else: progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name self.set_door_item(room_name, door_data.name, progressive_item_name) - self.REAL_ITEMS.append(progressive_item_name) + self.real_items.append(progressive_item_name) else: self.set_door_item(room_name, door_data.name, door_data.item_name) def __init__(self, world: "LingoWorld"): - self.ITEM_BY_DOOR = {} - self.LOCATIONS_BY_ROOM = {} - self.REAL_LOCATIONS = [] - self.EVENT_LOC_TO_ITEM = {} - self.REAL_ITEMS = [] - self.VICTORY_CONDITION = "" - self.MASTERY_LOCATION = "" - self.LEVEL_2_LOCATION = "" - self.PAINTING_MAPPING = {} - self.FORCED_GOOD_ITEM = "" + self.item_by_door = {} + self.locations_by_room = {} + self.real_locations = [] + self.event_loc_to_item = {} + self.real_items = [] + self.victory_condition = "" + self.mastery_location = "" + self.level_2_location = "" + self.painting_mapping = {} + self.forced_good_item = "" + self.panel_reqs = {} + self.door_reqs = {} + self.mastery_reqs = [] + self.counting_panel_reqs = {} door_shuffle = world.options.shuffle_doors color_shuffle = world.options.shuffle_colors @@ -79,17 +120,10 @@ def __init__(self, world: "LingoWorld"): raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not " "be enough locations for all of the door items.") - # Create an event for every door, representing whether that door has been opened. Also create event items for - # doors that are event-only. - for room_name, room_data in DOORS_BY_ROOM.items(): - for door_name, door_data in room_data.items(): - if door_shuffle == ShuffleDoors.option_none: - itemloc_name = f"{room_name} - {door_name} (Opened)" - self.add_location(room_name, PlayerLocation(itemloc_name, None, door_data.panels)) - self.EVENT_LOC_TO_ITEM[itemloc_name] = itemloc_name - self.set_door_item(room_name, door_name, itemloc_name) - else: - # This line is duplicated from StaticLingoItems + # Create door items, where needed. + if door_shuffle != ShuffleDoors.option_none: + for room_name, room_data in DOORS_BY_ROOM.items(): + for door_name, door_data in room_data.items(): if door_data.skip_item is False and door_data.event is False: if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple: # Grouped doors are handled differently if shuffle doors is on simple. @@ -97,49 +131,44 @@ def __init__(self, world: "LingoWorld"): else: self.handle_non_grouped_door(room_name, door_data, world) - if door_data.event: - self.add_location(room_name, PlayerLocation(door_data.item_name, None, door_data.panels)) - self.EVENT_LOC_TO_ITEM[door_data.item_name] = door_data.item_name + " (Opened)" - self.set_door_item(room_name, door_name, door_data.item_name + " (Opened)") - - # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. We also - # create events for each counting panel, so that we can determine when LEVEL 2 is accessible. + # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. for room_name, room_data in PANELS_BY_ROOM.items(): for panel_name, panel_data in room_data.items(): if panel_data.achievement: - event_name = room_name + " - " + panel_name + " (Achieved)" - self.add_location(room_name, PlayerLocation(event_name, None, - [RoomAndPanel(room_name, panel_name)])) - self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement" + access_req = AccessRequirements() + access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world)) + access_req.rooms.add(room_name) - if not panel_data.non_counting and victory_condition == VictoryCondition.option_level_2: - event_name = room_name + " - " + panel_name + " (Counted)" - self.add_location(room_name, PlayerLocation(event_name, None, - [RoomAndPanel(room_name, panel_name)])) - self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" + self.mastery_reqs.append(access_req) # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need # to prevent the actual victory condition from becoming a check. - self.MASTERY_LOCATION = "Orange Tower Seventh Floor - THE MASTER" - self.LEVEL_2_LOCATION = "N/A" + self.mastery_location = "Orange Tower Seventh Floor - THE MASTER" + self.level_2_location = "Second Room - LEVEL 2" if victory_condition == VictoryCondition.option_the_end: - self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END" - self.add_location("Orange Tower Seventh Floor", PlayerLocation("The End (Solved)")) - self.EVENT_LOC_TO_ITEM["The End (Solved)"] = "Victory" + self.victory_condition = "Orange Tower Seventh Floor - THE END" + self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, [], world) + self.event_loc_to_item["The End (Solved)"] = "Victory" elif victory_condition == VictoryCondition.option_the_master: - self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE MASTER" - self.MASTERY_LOCATION = "Orange Tower Seventh Floor - Mastery Achievements" + self.victory_condition = "Orange Tower Seventh Floor - THE MASTER" + self.mastery_location = "Orange Tower Seventh Floor - Mastery Achievements" - self.add_location("Orange Tower Seventh Floor", PlayerLocation(self.MASTERY_LOCATION, None, [])) - self.EVENT_LOC_TO_ITEM[self.MASTERY_LOCATION] = "Victory" + self.add_location("Orange Tower Seventh Floor", self.mastery_location, None, [], world) + self.event_loc_to_item[self.mastery_location] = "Victory" elif victory_condition == VictoryCondition.option_level_2: - self.VICTORY_CONDITION = "Second Room - LEVEL 2" - self.LEVEL_2_LOCATION = "Second Room - Unlock Level 2" + self.victory_condition = "Second Room - LEVEL 2" + self.level_2_location = "Second Room - Unlock Level 2" + + self.add_location("Second Room", self.level_2_location, None, [RoomAndPanel("Second Room", "LEVEL 2")], + world) + self.event_loc_to_item[self.level_2_location] = "Victory" + + if world.options.level_2_requirement == 1: + raise Exception("The Level 2 requirement must be at least 2 when LEVEL 2 is the victory condition.") - self.add_location("Second Room", PlayerLocation(self.LEVEL_2_LOCATION, None, - [RoomAndPanel("Second Room", "LEVEL 2")])) - self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory" + # Create groups of counting panel access requirements for the LEVEL 2 check. + self.create_panel_hunt_events(world) # Instantiate all real locations. location_classification = LocationClassification.normal @@ -149,18 +178,17 @@ def __init__(self, world: "LingoWorld"): location_classification = LocationClassification.insanity for location_name, location_data in ALL_LOCATION_TABLE.items(): - if location_name != self.VICTORY_CONDITION: + if location_name != self.victory_condition: if location_classification not in location_data.classification: continue - self.add_location(location_data.room, PlayerLocation(location_name, location_data.code, - location_data.panels)) - self.REAL_LOCATIONS.append(location_name) + self.add_location(location_data.room, location_name, location_data.code, location_data.panels, world) + self.real_locations.append(location_name) # Instantiate all real items. for name, item in ALL_ITEM_TABLE.items(): if item.should_include(world): - self.REAL_ITEMS.append(name) + self.real_items.append(name) # Create the paintings mapping, if painting shuffle is on. if painting_shuffle: @@ -201,7 +229,7 @@ def __init__(self, world: "LingoWorld"): continue # If painting shuffle is on, we only want to consider paintings that actually go somewhere. - if painting_shuffle and painting_obj.id not in self.PAINTING_MAPPING.keys(): + if painting_shuffle and painting_obj.id not in self.painting_mapping.keys(): continue pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door] @@ -226,12 +254,12 @@ def __init__(self, world: "LingoWorld"): good_item_options.remove(item) if len(good_item_options) > 0: - self.FORCED_GOOD_ITEM = world.random.choice(good_item_options) - self.REAL_ITEMS.remove(self.FORCED_GOOD_ITEM) - self.REAL_LOCATIONS.remove("Second Room - Good Luck") + self.forced_good_item = world.random.choice(good_item_options) + self.real_items.remove(self.forced_good_item) + self.real_locations.remove("Second Room - Good Luck") def randomize_paintings(self, world: "LingoWorld") -> bool: - self.PAINTING_MAPPING.clear() + self.painting_mapping.clear() door_shuffle = world.options.shuffle_doors @@ -253,7 +281,7 @@ def randomize_paintings(self, world: "LingoWorld") -> bool: if painting.exit_only and painting.required] req_entrances = world.random.sample(req_enterable, len(req_exits)) - self.PAINTING_MAPPING = dict(zip(req_entrances, req_exits)) + self.painting_mapping = dict(zip(req_entrances, req_exits)) # Next, determine the rest of the exit paintings. exitable = [painting_id for painting_id, painting in PAINTINGS.items() @@ -272,25 +300,125 @@ def randomize_paintings(self, world: "LingoWorld") -> bool: for warp_exit in nonreq_exits: warp_enter = world.random.choice(chosen_entrances) chosen_entrances.remove(warp_enter) - self.PAINTING_MAPPING[warp_enter] = warp_exit + self.painting_mapping[warp_enter] = warp_exit # Assign each of the remaining entrances to any required or non-required exit. for warp_enter in chosen_entrances: warp_exit = world.random.choice(chosen_exits) - self.PAINTING_MAPPING[warp_enter] = warp_exit + self.painting_mapping[warp_enter] = warp_exit # The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves). # There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the # exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall # is forced to point to the vanilla exit. - if "eye_painting_2" not in self.PAINTING_MAPPING.keys(): - self.PAINTING_MAPPING["eye_painting"] = "eye_painting_2" + if "eye_painting_2" not in self.painting_mapping.keys(): + self.painting_mapping["eye_painting"] = "eye_painting_2" # Just for sanity's sake, ensure that all required painting rooms are accessed. for painting_id, painting in PAINTINGS.items(): - if painting_id not in self.PAINTING_MAPPING.values() \ + if painting_id not in self.painting_mapping.values() \ and (painting.required or (painting.required_when_no_doors and door_shuffle == ShuffleDoors.option_none)): return False return True + + def calculate_panel_requirements(self, room: str, panel: str, world: "LingoWorld"): + """ + Calculate and return the access requirements for solving a given panel. The goal is to eliminate recursion in + the access rule function by collecting the rooms, doors, and colors needed by this panel and any panel required + by this panel. Memoization is used so that no panel is evaluated more than once. + """ + if panel not in self.panel_reqs.setdefault(room, {}): + access_reqs = AccessRequirements() + panel_object = PANELS_BY_ROOM[room][panel] + + for req_room in panel_object.required_rooms: + access_reqs.rooms.add(req_room) + + for req_door in panel_object.required_doors: + door_object = DOORS_BY_ROOM[room if req_door.room is None else req_door.room][req_door.door] + if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none: + sub_access_reqs = self.calculate_door_requirements( + room if req_door.room is None else req_door.room, req_door.door, world) + access_reqs.merge(sub_access_reqs) + else: + access_reqs.doors.add(RoomAndDoor(room if req_door.room is None else req_door.room, req_door.door)) + + for color in panel_object.colors: + access_reqs.colors.add(color) + + for req_panel in panel_object.required_panels: + if req_panel.room is not None and req_panel.room != room: + access_reqs.rooms.add(req_panel.room) + + sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, + req_panel.panel, world) + access_reqs.merge(sub_access_reqs) + + self.panel_reqs[room][panel] = access_reqs + + return self.panel_reqs[room][panel] + + def calculate_door_requirements(self, room: str, door: str, world: "LingoWorld"): + """ + Similar to calculate_panel_requirements, but for event doors. + """ + if door not in self.door_reqs.setdefault(room, {}): + access_reqs = AccessRequirements() + door_object = DOORS_BY_ROOM[room][door] + + for req_panel in door_object.panels: + if req_panel.room is not None and req_panel.room != room: + access_reqs.rooms.add(req_panel.room) + + sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, + req_panel.panel, world) + access_reqs.merge(sub_access_reqs) + + self.door_reqs[room][door] = access_reqs + + return self.door_reqs[room][door] + + def create_panel_hunt_events(self, world: "LingoWorld"): + """ + Creates the event locations/items used for determining access to the LEVEL 2 panel. Instead of creating an event + for every single counting panel in the game, we try to coalesce panels with identical access rules into the same + event. Right now, this means the following: + + When color shuffle is off, panels in a room with no extra access requirements (room, door, or other panel) are + all coalesced into one event. + + When color shuffle is on, single-colored panels (including white) in a room are combined into one event per + color. Multicolored panels and panels with any extra access requirements are not coalesced, and will each + receive their own event. + """ + for room_name, room_data in PANELS_BY_ROOM.items(): + unhindered_panels_by_color: dict[Optional[str], int] = {} + + for panel_name, panel_data in room_data.items(): + # We won't count non-counting panels. + if panel_data.non_counting: + continue + + # We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will + # only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. + if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\ + or len(panel_data.required_rooms) > 0\ + or (world.options.shuffle_colors and len(panel_data.colors) > 1): + self.counting_panel_reqs.setdefault(room_name, []).append( + (self.calculate_panel_requirements(room_name, panel_name, world), 1)) + else: + if len(panel_data.colors) == 0 or not world.options.shuffle_colors: + color = None + else: + color = panel_data.colors[0] + + unhindered_panels_by_color[color] = unhindered_panels_by_color.get(color, 0) + 1 + + for color, panel_count in unhindered_panels_by_color.items(): + access_reqs = AccessRequirements() + if color is not None: + access_reqs.colors.add(color) + + self.counting_panel_reqs.setdefault(room_name, []).append((access_reqs, panel_count)) diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index e5f947de05e4..c24144a1609e 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -1,11 +1,11 @@ -from typing import Dict, TYPE_CHECKING +from typing import Dict, Optional, TYPE_CHECKING -from BaseClasses import ItemClassification, Region +from BaseClasses import Entrance, ItemClassification, Region from .items import LingoItem from .locations import LingoLocation from .player_logic import LingoPlayerLogic from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda -from .static_logic import ALL_ROOMS, PAINTINGS, Room +from .static_logic import ALL_ROOMS, PAINTINGS, Room, RoomAndDoor if TYPE_CHECKING: from . import LingoWorld @@ -13,12 +13,12 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogic) -> Region: new_region = Region(room.name, world.player, world.multiworld) - for location in player_logic.LOCATIONS_BY_ROOM.get(room.name, {}): + for location in player_logic.locations_by_room.get(room.name, {}): new_location = LingoLocation(world.player, location.name, location.code, new_region) - new_location.access_rule = make_location_lambda(location, room.name, world, player_logic) + new_location.access_rule = make_location_lambda(location, world, player_logic) new_region.locations.append(new_location) - if location.name in player_logic.EVENT_LOC_TO_ITEM: - event_name = player_logic.EVENT_LOC_TO_ITEM[location.name] + if location.name in player_logic.event_loc_to_item: + event_name = player_logic.event_loc_to_item[location.name] event_item = LingoItem(event_name, ItemClassification.progression, None, world.player) new_location.place_locked_item(event_item) @@ -31,7 +31,22 @@ def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_ source_region.connect( target_region, "Pilgrimage", - lambda state: lingo_can_use_pilgrimage(state, world.player, player_logic)) + lambda state: lingo_can_use_pilgrimage(state, world, player_logic)) + + +def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str, + door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic): + connection = Entrance(world.player, description, source_region) + connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world, player_logic) + + source_region.exits.append(connection) + connection.connect(target_region) + + if door is not None: + effective_room = target_region.name if door.room is None else door.room + if door.door not in player_logic.item_by_door.get(effective_room, {}): + for region in player_logic.calculate_door_requirements(effective_room, door.door, world).rooms: + world.multiworld.register_indirect_condition(regions[region], connection) def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld", @@ -41,11 +56,10 @@ def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str target_region = regions[target_painting.room] source_region = regions[source_painting.room] - source_region.connect( - target_region, - f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)", - lambda state: lingo_can_use_entrance(state, target_painting.room, source_painting.required_door, world.player, - player_logic)) + + entrance_name = f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)" + connect_entrance(regions, source_region, target_region, entrance_name, source_painting.required_door, world, + player_logic) def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: @@ -74,10 +88,8 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: else: entrance_name += f" (through {room.name} - {entrance.door.door})" - regions[entrance.room].connect( - regions[room.name], entrance_name, - lambda state, r=room, e=entrance: lingo_can_use_entrance(state, r.name, e.door, world.player, - player_logic)) + connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world, + player_logic) handle_pilgrim_room(regions, world, player_logic) @@ -85,7 +97,7 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways") if painting_shuffle: - for warp_enter, warp_exit in player_logic.PAINTING_MAPPING.items(): + for warp_enter, warp_exit in player_logic.painting_mapping.items(): connect_painting(regions, warp_enter, warp_exit, world, player_logic) world.multiworld.regions += regions.values() diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index d59b8a1ef78a..ee9dcc41929f 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -1,23 +1,23 @@ from typing import TYPE_CHECKING from BaseClasses import CollectionState -from .options import VictoryCondition -from .player_logic import LingoPlayerLogic, PlayerLocation -from .static_logic import PANELS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor +from .player_logic import AccessRequirements, LingoPlayerLogic, PlayerLocation +from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor if TYPE_CHECKING: from . import LingoWorld -def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, player: int, +def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld", player_logic: LingoPlayerLogic): if door is None: return True - return _lingo_can_open_door(state, room, room if door.room is None else door.room, door.door, player, player_logic) + effective_room = room if door.room is None else door.room + return _lingo_can_open_door(state, effective_room, door.door, world, player_logic) -def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: LingoPlayerLogic): +def lingo_can_use_pilgrimage(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): fake_pilgrimage = [ ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"], @@ -28,77 +28,77 @@ def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: ["Outside The Agreeable", "Tenacious Entrance"] ] for entrance in fake_pilgrimage: - if not state.has(player_logic.ITEM_BY_DOOR[entrance[0]][entrance[1]], player): + if not _lingo_can_open_door(state, entrance[0], entrance[1], world, player_logic): return False return True -def lingo_can_use_location(state: CollectionState, location: PlayerLocation, room_name: str, world: "LingoWorld", +def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): - for panel in location.panels: - panel_room = room_name if panel.room is None else panel.room - if not _lingo_can_solve_panel(state, room_name, panel_room, panel.panel, world, player_logic): - return False - - return True - + return _lingo_can_satisfy_requirements(state, location.access, world, player_logic) -def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): - return state.has("Mastery Achievement", world.player, world.options.mastery_achievements.value) - - -def _lingo_can_open_door(state: CollectionState, start_room: str, room: str, door: str, player: int, - player_logic: LingoPlayerLogic): - """ - Determines whether a door can be opened - """ - item_name = player_logic.ITEM_BY_DOOR[room][door] - if item_name in PROGRESSIVE_ITEMS: - progression = PROGRESSION_BY_ROOM[room][door] - return state.has(item_name, player, progression.index) - return state.has(item_name, player) +def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): + satisfied_count = 0 + for access_req in player_logic.mastery_reqs: + if _lingo_can_satisfy_requirements(state, access_req, world, player_logic): + satisfied_count += 1 + return satisfied_count >= world.options.mastery_achievements.value -def _lingo_can_solve_panel(state: CollectionState, start_room: str, room: str, panel: str, world: "LingoWorld", - player_logic: LingoPlayerLogic): - """ - Determines whether a panel can be solved - """ - if start_room != room and not state.can_reach(room, "Region", world.player): - return False +def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): + counted_panels = 0 + state.update_reachable_regions(world.player) + for region in state.reachable_regions[world.player]: + for access_req, panel_count in player_logic.counting_panel_reqs.get(region.name, []): + if _lingo_can_satisfy_requirements(state, access_req, world, player_logic): + counted_panels += panel_count + if counted_panels >= world.options.level_2_requirement.value - 1: + return True + return False - if room == "Second Room" and panel == "ANOTHER TRY" \ - and world.options.victory_condition == VictoryCondition.option_level_2 \ - and not state.has("Counting Panel Solved", world.player, world.options.level_2_requirement.value - 1): - return False - panel_object = PANELS_BY_ROOM[room][panel] - for req_room in panel_object.required_rooms: +def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld", + player_logic: LingoPlayerLogic): + for req_room in access.rooms: if not state.can_reach(req_room, "Region", world.player): return False - for req_door in panel_object.required_doors: - if not _lingo_can_open_door(state, start_room, room if req_door.room is None else req_door.room, - req_door.door, world.player, player_logic): - return False - - for req_panel in panel_object.required_panels: - if not _lingo_can_solve_panel(state, start_room, room if req_panel.room is None else req_panel.room, - req_panel.panel, world, player_logic): + for req_door in access.doors: + if not _lingo_can_open_door(state, req_door.room, req_door.door, world, player_logic): return False - if len(panel_object.colors) > 0 and world.options.shuffle_colors: - for color in panel_object.colors: + if len(access.colors) > 0 and world.options.shuffle_colors: + for color in access.colors: if not state.has(color.capitalize(), world.player): return False return True -def make_location_lambda(location: PlayerLocation, room_name: str, world: "LingoWorld", player_logic: LingoPlayerLogic): - if location.name == player_logic.MASTERY_LOCATION: - return lambda state: lingo_can_use_mastery_location(state, world) +def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld", + player_logic: LingoPlayerLogic): + """ + Determines whether a door can be opened + """ + if door not in player_logic.item_by_door.get(room, {}): + return _lingo_can_satisfy_requirements(state, player_logic.door_reqs[room][door], world, player_logic) + + item_name = player_logic.item_by_door[room][door] + if item_name in PROGRESSIVE_ITEMS: + progression = PROGRESSION_BY_ROOM[room][door] + return state.has(item_name, world.player, progression.index) + + return state.has(item_name, world.player) + + +def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): + if location.name == player_logic.mastery_location: + return lambda state: lingo_can_use_mastery_location(state, world, player_logic) + + if world.options.level_2_requirement > 1\ + and (location.name == "Second Room - ANOTHER TRY" or location.name == player_logic.level_2_location): + return lambda state: lingo_can_use_level_2_location(state, world, player_logic) - return lambda state: lingo_can_use_location(state, location, room_name, world, player_logic) + return lambda state: lingo_can_use_location(state, location, world, player_logic) diff --git a/worlds/lingo/test/TestPanelsanity.py b/worlds/lingo/test/TestPanelsanity.py new file mode 100644 index 000000000000..34c1b3815a46 --- /dev/null +++ b/worlds/lingo/test/TestPanelsanity.py @@ -0,0 +1,19 @@ +from . import LingoTestBase + + +class TestPanelHunt(LingoTestBase): + options = { + "shuffle_doors": "complex", + "location_checks": "insanity", + "victory_condition": "level_2", + "level_2_requirement": "15" + } + + def test_another_try(self) -> None: + self.collect_by_name("The Traveled - Entrance") # idk why this is needed + self.assertFalse(self.can_reach_location("Second Room - ANOTHER TRY")) + self.assertFalse(self.can_reach_location("Second Room - Unlock Level 2")) + + self.collect_by_name("Second Room - Exit Door") + self.assertTrue(self.can_reach_location("Second Room - ANOTHER TRY")) + self.assertTrue(self.can_reach_location("Second Room - Unlock Level 2")) From ba5327814799aa3d7983d5d161a4e9d50d923b3d Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Sat, 25 Nov 2023 13:53:02 +0100 Subject: [PATCH 086/104] core: make option resolution in world tests deterministic (#2471) Co-authored-by: Zach Parks --- test/bases.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/bases.py b/test/bases.py index 2054c2d18725..d6a43c598ffb 100644 --- a/test/bases.py +++ b/test/bases.py @@ -1,8 +1,10 @@ +import random import sys import typing import unittest from argparse import Namespace +from Generate import get_seed_name from test.general import gen_steps from worlds import AutoWorld from worlds.AutoWorld import call_all @@ -152,6 +154,8 @@ def world_setup(self, seed: typing.Optional[int] = None) -> None: self.multiworld.player_name = {1: "Tester"} self.multiworld.set_seed(seed) self.multiworld.state = CollectionState(self.multiworld) + random.seed(self.multiworld.seed) + self.multiworld.seed_name = get_seed_name(random) # only called to get same RNG progression as Generate.py args = Namespace() for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items(): setattr(args, name, { From 9afca87045a02fe60db07e25a82f40ebb46d8f7d Mon Sep 17 00:00:00 2001 From: David St-Louis Date: Sat, 25 Nov 2023 09:22:30 -0500 Subject: [PATCH 087/104] Heretic: implement new game (#2256) --- README.md | 2 +- docs/CODEOWNERS | 3 + worlds/heretic/Items.py | 1606 ++++++ worlds/heretic/Locations.py | 8229 +++++++++++++++++++++++++++++ worlds/heretic/Maps.py | 52 + worlds/heretic/Options.py | 167 + worlds/heretic/Regions.py | 894 ++++ worlds/heretic/Rules.py | 736 +++ worlds/heretic/__init__.py | 287 + worlds/heretic/docs/en_Heretic.md | 23 + worlds/heretic/docs/setup_en.md | 51 + 11 files changed, 12049 insertions(+), 1 deletion(-) create mode 100644 worlds/heretic/Items.py create mode 100644 worlds/heretic/Locations.py create mode 100644 worlds/heretic/Maps.py create mode 100644 worlds/heretic/Options.py create mode 100644 worlds/heretic/Regions.py create mode 100644 worlds/heretic/Rules.py create mode 100644 worlds/heretic/__init__.py create mode 100644 worlds/heretic/docs/en_Heretic.md create mode 100644 worlds/heretic/docs/setup_en.md diff --git a/README.md b/README.md index 2bca422fea90..b51fe00f9ac2 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Currently, the following games are supported: * Pokémon Emerald * DOOM II * Shivers - +* Heretic For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 6231da823234..c589b1333c9b 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -55,6 +55,9 @@ # Final Fantasy /worlds/ff1/ @jtoyoda +# Heretic +/worlds/heretic/ @Daivuk + # Hollow Knight /worlds/hk/ @BadMagic100 @ThePhar diff --git a/worlds/heretic/Items.py b/worlds/heretic/Items.py new file mode 100644 index 000000000000..a0907a3a3040 --- /dev/null +++ b/worlds/heretic/Items.py @@ -0,0 +1,1606 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from BaseClasses import ItemClassification +from typing import TypedDict, Dict, Set + + +class ItemDict(TypedDict, total=False): + classification: ItemClassification + count: int + name: str + doom_type: int # Unique numerical id used to spawn the item. -1 is level item, -2 is level complete item. + episode: int # Relevant if that item targets a specific level, like keycard or map reveal pickup. + map: int + + +item_table: Dict[int, ItemDict] = { + 370000: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Gauntlets of the Necromancer', + 'doom_type': 2005, + 'episode': -1, + 'map': -1}, + 370001: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ethereal Crossbow', + 'doom_type': 2001, + 'episode': -1, + 'map': -1}, + 370002: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Dragon Claw', + 'doom_type': 53, + 'episode': -1, + 'map': -1}, + 370003: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Phoenix Rod', + 'doom_type': 2003, + 'episode': -1, + 'map': -1}, + 370004: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Firemace', + 'doom_type': 2002, + 'episode': -1, + 'map': -1}, + 370005: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Hellstaff', + 'doom_type': 2004, + 'episode': -1, + 'map': -1}, + 370006: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Bag of Holding', + 'doom_type': 8, + 'episode': -1, + 'map': -1}, + 370007: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Chaos Device', + 'doom_type': 36, + 'episode': -1, + 'map': -1}, + 370008: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Morph Ovum', + 'doom_type': 30, + 'episode': -1, + 'map': -1}, + 370009: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Mystic Urn', + 'doom_type': 32, + 'episode': -1, + 'map': -1}, + 370010: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Quartz Flask', + 'doom_type': 82, + 'episode': -1, + 'map': -1}, + 370011: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Ring of Invincibility', + 'doom_type': 84, + 'episode': -1, + 'map': -1}, + 370012: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Shadowsphere', + 'doom_type': 75, + 'episode': -1, + 'map': -1}, + 370013: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Timebomb of the Ancients', + 'doom_type': 34, + 'episode': -1, + 'map': -1}, + 370014: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Tome of Power', + 'doom_type': 86, + 'episode': -1, + 'map': -1}, + 370015: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Torch', + 'doom_type': 33, + 'episode': -1, + 'map': -1}, + 370016: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Silver Shield', + 'doom_type': 85, + 'episode': -1, + 'map': -1}, + 370017: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Enchanted Shield', + 'doom_type': 31, + 'episode': -1, + 'map': -1}, + 370018: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Crystal Geode', + 'doom_type': 12, + 'episode': -1, + 'map': -1}, + 370019: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Energy Orb', + 'doom_type': 55, + 'episode': -1, + 'map': -1}, + 370020: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Greater Runes', + 'doom_type': 21, + 'episode': -1, + 'map': -1}, + 370021: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Inferno Orb', + 'doom_type': 23, + 'episode': -1, + 'map': -1}, + 370022: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Pile of Mace Spheres', + 'doom_type': 16, + 'episode': -1, + 'map': -1}, + 370023: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Quiver of Ethereal Arrows', + 'doom_type': 19, + 'episode': -1, + 'map': -1}, + 370200: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Docks (E1M1) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 1}, + 370201: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Dungeons (E1M2) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 2}, + 370202: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Dungeons (E1M2) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 2}, + 370203: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Dungeons (E1M2) - Blue key', + 'doom_type': 79, + 'episode': 1, + 'map': 2}, + 370204: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gatehouse (E1M3) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 3}, + 370205: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gatehouse (E1M3) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 3}, + 370206: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Guard Tower (E1M4) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 4}, + 370207: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Guard Tower (E1M4) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 4}, + 370208: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (E1M5) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 5}, + 370209: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (E1M5) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 5}, + 370210: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (E1M5) - Blue key', + 'doom_type': 79, + 'episode': 1, + 'map': 5}, + 370211: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cathedral (E1M6) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 6}, + 370212: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cathedral (E1M6) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 6}, + 370213: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crypts (E1M7) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 7}, + 370214: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crypts (E1M7) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 7}, + 370215: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crypts (E1M7) - Blue key', + 'doom_type': 79, + 'episode': 1, + 'map': 7}, + 370216: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Graveyard (E1M9) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 9}, + 370217: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Graveyard (E1M9) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 9}, + 370218: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Graveyard (E1M9) - Blue key', + 'doom_type': 79, + 'episode': 1, + 'map': 9}, + 370219: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crater (E2M1) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 1}, + 370220: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crater (E2M1) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 1}, + 370221: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Lava Pits (E2M2) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 2}, + 370222: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Lava Pits (E2M2) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 2}, + 370223: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The River of Fire (E2M3) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 3}, + 370224: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The River of Fire (E2M3) - Blue key', + 'doom_type': 79, + 'episode': 2, + 'map': 3}, + 370225: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The River of Fire (E2M3) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 3}, + 370226: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ice Grotto (E2M4) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 4}, + 370227: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ice Grotto (E2M4) - Blue key', + 'doom_type': 79, + 'episode': 2, + 'map': 4}, + 370228: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ice Grotto (E2M4) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 4}, + 370229: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (E2M5) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 5}, + 370230: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (E2M5) - Blue key', + 'doom_type': 79, + 'episode': 2, + 'map': 5}, + 370231: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (E2M5) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 5}, + 370232: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Labyrinth (E2M6) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 6}, + 370233: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Labyrinth (E2M6) - Blue key', + 'doom_type': 79, + 'episode': 2, + 'map': 6}, + 370234: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Labyrinth (E2M6) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 6}, + 370235: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Great Hall (E2M7) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 7}, + 370236: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Great Hall (E2M7) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 7}, + 370237: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Great Hall (E2M7) - Blue key', + 'doom_type': 79, + 'episode': 2, + 'map': 7}, + 370238: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Glacier (E2M9) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 9}, + 370239: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Glacier (E2M9) - Blue key', + 'doom_type': 79, + 'episode': 2, + 'map': 9}, + 370240: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Glacier (E2M9) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 9}, + 370241: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Storehouse (E3M1) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 1}, + 370242: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Storehouse (E3M1) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 1}, + 370243: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cesspool (E3M2) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 2}, + 370244: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cesspool (E3M2) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 2}, + 370245: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cesspool (E3M2) - Blue key', + 'doom_type': 79, + 'episode': 3, + 'map': 2}, + 370246: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Confluence (E3M3) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 3}, + 370247: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Confluence (E3M3) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 3}, + 370248: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Confluence (E3M3) - Blue key', + 'doom_type': 79, + 'episode': 3, + 'map': 3}, + 370249: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Azure Fortress (E3M4) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 4}, + 370250: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Azure Fortress (E3M4) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 4}, + 370251: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ophidian Lair (E3M5) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 5}, + 370252: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ophidian Lair (E3M5) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 5}, + 370253: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Halls of Fear (E3M6) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 6}, + 370254: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Halls of Fear (E3M6) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 6}, + 370255: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Halls of Fear (E3M6) - Blue key', + 'doom_type': 79, + 'episode': 3, + 'map': 6}, + 370256: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (E3M7) - Blue key', + 'doom_type': 79, + 'episode': 3, + 'map': 7}, + 370257: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (E3M7) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 7}, + 370258: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (E3M7) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 7}, + 370259: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Aquifier (E3M9) - Blue key', + 'doom_type': 79, + 'episode': 3, + 'map': 9}, + 370260: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Aquifier (E3M9) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 9}, + 370261: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Aquifier (E3M9) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 9}, + 370262: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Catafalque (E4M1) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 1}, + 370263: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Catafalque (E4M1) - Green key', + 'doom_type': 73, + 'episode': 4, + 'map': 1}, + 370264: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Blockhouse (E4M2) - Green key', + 'doom_type': 73, + 'episode': 4, + 'map': 2}, + 370265: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Blockhouse (E4M2) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 2}, + 370266: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Blockhouse (E4M2) - Blue key', + 'doom_type': 79, + 'episode': 4, + 'map': 2}, + 370267: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ambulatory (E4M3) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 3}, + 370268: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ambulatory (E4M3) - Green key', + 'doom_type': 73, + 'episode': 4, + 'map': 3}, + 370269: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ambulatory (E4M3) - Blue key', + 'doom_type': 79, + 'episode': 4, + 'map': 3}, + 370270: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Great Stair (E4M5) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 5}, + 370271: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Great Stair (E4M5) - Green key', + 'doom_type': 73, + 'episode': 4, + 'map': 5}, + 370272: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Great Stair (E4M5) - Blue key', + 'doom_type': 79, + 'episode': 4, + 'map': 5}, + 370273: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Halls of the Apostate (E4M6) - Green key', + 'doom_type': 73, + 'episode': 4, + 'map': 6}, + 370274: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Halls of the Apostate (E4M6) - Blue key', + 'doom_type': 79, + 'episode': 4, + 'map': 6}, + 370275: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Halls of the Apostate (E4M6) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 6}, + 370276: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ramparts of Perdition (E4M7) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 7}, + 370277: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ramparts of Perdition (E4M7) - Green key', + 'doom_type': 73, + 'episode': 4, + 'map': 7}, + 370278: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ramparts of Perdition (E4M7) - Blue key', + 'doom_type': 79, + 'episode': 4, + 'map': 7}, + 370279: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Shattered Bridge (E4M8) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 8}, + 370280: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Mausoleum (E4M9) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 9}, + 370281: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ochre Cliffs (E5M1) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 1}, + 370282: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ochre Cliffs (E5M1) - Blue key', + 'doom_type': 79, + 'episode': 5, + 'map': 1}, + 370283: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ochre Cliffs (E5M1) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 1}, + 370284: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Rapids (E5M2) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 2}, + 370285: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Rapids (E5M2) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 2}, + 370286: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Quay (E5M3) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 3}, + 370287: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Quay (E5M3) - Blue key', + 'doom_type': 79, + 'episode': 5, + 'map': 3}, + 370288: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Quay (E5M3) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 3}, + 370289: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Courtyard (E5M4) - Blue key', + 'doom_type': 79, + 'episode': 5, + 'map': 4}, + 370290: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Courtyard (E5M4) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 4}, + 370291: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Courtyard (E5M4) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 4}, + 370292: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Hydratyr (E5M5) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 5}, + 370293: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Hydratyr (E5M5) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 5}, + 370294: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Hydratyr (E5M5) - Blue key', + 'doom_type': 79, + 'episode': 5, + 'map': 5}, + 370295: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Colonnade (E5M6) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 6}, + 370296: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Colonnade (E5M6) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 6}, + 370297: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Colonnade (E5M6) - Blue key', + 'doom_type': 79, + 'episode': 5, + 'map': 6}, + 370298: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Foetid Manse (E5M7) - Blue key', + 'doom_type': 79, + 'episode': 5, + 'map': 7}, + 370299: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Foetid Manse (E5M7) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 7}, + 370300: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Foetid Manse (E5M7) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 7}, + 370301: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Skein of D'Sparil (E5M9) - Blue key", + 'doom_type': 79, + 'episode': 5, + 'map': 9}, + 370302: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Skein of D'Sparil (E5M9) - Green key", + 'doom_type': 73, + 'episode': 5, + 'map': 9}, + 370303: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Skein of D'Sparil (E5M9) - Yellow key", + 'doom_type': 80, + 'episode': 5, + 'map': 9}, + 370400: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Docks (E1M1)', + 'doom_type': -1, + 'episode': 1, + 'map': 1}, + 370401: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Docks (E1M1) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 1}, + 370402: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Docks (E1M1) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 1}, + 370403: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Dungeons (E1M2)', + 'doom_type': -1, + 'episode': 1, + 'map': 2}, + 370404: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Dungeons (E1M2) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 2}, + 370405: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Dungeons (E1M2) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 2}, + 370406: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gatehouse (E1M3)', + 'doom_type': -1, + 'episode': 1, + 'map': 3}, + 370407: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gatehouse (E1M3) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 3}, + 370408: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Gatehouse (E1M3) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 3}, + 370409: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Guard Tower (E1M4)', + 'doom_type': -1, + 'episode': 1, + 'map': 4}, + 370410: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Guard Tower (E1M4) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 4}, + 370411: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Guard Tower (E1M4) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 4}, + 370412: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (E1M5)', + 'doom_type': -1, + 'episode': 1, + 'map': 5}, + 370413: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (E1M5) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 5}, + 370414: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Citadel (E1M5) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 5}, + 370415: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cathedral (E1M6)', + 'doom_type': -1, + 'episode': 1, + 'map': 6}, + 370416: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cathedral (E1M6) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 6}, + 370417: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Cathedral (E1M6) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 6}, + 370418: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crypts (E1M7)', + 'doom_type': -1, + 'episode': 1, + 'map': 7}, + 370419: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crypts (E1M7) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 7}, + 370420: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Crypts (E1M7) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 7}, + 370421: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Hell's Maw (E1M8)", + 'doom_type': -1, + 'episode': 1, + 'map': 8}, + 370422: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Hell's Maw (E1M8) - Complete", + 'doom_type': -2, + 'episode': 1, + 'map': 8}, + 370423: {'classification': ItemClassification.filler, + 'count': 1, + 'name': "Hell's Maw (E1M8) - Map Scroll", + 'doom_type': 35, + 'episode': 1, + 'map': 8}, + 370424: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Graveyard (E1M9)', + 'doom_type': -1, + 'episode': 1, + 'map': 9}, + 370425: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Graveyard (E1M9) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 9}, + 370426: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Graveyard (E1M9) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 9}, + 370427: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crater (E2M1)', + 'doom_type': -1, + 'episode': 2, + 'map': 1}, + 370428: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crater (E2M1) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 1}, + 370429: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Crater (E2M1) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 1}, + 370430: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Lava Pits (E2M2)', + 'doom_type': -1, + 'episode': 2, + 'map': 2}, + 370431: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Lava Pits (E2M2) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 2}, + 370432: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Lava Pits (E2M2) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 2}, + 370433: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The River of Fire (E2M3)', + 'doom_type': -1, + 'episode': 2, + 'map': 3}, + 370434: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The River of Fire (E2M3) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 3}, + 370435: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The River of Fire (E2M3) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 3}, + 370436: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ice Grotto (E2M4)', + 'doom_type': -1, + 'episode': 2, + 'map': 4}, + 370437: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ice Grotto (E2M4) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 4}, + 370438: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Ice Grotto (E2M4) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 4}, + 370439: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (E2M5)', + 'doom_type': -1, + 'episode': 2, + 'map': 5}, + 370440: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (E2M5) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 5}, + 370441: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Catacombs (E2M5) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 5}, + 370442: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Labyrinth (E2M6)', + 'doom_type': -1, + 'episode': 2, + 'map': 6}, + 370443: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Labyrinth (E2M6) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 6}, + 370444: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Labyrinth (E2M6) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 6}, + 370445: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Great Hall (E2M7)', + 'doom_type': -1, + 'episode': 2, + 'map': 7}, + 370446: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Great Hall (E2M7) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 7}, + 370447: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Great Hall (E2M7) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 7}, + 370448: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Portals of Chaos (E2M8)', + 'doom_type': -1, + 'episode': 2, + 'map': 8}, + 370449: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Portals of Chaos (E2M8) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 8}, + 370450: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Portals of Chaos (E2M8) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 8}, + 370451: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Glacier (E2M9)', + 'doom_type': -1, + 'episode': 2, + 'map': 9}, + 370452: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Glacier (E2M9) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 9}, + 370453: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Glacier (E2M9) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 9}, + 370454: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Storehouse (E3M1)', + 'doom_type': -1, + 'episode': 3, + 'map': 1}, + 370455: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Storehouse (E3M1) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 1}, + 370456: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Storehouse (E3M1) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 1}, + 370457: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cesspool (E3M2)', + 'doom_type': -1, + 'episode': 3, + 'map': 2}, + 370458: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cesspool (E3M2) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 2}, + 370459: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Cesspool (E3M2) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 2}, + 370460: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Confluence (E3M3)', + 'doom_type': -1, + 'episode': 3, + 'map': 3}, + 370461: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Confluence (E3M3) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 3}, + 370462: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Confluence (E3M3) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 3}, + 370463: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Azure Fortress (E3M4)', + 'doom_type': -1, + 'episode': 3, + 'map': 4}, + 370464: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Azure Fortress (E3M4) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 4}, + 370465: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Azure Fortress (E3M4) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 4}, + 370466: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ophidian Lair (E3M5)', + 'doom_type': -1, + 'episode': 3, + 'map': 5}, + 370467: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ophidian Lair (E3M5) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 5}, + 370468: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Ophidian Lair (E3M5) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 5}, + 370469: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Halls of Fear (E3M6)', + 'doom_type': -1, + 'episode': 3, + 'map': 6}, + 370470: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Halls of Fear (E3M6) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 6}, + 370471: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Halls of Fear (E3M6) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 6}, + 370472: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (E3M7)', + 'doom_type': -1, + 'episode': 3, + 'map': 7}, + 370473: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (E3M7) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 7}, + 370474: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Chasm (E3M7) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 7}, + 370475: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "D'Sparil'S Keep (E3M8)", + 'doom_type': -1, + 'episode': 3, + 'map': 8}, + 370476: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "D'Sparil'S Keep (E3M8) - Complete", + 'doom_type': -2, + 'episode': 3, + 'map': 8}, + 370477: {'classification': ItemClassification.filler, + 'count': 1, + 'name': "D'Sparil'S Keep (E3M8) - Map Scroll", + 'doom_type': 35, + 'episode': 3, + 'map': 8}, + 370478: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Aquifier (E3M9)', + 'doom_type': -1, + 'episode': 3, + 'map': 9}, + 370479: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Aquifier (E3M9) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 9}, + 370480: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Aquifier (E3M9) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 9}, + 370481: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Catafalque (E4M1)', + 'doom_type': -1, + 'episode': 4, + 'map': 1}, + 370482: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Catafalque (E4M1) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 1}, + 370483: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Catafalque (E4M1) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 1}, + 370484: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Blockhouse (E4M2)', + 'doom_type': -1, + 'episode': 4, + 'map': 2}, + 370485: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Blockhouse (E4M2) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 2}, + 370486: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Blockhouse (E4M2) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 2}, + 370487: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ambulatory (E4M3)', + 'doom_type': -1, + 'episode': 4, + 'map': 3}, + 370488: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ambulatory (E4M3) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 3}, + 370489: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Ambulatory (E4M3) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 3}, + 370490: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Sepulcher (E4M4)', + 'doom_type': -1, + 'episode': 4, + 'map': 4}, + 370491: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Sepulcher (E4M4) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 4}, + 370492: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Sepulcher (E4M4) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 4}, + 370493: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Great Stair (E4M5)', + 'doom_type': -1, + 'episode': 4, + 'map': 5}, + 370494: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Great Stair (E4M5) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 5}, + 370495: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Great Stair (E4M5) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 5}, + 370496: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Halls of the Apostate (E4M6)', + 'doom_type': -1, + 'episode': 4, + 'map': 6}, + 370497: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Halls of the Apostate (E4M6) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 6}, + 370498: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Halls of the Apostate (E4M6) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 6}, + 370499: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ramparts of Perdition (E4M7)', + 'doom_type': -1, + 'episode': 4, + 'map': 7}, + 370500: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ramparts of Perdition (E4M7) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 7}, + 370501: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Ramparts of Perdition (E4M7) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 7}, + 370502: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Shattered Bridge (E4M8)', + 'doom_type': -1, + 'episode': 4, + 'map': 8}, + 370503: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Shattered Bridge (E4M8) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 8}, + 370504: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Shattered Bridge (E4M8) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 8}, + 370505: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Mausoleum (E4M9)', + 'doom_type': -1, + 'episode': 4, + 'map': 9}, + 370506: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Mausoleum (E4M9) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 9}, + 370507: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Mausoleum (E4M9) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 9}, + 370508: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ochre Cliffs (E5M1)', + 'doom_type': -1, + 'episode': 5, + 'map': 1}, + 370509: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ochre Cliffs (E5M1) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 1}, + 370510: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Ochre Cliffs (E5M1) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 1}, + 370511: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Rapids (E5M2)', + 'doom_type': -1, + 'episode': 5, + 'map': 2}, + 370512: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Rapids (E5M2) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 2}, + 370513: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Rapids (E5M2) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 2}, + 370514: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Quay (E5M3)', + 'doom_type': -1, + 'episode': 5, + 'map': 3}, + 370515: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Quay (E5M3) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 3}, + 370516: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Quay (E5M3) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 3}, + 370517: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Courtyard (E5M4)', + 'doom_type': -1, + 'episode': 5, + 'map': 4}, + 370518: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Courtyard (E5M4) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 4}, + 370519: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Courtyard (E5M4) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 4}, + 370520: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Hydratyr (E5M5)', + 'doom_type': -1, + 'episode': 5, + 'map': 5}, + 370521: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Hydratyr (E5M5) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 5}, + 370522: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Hydratyr (E5M5) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 5}, + 370523: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Colonnade (E5M6)', + 'doom_type': -1, + 'episode': 5, + 'map': 6}, + 370524: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Colonnade (E5M6) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 6}, + 370525: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Colonnade (E5M6) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 6}, + 370526: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Foetid Manse (E5M7)', + 'doom_type': -1, + 'episode': 5, + 'map': 7}, + 370527: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Foetid Manse (E5M7) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 7}, + 370528: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Foetid Manse (E5M7) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 7}, + 370529: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Field of Judgement (E5M8)', + 'doom_type': -1, + 'episode': 5, + 'map': 8}, + 370530: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Field of Judgement (E5M8) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 8}, + 370531: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Field of Judgement (E5M8) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 8}, + 370532: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Skein of D'Sparil (E5M9)", + 'doom_type': -1, + 'episode': 5, + 'map': 9}, + 370533: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Skein of D'Sparil (E5M9) - Complete", + 'doom_type': -2, + 'episode': 5, + 'map': 9}, + 370534: {'classification': ItemClassification.filler, + 'count': 1, + 'name': "Skein of D'Sparil (E5M9) - Map Scroll", + 'doom_type': 35, + 'episode': 5, + 'map': 9}, +} + + +item_name_groups: Dict[str, Set[str]] = { + 'Ammos': {'Crystal Geode', 'Energy Orb', 'Greater Runes', 'Inferno Orb', 'Pile of Mace Spheres', 'Quiver of Ethereal Arrows', }, + 'Armors': {'Enchanted Shield', 'Silver Shield', }, + 'Artifacts': {'Chaos Device', 'Morph Ovum', 'Mystic Urn', 'Quartz Flask', 'Ring of Invincibility', 'Shadowsphere', 'Timebomb of the Ancients', 'Tome of Power', 'Torch', }, + 'Keys': {'Ambulatory (E4M3) - Blue key', 'Ambulatory (E4M3) - Green key', 'Ambulatory (E4M3) - Yellow key', 'Blockhouse (E4M2) - Blue key', 'Blockhouse (E4M2) - Green key', 'Blockhouse (E4M2) - Yellow key', 'Catafalque (E4M1) - Green key', 'Catafalque (E4M1) - Yellow key', 'Colonnade (E5M6) - Blue key', 'Colonnade (E5M6) - Green key', 'Colonnade (E5M6) - Yellow key', 'Courtyard (E5M4) - Blue key', 'Courtyard (E5M4) - Green key', 'Courtyard (E5M4) - Yellow key', 'Foetid Manse (E5M7) - Blue key', 'Foetid Manse (E5M7) - Green key', 'Foetid Manse (E5M7) - Yellow key', 'Great Stair (E4M5) - Blue key', 'Great Stair (E4M5) - Green key', 'Great Stair (E4M5) - Yellow key', 'Halls of the Apostate (E4M6) - Blue key', 'Halls of the Apostate (E4M6) - Green key', 'Halls of the Apostate (E4M6) - Yellow key', 'Hydratyr (E5M5) - Blue key', 'Hydratyr (E5M5) - Green key', 'Hydratyr (E5M5) - Yellow key', 'Mausoleum (E4M9) - Yellow key', 'Ochre Cliffs (E5M1) - Blue key', 'Ochre Cliffs (E5M1) - Green key', 'Ochre Cliffs (E5M1) - Yellow key', 'Quay (E5M3) - Blue key', 'Quay (E5M3) - Green key', 'Quay (E5M3) - Yellow key', 'Ramparts of Perdition (E4M7) - Blue key', 'Ramparts of Perdition (E4M7) - Green key', 'Ramparts of Perdition (E4M7) - Yellow key', 'Rapids (E5M2) - Green key', 'Rapids (E5M2) - Yellow key', 'Shattered Bridge (E4M8) - Yellow key', "Skein of D'Sparil (E5M9) - Blue key", "Skein of D'Sparil (E5M9) - Green key", "Skein of D'Sparil (E5M9) - Yellow key", 'The Aquifier (E3M9) - Blue key', 'The Aquifier (E3M9) - Green key', 'The Aquifier (E3M9) - Yellow key', 'The Azure Fortress (E3M4) - Green key', 'The Azure Fortress (E3M4) - Yellow key', 'The Catacombs (E2M5) - Blue key', 'The Catacombs (E2M5) - Green key', 'The Catacombs (E2M5) - Yellow key', 'The Cathedral (E1M6) - Green key', 'The Cathedral (E1M6) - Yellow key', 'The Cesspool (E3M2) - Blue key', 'The Cesspool (E3M2) - Green key', 'The Cesspool (E3M2) - Yellow key', 'The Chasm (E3M7) - Blue key', 'The Chasm (E3M7) - Green key', 'The Chasm (E3M7) - Yellow key', 'The Citadel (E1M5) - Blue key', 'The Citadel (E1M5) - Green key', 'The Citadel (E1M5) - Yellow key', 'The Confluence (E3M3) - Blue key', 'The Confluence (E3M3) - Green key', 'The Confluence (E3M3) - Yellow key', 'The Crater (E2M1) - Green key', 'The Crater (E2M1) - Yellow key', 'The Crypts (E1M7) - Blue key', 'The Crypts (E1M7) - Green key', 'The Crypts (E1M7) - Yellow key', 'The Docks (E1M1) - Yellow key', 'The Dungeons (E1M2) - Blue key', 'The Dungeons (E1M2) - Green key', 'The Dungeons (E1M2) - Yellow key', 'The Gatehouse (E1M3) - Green key', 'The Gatehouse (E1M3) - Yellow key', 'The Glacier (E2M9) - Blue key', 'The Glacier (E2M9) - Green key', 'The Glacier (E2M9) - Yellow key', 'The Graveyard (E1M9) - Blue key', 'The Graveyard (E1M9) - Green key', 'The Graveyard (E1M9) - Yellow key', 'The Great Hall (E2M7) - Blue key', 'The Great Hall (E2M7) - Green key', 'The Great Hall (E2M7) - Yellow key', 'The Guard Tower (E1M4) - Green key', 'The Guard Tower (E1M4) - Yellow key', 'The Halls of Fear (E3M6) - Blue key', 'The Halls of Fear (E3M6) - Green key', 'The Halls of Fear (E3M6) - Yellow key', 'The Ice Grotto (E2M4) - Blue key', 'The Ice Grotto (E2M4) - Green key', 'The Ice Grotto (E2M4) - Yellow key', 'The Labyrinth (E2M6) - Blue key', 'The Labyrinth (E2M6) - Green key', 'The Labyrinth (E2M6) - Yellow key', 'The Lava Pits (E2M2) - Green key', 'The Lava Pits (E2M2) - Yellow key', 'The Ophidian Lair (E3M5) - Green key', 'The Ophidian Lair (E3M5) - Yellow key', 'The River of Fire (E2M3) - Blue key', 'The River of Fire (E2M3) - Green key', 'The River of Fire (E2M3) - Yellow key', 'The Storehouse (E3M1) - Green key', 'The Storehouse (E3M1) - Yellow key', }, + 'Levels': {'Ambulatory (E4M3)', 'Blockhouse (E4M2)', 'Catafalque (E4M1)', 'Colonnade (E5M6)', 'Courtyard (E5M4)', "D'Sparil'S Keep (E3M8)", 'Field of Judgement (E5M8)', 'Foetid Manse (E5M7)', 'Great Stair (E4M5)', 'Halls of the Apostate (E4M6)', "Hell's Maw (E1M8)", 'Hydratyr (E5M5)', 'Mausoleum (E4M9)', 'Ochre Cliffs (E5M1)', 'Quay (E5M3)', 'Ramparts of Perdition (E4M7)', 'Rapids (E5M2)', 'Sepulcher (E4M4)', 'Shattered Bridge (E4M8)', "Skein of D'Sparil (E5M9)", 'The Aquifier (E3M9)', 'The Azure Fortress (E3M4)', 'The Catacombs (E2M5)', 'The Cathedral (E1M6)', 'The Cesspool (E3M2)', 'The Chasm (E3M7)', 'The Citadel (E1M5)', 'The Confluence (E3M3)', 'The Crater (E2M1)', 'The Crypts (E1M7)', 'The Docks (E1M1)', 'The Dungeons (E1M2)', 'The Gatehouse (E1M3)', 'The Glacier (E2M9)', 'The Graveyard (E1M9)', 'The Great Hall (E2M7)', 'The Guard Tower (E1M4)', 'The Halls of Fear (E3M6)', 'The Ice Grotto (E2M4)', 'The Labyrinth (E2M6)', 'The Lava Pits (E2M2)', 'The Ophidian Lair (E3M5)', 'The Portals of Chaos (E2M8)', 'The River of Fire (E2M3)', 'The Storehouse (E3M1)', }, + 'Map Scrolls': {'Ambulatory (E4M3) - Map Scroll', 'Blockhouse (E4M2) - Map Scroll', 'Catafalque (E4M1) - Map Scroll', 'Colonnade (E5M6) - Map Scroll', 'Courtyard (E5M4) - Map Scroll', "D'Sparil'S Keep (E3M8) - Map Scroll", 'Field of Judgement (E5M8) - Map Scroll', 'Foetid Manse (E5M7) - Map Scroll', 'Great Stair (E4M5) - Map Scroll', 'Halls of the Apostate (E4M6) - Map Scroll', "Hell's Maw (E1M8) - Map Scroll", 'Hydratyr (E5M5) - Map Scroll', 'Mausoleum (E4M9) - Map Scroll', 'Ochre Cliffs (E5M1) - Map Scroll', 'Quay (E5M3) - Map Scroll', 'Ramparts of Perdition (E4M7) - Map Scroll', 'Rapids (E5M2) - Map Scroll', 'Sepulcher (E4M4) - Map Scroll', 'Shattered Bridge (E4M8) - Map Scroll', "Skein of D'Sparil (E5M9) - Map Scroll", 'The Aquifier (E3M9) - Map Scroll', 'The Azure Fortress (E3M4) - Map Scroll', 'The Catacombs (E2M5) - Map Scroll', 'The Cathedral (E1M6) - Map Scroll', 'The Cesspool (E3M2) - Map Scroll', 'The Chasm (E3M7) - Map Scroll', 'The Citadel (E1M5) - Map Scroll', 'The Confluence (E3M3) - Map Scroll', 'The Crater (E2M1) - Map Scroll', 'The Crypts (E1M7) - Map Scroll', 'The Docks (E1M1) - Map Scroll', 'The Dungeons (E1M2) - Map Scroll', 'The Gatehouse (E1M3) - Map Scroll', 'The Glacier (E2M9) - Map Scroll', 'The Graveyard (E1M9) - Map Scroll', 'The Great Hall (E2M7) - Map Scroll', 'The Guard Tower (E1M4) - Map Scroll', 'The Halls of Fear (E3M6) - Map Scroll', 'The Ice Grotto (E2M4) - Map Scroll', 'The Labyrinth (E2M6) - Map Scroll', 'The Lava Pits (E2M2) - Map Scroll', 'The Ophidian Lair (E3M5) - Map Scroll', 'The Portals of Chaos (E2M8) - Map Scroll', 'The River of Fire (E2M3) - Map Scroll', 'The Storehouse (E3M1) - Map Scroll', }, + 'Weapons': {'Dragon Claw', 'Ethereal Crossbow', 'Firemace', 'Gauntlets of the Necromancer', 'Hellstaff', 'Phoenix Rod', }, +} diff --git a/worlds/heretic/Locations.py b/worlds/heretic/Locations.py new file mode 100644 index 000000000000..f9590de77660 --- /dev/null +++ b/worlds/heretic/Locations.py @@ -0,0 +1,8229 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import Dict, TypedDict, List, Set + + +class LocationDict(TypedDict, total=False): + name: str + episode: int + check_sanity: bool + map: int + index: int # Thing index as it is stored in the wad file. + doom_type: int # In case index end up unreliable, we can use doom type. Maps have often only one of each important things. + region: str + + +location_table: Dict[int, LocationDict] = { + 371000: {'name': 'The Docks (E1M1) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': 5, + 'doom_type': 80, + 'region': "The Docks (E1M1) Main"}, + 371001: {'name': 'The Docks (E1M1) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': 47, + 'doom_type': 85, + 'region': "The Docks (E1M1) Main"}, + 371002: {'name': 'The Docks (E1M1) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': 52, + 'doom_type': 2005, + 'region': "The Docks (E1M1) Yellow"}, + 371003: {'name': 'The Docks (E1M1) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': 55, + 'doom_type': 2001, + 'region': "The Docks (E1M1) Yellow"}, + 371004: {'name': 'The Docks (E1M1) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': 91, + 'doom_type': 8, + 'region': "The Docks (E1M1) Sea"}, + 371005: {'name': 'The Docks (E1M1) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': 174, + 'doom_type': 86, + 'region': "The Docks (E1M1) Yellow"}, + 371006: {'name': 'The Docks (E1M1) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "The Docks (E1M1) Yellow"}, + 371007: {'name': 'The Dungeons (E1M2) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 1, + 'doom_type': 53, + 'region': "The Dungeons (E1M2) Yellow"}, + 371008: {'name': 'The Dungeons (E1M2) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 5, + 'doom_type': 80, + 'region': "The Dungeons (E1M2) Main"}, + 371009: {'name': 'The Dungeons (E1M2) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 17, + 'doom_type': 73, + 'region': "The Dungeons (E1M2) Yellow"}, + 371010: {'name': 'The Dungeons (E1M2) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 18, + 'doom_type': 85, + 'region': "The Dungeons (E1M2) Main"}, + 371011: {'name': 'The Dungeons (E1M2) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 19, + 'doom_type': 33, + 'region': "The Dungeons (E1M2) Main"}, + 371012: {'name': 'The Dungeons (E1M2) - Map Scroll', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 29, + 'doom_type': 35, + 'region': "The Dungeons (E1M2) Yellow"}, + 371013: {'name': 'The Dungeons (E1M2) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 41, + 'doom_type': 75, + 'region': "The Dungeons (E1M2) Yellow"}, + 371014: {'name': 'The Dungeons (E1M2) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 44, + 'doom_type': 8, + 'region': "The Dungeons (E1M2) Green"}, + 371015: {'name': 'The Dungeons (E1M2) - Blue key', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 45, + 'doom_type': 79, + 'region': "The Dungeons (E1M2) Green"}, + 371016: {'name': 'The Dungeons (E1M2) - Ring of Invincibility', + 'episode': 1, + 'check_sanity': True, + 'map': 2, + 'index': 46, + 'doom_type': 84, + 'region': "The Dungeons (E1M2) Yellow"}, + 371017: {'name': 'The Dungeons (E1M2) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 77, + 'doom_type': 86, + 'region': "The Dungeons (E1M2) Main"}, + 371018: {'name': 'The Dungeons (E1M2) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 80, + 'doom_type': 2001, + 'region': "The Dungeons (E1M2) Main"}, + 371019: {'name': 'The Dungeons (E1M2) - Tome of Power 2', + 'episode': 1, + 'check_sanity': True, + 'map': 2, + 'index': 81, + 'doom_type': 86, + 'region': "The Dungeons (E1M2) Yellow"}, + 371020: {'name': 'The Dungeons (E1M2) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 253, + 'doom_type': 2005, + 'region': "The Dungeons (E1M2) Yellow"}, + 371021: {'name': 'The Dungeons (E1M2) - Silver Shield 2', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 303, + 'doom_type': 85, + 'region': "The Dungeons (E1M2) Yellow"}, + 371022: {'name': 'The Dungeons (E1M2) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "The Dungeons (E1M2) Blue"}, + 371023: {'name': 'The Gatehouse (E1M3) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 8, + 'doom_type': 80, + 'region': "The Gatehouse (E1M3) Main"}, + 371024: {'name': 'The Gatehouse (E1M3) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 9, + 'doom_type': 73, + 'region': "The Gatehouse (E1M3) Yellow"}, + 371025: {'name': 'The Gatehouse (E1M3) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 10, + 'doom_type': 53, + 'region': "The Gatehouse (E1M3) Main"}, + 371026: {'name': 'The Gatehouse (E1M3) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 22, + 'doom_type': 85, + 'region': "The Gatehouse (E1M3) Main"}, + 371027: {'name': 'The Gatehouse (E1M3) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 24, + 'doom_type': 2001, + 'region': "The Gatehouse (E1M3) Main"}, + 371028: {'name': 'The Gatehouse (E1M3) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 81, + 'doom_type': 86, + 'region': "The Gatehouse (E1M3) Sea"}, + 371029: {'name': 'The Gatehouse (E1M3) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 134, + 'doom_type': 8, + 'region': "The Gatehouse (E1M3) Yellow"}, + 371030: {'name': 'The Gatehouse (E1M3) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 145, + 'doom_type': 2005, + 'region': "The Gatehouse (E1M3) Yellow"}, + 371031: {'name': 'The Gatehouse (E1M3) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 203, + 'doom_type': 33, + 'region': "The Gatehouse (E1M3) Main"}, + 371032: {'name': 'The Gatehouse (E1M3) - Ring of Invincibility', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 220, + 'doom_type': 84, + 'region': "The Gatehouse (E1M3) Yellow"}, + 371033: {'name': 'The Gatehouse (E1M3) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 221, + 'doom_type': 75, + 'region': "The Gatehouse (E1M3) Main"}, + 371034: {'name': 'The Gatehouse (E1M3) - Morph Ovum', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 222, + 'doom_type': 30, + 'region': "The Gatehouse (E1M3) Yellow"}, + 371035: {'name': 'The Gatehouse (E1M3) - Tome of Power 2', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 286, + 'doom_type': 86, + 'region': "The Gatehouse (E1M3) Main"}, + 371036: {'name': 'The Gatehouse (E1M3) - Tome of Power 3', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 287, + 'doom_type': 86, + 'region': "The Gatehouse (E1M3) Main"}, + 371037: {'name': 'The Gatehouse (E1M3) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "The Gatehouse (E1M3) Green"}, + 371038: {'name': 'The Guard Tower (E1M4) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 0, + 'doom_type': 2005, + 'region': "The Guard Tower (E1M4) Main"}, + 371039: {'name': 'The Guard Tower (E1M4) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 2, + 'doom_type': 53, + 'region': "The Guard Tower (E1M4) Main"}, + 371040: {'name': 'The Guard Tower (E1M4) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 3, + 'doom_type': 2001, + 'region': "The Guard Tower (E1M4) Main"}, + 371041: {'name': 'The Guard Tower (E1M4) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 4, + 'doom_type': 80, + 'region': "The Guard Tower (E1M4) Main"}, + 371042: {'name': 'The Guard Tower (E1M4) - Morph Ovum', + 'episode': 1, + 'check_sanity': True, + 'map': 4, + 'index': 5, + 'doom_type': 30, + 'region': "The Guard Tower (E1M4) Main"}, + 371043: {'name': 'The Guard Tower (E1M4) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 57, + 'doom_type': 75, + 'region': "The Guard Tower (E1M4) Yellow"}, + 371044: {'name': 'The Guard Tower (E1M4) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 60, + 'doom_type': 73, + 'region': "The Guard Tower (E1M4) Yellow"}, + 371045: {'name': 'The Guard Tower (E1M4) - Bag of Holding', + 'episode': 1, + 'check_sanity': True, + 'map': 4, + 'index': 61, + 'doom_type': 8, + 'region': "The Guard Tower (E1M4) Main"}, + 371046: {'name': 'The Guard Tower (E1M4) - Map Scroll', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 64, + 'doom_type': 35, + 'region': "The Guard Tower (E1M4) Main"}, + 371047: {'name': 'The Guard Tower (E1M4) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 77, + 'doom_type': 86, + 'region': "The Guard Tower (E1M4) Main"}, + 371048: {'name': 'The Guard Tower (E1M4) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 78, + 'doom_type': 85, + 'region': "The Guard Tower (E1M4) Main"}, + 371049: {'name': 'The Guard Tower (E1M4) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 143, + 'doom_type': 33, + 'region': "The Guard Tower (E1M4) Main"}, + 371050: {'name': 'The Guard Tower (E1M4) - Tome of Power 2', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 220, + 'doom_type': 86, + 'region': "The Guard Tower (E1M4) Yellow"}, + 371051: {'name': 'The Guard Tower (E1M4) - Tome of Power 3', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 221, + 'doom_type': 86, + 'region': "The Guard Tower (E1M4) Main"}, + 371052: {'name': 'The Guard Tower (E1M4) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "The Guard Tower (E1M4) Green"}, + 371053: {'name': 'The Citadel (E1M5) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 1, + 'doom_type': 73, + 'region': "The Citadel (E1M5) Yellow"}, + 371054: {'name': 'The Citadel (E1M5) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 5, + 'doom_type': 80, + 'region': "The Citadel (E1M5) Main"}, + 371055: {'name': 'The Citadel (E1M5) - Blue key', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 19, + 'doom_type': 79, + 'region': "The Citadel (E1M5) Green"}, + 371056: {'name': 'The Citadel (E1M5) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 23, + 'doom_type': 86, + 'region': "The Citadel (E1M5) Well"}, + 371057: {'name': 'The Citadel (E1M5) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 28, + 'doom_type': 2001, + 'region': "The Citadel (E1M5) Yellow"}, + 371058: {'name': 'The Citadel (E1M5) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 29, + 'doom_type': 2005, + 'region': "The Citadel (E1M5) Main"}, + 371059: {'name': 'The Citadel (E1M5) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 30, + 'doom_type': 53, + 'region': "The Citadel (E1M5) Green"}, + 371060: {'name': 'The Citadel (E1M5) - Ring of Invincibility', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 31, + 'doom_type': 84, + 'region': "The Citadel (E1M5) Green"}, + 371061: {'name': 'The Citadel (E1M5) - Tome of Power 2', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 78, + 'doom_type': 86, + 'region': "The Citadel (E1M5) Blue"}, + 371062: {'name': 'The Citadel (E1M5) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 79, + 'doom_type': 75, + 'region': "The Citadel (E1M5) Main"}, + 371063: {'name': 'The Citadel (E1M5) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 80, + 'doom_type': 8, + 'region': "The Citadel (E1M5) Green"}, + 371064: {'name': 'The Citadel (E1M5) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 103, + 'doom_type': 33, + 'region': "The Citadel (E1M5) Main"}, + 371065: {'name': 'The Citadel (E1M5) - Tome of Power 3', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 105, + 'doom_type': 86, + 'region': "The Citadel (E1M5) Green"}, + 371066: {'name': 'The Citadel (E1M5) - Silver Shield', + 'episode': 1, + 'check_sanity': True, + 'map': 5, + 'index': 129, + 'doom_type': 85, + 'region': "The Citadel (E1M5) Main"}, + 371067: {'name': 'The Citadel (E1M5) - Morph Ovum', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 192, + 'doom_type': 30, + 'region': "The Citadel (E1M5) Green"}, + 371068: {'name': 'The Citadel (E1M5) - Map Scroll', + 'episode': 1, + 'check_sanity': True, + 'map': 5, + 'index': 203, + 'doom_type': 35, + 'region': "The Citadel (E1M5) Blue"}, + 371069: {'name': 'The Citadel (E1M5) - Silver Shield 2', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 204, + 'doom_type': 85, + 'region': "The Citadel (E1M5) Blue"}, + 371070: {'name': 'The Citadel (E1M5) - Torch 2', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 205, + 'doom_type': 33, + 'region': "The Citadel (E1M5) Green"}, + 371071: {'name': 'The Citadel (E1M5) - Tome of Power 4', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 319, + 'doom_type': 86, + 'region': "The Citadel (E1M5) Green"}, + 371072: {'name': 'The Citadel (E1M5) - Tome of Power 5', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 320, + 'doom_type': 86, + 'region': "The Citadel (E1M5) Green"}, + 371073: {'name': 'The Citadel (E1M5) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "The Citadel (E1M5) Blue"}, + 371074: {'name': 'The Cathedral (E1M6) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 8, + 'doom_type': 80, + 'region': "The Cathedral (E1M6) Main"}, + 371075: {'name': 'The Cathedral (E1M6) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 9, + 'doom_type': 2005, + 'region': "The Cathedral (E1M6) Main"}, + 371076: {'name': 'The Cathedral (E1M6) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 39, + 'doom_type': 2001, + 'region': "The Cathedral (E1M6) Yellow"}, + 371077: {'name': 'The Cathedral (E1M6) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 45, + 'doom_type': 53, + 'region': "The Cathedral (E1M6) Yellow"}, + 371078: {'name': 'The Cathedral (E1M6) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 56, + 'doom_type': 86, + 'region': "The Cathedral (E1M6) Yellow"}, + 371079: {'name': 'The Cathedral (E1M6) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 61, + 'doom_type': 75, + 'region': "The Cathedral (E1M6) Yellow"}, + 371080: {'name': 'The Cathedral (E1M6) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 98, + 'doom_type': 73, + 'region': "The Cathedral (E1M6) Yellow"}, + 371081: {'name': 'The Cathedral (E1M6) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 138, + 'doom_type': 85, + 'region': "The Cathedral (E1M6) Yellow"}, + 371082: {'name': 'The Cathedral (E1M6) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 139, + 'doom_type': 8, + 'region': "The Cathedral (E1M6) Yellow"}, + 371083: {'name': 'The Cathedral (E1M6) - Ring of Invincibility', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 142, + 'doom_type': 84, + 'region': "The Cathedral (E1M6) Yellow"}, + 371084: {'name': 'The Cathedral (E1M6) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 217, + 'doom_type': 33, + 'region': "The Cathedral (E1M6) Yellow"}, + 371085: {'name': 'The Cathedral (E1M6) - Tome of Power 2', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 273, + 'doom_type': 86, + 'region': "The Cathedral (E1M6) Yellow"}, + 371086: {'name': 'The Cathedral (E1M6) - Tome of Power 3', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 274, + 'doom_type': 86, + 'region': "The Cathedral (E1M6) Main"}, + 371087: {'name': 'The Cathedral (E1M6) - Morph Ovum', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 277, + 'doom_type': 30, + 'region': "The Cathedral (E1M6) Yellow"}, + 371088: {'name': 'The Cathedral (E1M6) - Map Scroll', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 279, + 'doom_type': 35, + 'region': "The Cathedral (E1M6) Yellow"}, + 371089: {'name': 'The Cathedral (E1M6) - Ring of Invincibility 2', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 280, + 'doom_type': 84, + 'region': "The Cathedral (E1M6) Yellow"}, + 371090: {'name': 'The Cathedral (E1M6) - Silver Shield 2', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 281, + 'doom_type': 85, + 'region': "The Cathedral (E1M6) Green"}, + 371091: {'name': 'The Cathedral (E1M6) - Tome of Power 4', + 'episode': 1, + 'check_sanity': True, + 'map': 6, + 'index': 371, + 'doom_type': 86, + 'region': "The Cathedral (E1M6) Green"}, + 371092: {'name': 'The Cathedral (E1M6) - Bag of Holding 2', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 449, + 'doom_type': 8, + 'region': "The Cathedral (E1M6) Green"}, + 371093: {'name': 'The Cathedral (E1M6) - Silver Shield 3', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 457, + 'doom_type': 85, + 'region': "The Cathedral (E1M6) Main Fly"}, + 371094: {'name': 'The Cathedral (E1M6) - Bag of Holding 3', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 458, + 'doom_type': 8, + 'region': "The Cathedral (E1M6) Main Fly"}, + 371095: {'name': 'The Cathedral (E1M6) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "The Cathedral (E1M6) Green"}, + 371096: {'name': 'The Crypts (E1M7) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 11, + 'doom_type': 80, + 'region': "The Crypts (E1M7) Main"}, + 371097: {'name': 'The Crypts (E1M7) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 17, + 'doom_type': 2001, + 'region': "The Crypts (E1M7) Yellow"}, + 371098: {'name': 'The Crypts (E1M7) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 21, + 'doom_type': 73, + 'region': "The Crypts (E1M7) Yellow"}, + 371099: {'name': 'The Crypts (E1M7) - Blue key', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 25, + 'doom_type': 79, + 'region': "The Crypts (E1M7) Green"}, + 371100: {'name': 'The Crypts (E1M7) - Morph Ovum', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 26, + 'doom_type': 30, + 'region': "The Crypts (E1M7) Yellow"}, + 371101: {'name': 'The Crypts (E1M7) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 45, + 'doom_type': 53, + 'region': "The Crypts (E1M7) Yellow"}, + 371102: {'name': 'The Crypts (E1M7) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 46, + 'doom_type': 2005, + 'region': "The Crypts (E1M7) Main"}, + 371103: {'name': 'The Crypts (E1M7) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 53, + 'doom_type': 86, + 'region': "The Crypts (E1M7) Yellow"}, + 371104: {'name': 'The Crypts (E1M7) - Ring of Invincibility', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 90, + 'doom_type': 84, + 'region': "The Crypts (E1M7) Yellow"}, + 371105: {'name': 'The Crypts (E1M7) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 98, + 'doom_type': 85, + 'region': "The Crypts (E1M7) Green"}, + 371106: {'name': 'The Crypts (E1M7) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 130, + 'doom_type': 8, + 'region': "The Crypts (E1M7) Blue"}, + 371107: {'name': 'The Crypts (E1M7) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 213, + 'doom_type': 33, + 'region': "The Crypts (E1M7) Green"}, + 371108: {'name': 'The Crypts (E1M7) - Torch 2', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 214, + 'doom_type': 33, + 'region': "The Crypts (E1M7) Blue"}, + 371109: {'name': 'The Crypts (E1M7) - Tome of Power 2', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 215, + 'doom_type': 86, + 'region': "The Crypts (E1M7) Yellow"}, + 371110: {'name': 'The Crypts (E1M7) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 224, + 'doom_type': 75, + 'region': "The Crypts (E1M7) Yellow"}, + 371111: {'name': 'The Crypts (E1M7) - Map Scroll', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 231, + 'doom_type': 35, + 'region': "The Crypts (E1M7) Blue"}, + 371112: {'name': 'The Crypts (E1M7) - Silver Shield 2', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 232, + 'doom_type': 85, + 'region': "The Crypts (E1M7) Green"}, + 371113: {'name': 'The Crypts (E1M7) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "The Crypts (E1M7) Blue"}, + 371114: {'name': "Hell's Maw (E1M8) - Ethereal Crossbow", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 10, + 'doom_type': 2001, + 'region': "Hell's Maw (E1M8) Main"}, + 371115: {'name': "Hell's Maw (E1M8) - Dragon Claw", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 11, + 'doom_type': 53, + 'region': "Hell's Maw (E1M8) Main"}, + 371116: {'name': "Hell's Maw (E1M8) - Tome of Power", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 63, + 'doom_type': 86, + 'region': "Hell's Maw (E1M8) Main"}, + 371117: {'name': "Hell's Maw (E1M8) - Gauntlets of the Necromancer", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 64, + 'doom_type': 2005, + 'region': "Hell's Maw (E1M8) Main"}, + 371118: {'name': "Hell's Maw (E1M8) - Tome of Power 2", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 65, + 'doom_type': 86, + 'region': "Hell's Maw (E1M8) Main"}, + 371119: {'name': "Hell's Maw (E1M8) - Silver Shield", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 101, + 'doom_type': 85, + 'region': "Hell's Maw (E1M8) Main"}, + 371120: {'name': "Hell's Maw (E1M8) - Shadowsphere", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 102, + 'doom_type': 75, + 'region': "Hell's Maw (E1M8) Main"}, + 371121: {'name': "Hell's Maw (E1M8) - Ring of Invincibility", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 103, + 'doom_type': 84, + 'region': "Hell's Maw (E1M8) Main"}, + 371122: {'name': "Hell's Maw (E1M8) - Bag of Holding", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 104, + 'doom_type': 8, + 'region': "Hell's Maw (E1M8) Main"}, + 371123: {'name': "Hell's Maw (E1M8) - Ring of Invincibility 2", + 'episode': 1, + 'check_sanity': True, + 'map': 8, + 'index': 237, + 'doom_type': 84, + 'region': "Hell's Maw (E1M8) Main"}, + 371124: {'name': "Hell's Maw (E1M8) - Bag of Holding 2", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 238, + 'doom_type': 8, + 'region': "Hell's Maw (E1M8) Main"}, + 371125: {'name': "Hell's Maw (E1M8) - Ring of Invincibility 3", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 247, + 'doom_type': 84, + 'region': "Hell's Maw (E1M8) Main"}, + 371126: {'name': "Hell's Maw (E1M8) - Morph Ovum", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 290, + 'doom_type': 30, + 'region': "Hell's Maw (E1M8) Main"}, + 371127: {'name': "Hell's Maw (E1M8) - Exit", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "Hell's Maw (E1M8) Main"}, + 371128: {'name': 'The Graveyard (E1M9) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 2, + 'doom_type': 80, + 'region': "The Graveyard (E1M9) Main"}, + 371129: {'name': 'The Graveyard (E1M9) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 21, + 'doom_type': 73, + 'region': "The Graveyard (E1M9) Yellow"}, + 371130: {'name': 'The Graveyard (E1M9) - Blue key', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 22, + 'doom_type': 79, + 'region': "The Graveyard (E1M9) Green"}, + 371131: {'name': 'The Graveyard (E1M9) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 23, + 'doom_type': 8, + 'region': "The Graveyard (E1M9) Main"}, + 371132: {'name': 'The Graveyard (E1M9) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 109, + 'doom_type': 53, + 'region': "The Graveyard (E1M9) Yellow"}, + 371133: {'name': 'The Graveyard (E1M9) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 110, + 'doom_type': 2001, + 'region': "The Graveyard (E1M9) Green"}, + 371134: {'name': 'The Graveyard (E1M9) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 128, + 'doom_type': 75, + 'region': "The Graveyard (E1M9) Green"}, + 371135: {'name': 'The Graveyard (E1M9) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 129, + 'doom_type': 85, + 'region': "The Graveyard (E1M9) Main"}, + 371136: {'name': 'The Graveyard (E1M9) - Ring of Invincibility', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 217, + 'doom_type': 84, + 'region': "The Graveyard (E1M9) Green"}, + 371137: {'name': 'The Graveyard (E1M9) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 253, + 'doom_type': 33, + 'region': "The Graveyard (E1M9) Green"}, + 371138: {'name': 'The Graveyard (E1M9) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 254, + 'doom_type': 86, + 'region': "The Graveyard (E1M9) Main"}, + 371139: {'name': 'The Graveyard (E1M9) - Morph Ovum', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 279, + 'doom_type': 30, + 'region': "The Graveyard (E1M9) Main"}, + 371140: {'name': 'The Graveyard (E1M9) - Map Scroll', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 280, + 'doom_type': 35, + 'region': "The Graveyard (E1M9) Blue"}, + 371141: {'name': 'The Graveyard (E1M9) - Dragon Claw 2', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 292, + 'doom_type': 53, + 'region': "The Graveyard (E1M9) Main"}, + 371142: {'name': 'The Graveyard (E1M9) - Tome of Power 2', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 339, + 'doom_type': 86, + 'region': "The Graveyard (E1M9) Green"}, + 371143: {'name': 'The Graveyard (E1M9) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "The Graveyard (E1M9) Blue"}, + 371144: {'name': 'The Crater (E2M1) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 8, + 'doom_type': 80, + 'region': "The Crater (E2M1) Main"}, + 371145: {'name': 'The Crater (E2M1) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 10, + 'doom_type': 73, + 'region': "The Crater (E2M1) Yellow"}, + 371146: {'name': 'The Crater (E2M1) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 39, + 'doom_type': 2001, + 'region': "The Crater (E2M1) Main"}, + 371147: {'name': 'The Crater (E2M1) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 49, + 'doom_type': 86, + 'region': "The Crater (E2M1) Main"}, + 371148: {'name': 'The Crater (E2M1) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 90, + 'doom_type': 53, + 'region': "The Crater (E2M1) Yellow"}, + 371149: {'name': 'The Crater (E2M1) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 98, + 'doom_type': 8, + 'region': "The Crater (E2M1) Yellow"}, + 371150: {'name': 'The Crater (E2M1) - Hellstaff', + 'episode': 2, + 'check_sanity': True, + 'map': 1, + 'index': 103, + 'doom_type': 2004, + 'region': "The Crater (E2M1) Yellow"}, + 371151: {'name': 'The Crater (E2M1) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 141, + 'doom_type': 75, + 'region': "The Crater (E2M1) Yellow"}, + 371152: {'name': 'The Crater (E2M1) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 145, + 'doom_type': 85, + 'region': "The Crater (E2M1) Main"}, + 371153: {'name': 'The Crater (E2M1) - Torch', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 146, + 'doom_type': 33, + 'region': "The Crater (E2M1) Main"}, + 371154: {'name': 'The Crater (E2M1) - Mystic Urn', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 236, + 'doom_type': 32, + 'region': "The Crater (E2M1) Yellow"}, + 371155: {'name': 'The Crater (E2M1) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "The Crater (E2M1) Green"}, + 371156: {'name': 'The Lava Pits (E2M2) - Green key', + 'episode': 2, + 'check_sanity': True, + 'map': 2, + 'index': 8, + 'doom_type': 73, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371157: {'name': 'The Lava Pits (E2M2) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 9, + 'doom_type': 80, + 'region': "The Lava Pits (E2M2) Main"}, + 371158: {'name': 'The Lava Pits (E2M2) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 25, + 'doom_type': 2001, + 'region': "The Lava Pits (E2M2) Main"}, + 371159: {'name': 'The Lava Pits (E2M2) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 67, + 'doom_type': 75, + 'region': "The Lava Pits (E2M2) Main"}, + 371160: {'name': 'The Lava Pits (E2M2) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 98, + 'doom_type': 84, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371161: {'name': 'The Lava Pits (E2M2) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 109, + 'doom_type': 53, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371162: {'name': 'The Lava Pits (E2M2) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 117, + 'doom_type': 2004, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371163: {'name': 'The Lava Pits (E2M2) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 122, + 'doom_type': 8, + 'region': "The Lava Pits (E2M2) Green"}, + 371164: {'name': 'The Lava Pits (E2M2) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 123, + 'doom_type': 86, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371165: {'name': 'The Lava Pits (E2M2) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 124, + 'doom_type': 85, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371166: {'name': 'The Lava Pits (E2M2) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 127, + 'doom_type': 2005, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371167: {'name': 'The Lava Pits (E2M2) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 133, + 'doom_type': 31, + 'region': "The Lava Pits (E2M2) Green"}, + 371168: {'name': 'The Lava Pits (E2M2) - Mystic Urn', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 230, + 'doom_type': 32, + 'region': "The Lava Pits (E2M2) Green"}, + 371169: {'name': 'The Lava Pits (E2M2) - Map Scroll', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 232, + 'doom_type': 35, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371170: {'name': 'The Lava Pits (E2M2) - Tome of Power 2', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 233, + 'doom_type': 86, + 'region': "The Lava Pits (E2M2) Main"}, + 371171: {'name': 'The Lava Pits (E2M2) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 234, + 'doom_type': 36, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371172: {'name': 'The Lava Pits (E2M2) - Tome of Power 3', + 'episode': 2, + 'check_sanity': True, + 'map': 2, + 'index': 323, + 'doom_type': 86, + 'region': "The Lava Pits (E2M2) Main"}, + 371173: {'name': 'The Lava Pits (E2M2) - Silver Shield 2', + 'episode': 2, + 'check_sanity': True, + 'map': 2, + 'index': 324, + 'doom_type': 85, + 'region': "The Lava Pits (E2M2) Main"}, + 371174: {'name': 'The Lava Pits (E2M2) - Bag of Holding 2', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 329, + 'doom_type': 8, + 'region': "The Lava Pits (E2M2) Main"}, + 371175: {'name': 'The Lava Pits (E2M2) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 341, + 'doom_type': 30, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371176: {'name': 'The Lava Pits (E2M2) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "The Lava Pits (E2M2) Green"}, + 371177: {'name': 'The River of Fire (E2M3) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 9, + 'doom_type': 80, + 'region': "The River of Fire (E2M3) Main"}, + 371178: {'name': 'The River of Fire (E2M3) - Blue key', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 10, + 'doom_type': 79, + 'region': "The River of Fire (E2M3) Main"}, + 371179: {'name': 'The River of Fire (E2M3) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 17, + 'doom_type': 73, + 'region': "The River of Fire (E2M3) Yellow"}, + 371180: {'name': 'The River of Fire (E2M3) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 26, + 'doom_type': 2005, + 'region': "The River of Fire (E2M3) Main"}, + 371181: {'name': 'The River of Fire (E2M3) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 57, + 'doom_type': 2001, + 'region': "The River of Fire (E2M3) Main"}, + 371182: {'name': 'The River of Fire (E2M3) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 92, + 'doom_type': 53, + 'region': "The River of Fire (E2M3) Main"}, + 371183: {'name': 'The River of Fire (E2M3) - Phoenix Rod', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 122, + 'doom_type': 2003, + 'region': "The River of Fire (E2M3) Main"}, + 371184: {'name': 'The River of Fire (E2M3) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 128, + 'doom_type': 2004, + 'region': "The River of Fire (E2M3) Blue"}, + 371185: {'name': 'The River of Fire (E2M3) - Bag of Holding', + 'episode': 2, + 'check_sanity': True, + 'map': 3, + 'index': 136, + 'doom_type': 8, + 'region': "The River of Fire (E2M3) Blue"}, + 371186: {'name': 'The River of Fire (E2M3) - Shadowsphere', + 'episode': 2, + 'check_sanity': True, + 'map': 3, + 'index': 145, + 'doom_type': 75, + 'region': "The River of Fire (E2M3) Green"}, + 371187: {'name': 'The River of Fire (E2M3) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 146, + 'doom_type': 86, + 'region': "The River of Fire (E2M3) Main"}, + 371188: {'name': 'The River of Fire (E2M3) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 147, + 'doom_type': 84, + 'region': "The River of Fire (E2M3) Main"}, + 371189: {'name': 'The River of Fire (E2M3) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 148, + 'doom_type': 85, + 'region': "The River of Fire (E2M3) Main"}, + 371190: {'name': 'The River of Fire (E2M3) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 297, + 'doom_type': 31, + 'region': "The River of Fire (E2M3) Blue"}, + 371191: {'name': 'The River of Fire (E2M3) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 298, + 'doom_type': 36, + 'region': "The River of Fire (E2M3) Blue"}, + 371192: {'name': 'The River of Fire (E2M3) - Mystic Urn', + 'episode': 2, + 'check_sanity': True, + 'map': 3, + 'index': 299, + 'doom_type': 32, + 'region': "The River of Fire (E2M3) Main"}, + 371193: {'name': 'The River of Fire (E2M3) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 300, + 'doom_type': 30, + 'region': "The River of Fire (E2M3) Yellow"}, + 371194: {'name': 'The River of Fire (E2M3) - Tome of Power 2', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 313, + 'doom_type': 86, + 'region': "The River of Fire (E2M3) Green"}, + 371195: {'name': 'The River of Fire (E2M3) - Firemace', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 413, + 'doom_type': 2002, + 'region': "The River of Fire (E2M3) Main"}, + 371196: {'name': 'The River of Fire (E2M3) - Firemace 2', + 'episode': 2, + 'check_sanity': True, + 'map': 3, + 'index': 441, + 'doom_type': 2002, + 'region': "The River of Fire (E2M3) Yellow"}, + 371197: {'name': 'The River of Fire (E2M3) - Firemace 3', + 'episode': 2, + 'check_sanity': True, + 'map': 3, + 'index': 448, + 'doom_type': 2002, + 'region': "The River of Fire (E2M3) Blue"}, + 371198: {'name': 'The River of Fire (E2M3) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "The River of Fire (E2M3) Blue"}, + 371199: {'name': 'The Ice Grotto (E2M4) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 18, + 'doom_type': 80, + 'region': "The Ice Grotto (E2M4) Main"}, + 371200: {'name': 'The Ice Grotto (E2M4) - Blue key', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 19, + 'doom_type': 79, + 'region': "The Ice Grotto (E2M4) Green"}, + 371201: {'name': 'The Ice Grotto (E2M4) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 28, + 'doom_type': 73, + 'region': "The Ice Grotto (E2M4) Yellow"}, + 371202: {'name': 'The Ice Grotto (E2M4) - Phoenix Rod', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 29, + 'doom_type': 2003, + 'region': "The Ice Grotto (E2M4) Yellow"}, + 371203: {'name': 'The Ice Grotto (E2M4) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': True, + 'map': 4, + 'index': 30, + 'doom_type': 2005, + 'region': "The Ice Grotto (E2M4) Main"}, + 371204: {'name': 'The Ice Grotto (E2M4) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 31, + 'doom_type': 2001, + 'region': "The Ice Grotto (E2M4) Main"}, + 371205: {'name': 'The Ice Grotto (E2M4) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 32, + 'doom_type': 2004, + 'region': "The Ice Grotto (E2M4) Blue"}, + 371206: {'name': 'The Ice Grotto (E2M4) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 33, + 'doom_type': 53, + 'region': "The Ice Grotto (E2M4) Green"}, + 371207: {'name': 'The Ice Grotto (E2M4) - Torch', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 34, + 'doom_type': 33, + 'region': "The Ice Grotto (E2M4) Green"}, + 371208: {'name': 'The Ice Grotto (E2M4) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 35, + 'doom_type': 8, + 'region': "The Ice Grotto (E2M4) Main"}, + 371209: {'name': 'The Ice Grotto (E2M4) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 36, + 'doom_type': 75, + 'region': "The Ice Grotto (E2M4) Green"}, + 371210: {'name': 'The Ice Grotto (E2M4) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 37, + 'doom_type': 36, + 'region': "The Ice Grotto (E2M4) Green"}, + 371211: {'name': 'The Ice Grotto (E2M4) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 38, + 'doom_type': 85, + 'region': "The Ice Grotto (E2M4) Main"}, + 371212: {'name': 'The Ice Grotto (E2M4) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 39, + 'doom_type': 86, + 'region': "The Ice Grotto (E2M4) Green"}, + 371213: {'name': 'The Ice Grotto (E2M4) - Tome of Power 2', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 40, + 'doom_type': 86, + 'region': "The Ice Grotto (E2M4) Main"}, + 371214: {'name': 'The Ice Grotto (E2M4) - Bag of Holding 2', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 41, + 'doom_type': 8, + 'region': "The Ice Grotto (E2M4) Green"}, + 371215: {'name': 'The Ice Grotto (E2M4) - Tome of Power 3', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 128, + 'doom_type': 86, + 'region': "The Ice Grotto (E2M4) Yellow"}, + 371216: {'name': 'The Ice Grotto (E2M4) - Map Scroll', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 283, + 'doom_type': 35, + 'region': "The Ice Grotto (E2M4) Green"}, + 371217: {'name': 'The Ice Grotto (E2M4) - Mystic Urn', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 289, + 'doom_type': 32, + 'region': "The Ice Grotto (E2M4) Magenta"}, + 371218: {'name': 'The Ice Grotto (E2M4) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 291, + 'doom_type': 31, + 'region': "The Ice Grotto (E2M4) Green"}, + 371219: {'name': 'The Ice Grotto (E2M4) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 299, + 'doom_type': 30, + 'region': "The Ice Grotto (E2M4) Main"}, + 371220: {'name': 'The Ice Grotto (E2M4) - Shadowsphere 2', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 300, + 'doom_type': 75, + 'region': "The Ice Grotto (E2M4) Main"}, + 371221: {'name': 'The Ice Grotto (E2M4) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "The Ice Grotto (E2M4) Blue"}, + 371222: {'name': 'The Catacombs (E2M5) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 14, + 'doom_type': 80, + 'region': "The Catacombs (E2M5) Main"}, + 371223: {'name': 'The Catacombs (E2M5) - Blue key', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 25, + 'doom_type': 79, + 'region': "The Catacombs (E2M5) Green"}, + 371224: {'name': 'The Catacombs (E2M5) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 27, + 'doom_type': 2004, + 'region': "The Catacombs (E2M5) Yellow"}, + 371225: {'name': 'The Catacombs (E2M5) - Phoenix Rod', + 'episode': 2, + 'check_sanity': True, + 'map': 5, + 'index': 44, + 'doom_type': 2003, + 'region': "The Catacombs (E2M5) Green"}, + 371226: {'name': 'The Catacombs (E2M5) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 107, + 'doom_type': 2001, + 'region': "The Catacombs (E2M5) Yellow"}, + 371227: {'name': 'The Catacombs (E2M5) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 108, + 'doom_type': 2005, + 'region': "The Catacombs (E2M5) Main"}, + 371228: {'name': 'The Catacombs (E2M5) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 109, + 'doom_type': 53, + 'region': "The Catacombs (E2M5) Main"}, + 371229: {'name': 'The Catacombs (E2M5) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 110, + 'doom_type': 8, + 'region': "The Catacombs (E2M5) Main"}, + 371230: {'name': 'The Catacombs (E2M5) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 112, + 'doom_type': 85, + 'region': "The Catacombs (E2M5) Yellow"}, + 371231: {'name': 'The Catacombs (E2M5) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 113, + 'doom_type': 75, + 'region': "The Catacombs (E2M5) Yellow"}, + 371232: {'name': 'The Catacombs (E2M5) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 114, + 'doom_type': 84, + 'region': "The Catacombs (E2M5) Yellow"}, + 371233: {'name': 'The Catacombs (E2M5) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 115, + 'doom_type': 86, + 'region': "The Catacombs (E2M5) Yellow"}, + 371234: {'name': 'The Catacombs (E2M5) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 116, + 'doom_type': 73, + 'region': "The Catacombs (E2M5) Yellow"}, + 371235: {'name': 'The Catacombs (E2M5) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 263, + 'doom_type': 36, + 'region': "The Catacombs (E2M5) Yellow"}, + 371236: {'name': 'The Catacombs (E2M5) - Tome of Power 2', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 322, + 'doom_type': 86, + 'region': "The Catacombs (E2M5) Yellow"}, + 371237: {'name': 'The Catacombs (E2M5) - Map Scroll', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 323, + 'doom_type': 35, + 'region': "The Catacombs (E2M5) Green"}, + 371238: {'name': 'The Catacombs (E2M5) - Mystic Urn', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 324, + 'doom_type': 32, + 'region': "The Catacombs (E2M5) Yellow"}, + 371239: {'name': 'The Catacombs (E2M5) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 325, + 'doom_type': 30, + 'region': "The Catacombs (E2M5) Green"}, + 371240: {'name': 'The Catacombs (E2M5) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 326, + 'doom_type': 31, + 'region': "The Catacombs (E2M5) Green"}, + 371241: {'name': 'The Catacombs (E2M5) - Torch', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 327, + 'doom_type': 33, + 'region': "The Catacombs (E2M5) Main"}, + 371242: {'name': 'The Catacombs (E2M5) - Tome of Power 3', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 328, + 'doom_type': 86, + 'region': "The Catacombs (E2M5) Yellow"}, + 371243: {'name': 'The Catacombs (E2M5) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "The Catacombs (E2M5) Blue"}, + 371244: {'name': 'The Labyrinth (E2M6) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 7, + 'doom_type': 80, + 'region': "The Labyrinth (E2M6) Main"}, + 371245: {'name': 'The Labyrinth (E2M6) - Blue key', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 14, + 'doom_type': 79, + 'region': "The Labyrinth (E2M6) Green"}, + 371246: {'name': 'The Labyrinth (E2M6) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 15, + 'doom_type': 73, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371247: {'name': 'The Labyrinth (E2M6) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 22, + 'doom_type': 2004, + 'region': "The Labyrinth (E2M6) Green"}, + 371248: {'name': 'The Labyrinth (E2M6) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 23, + 'doom_type': 2005, + 'region': "The Labyrinth (E2M6) Main"}, + 371249: {'name': 'The Labyrinth (E2M6) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 24, + 'doom_type': 2001, + 'region': "The Labyrinth (E2M6) Main"}, + 371250: {'name': 'The Labyrinth (E2M6) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 25, + 'doom_type': 53, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371251: {'name': 'The Labyrinth (E2M6) - Phoenix Rod', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 26, + 'doom_type': 2003, + 'region': "The Labyrinth (E2M6) Green"}, + 371252: {'name': 'The Labyrinth (E2M6) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 27, + 'doom_type': 8, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371253: {'name': 'The Labyrinth (E2M6) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 31, + 'doom_type': 75, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371254: {'name': 'The Labyrinth (E2M6) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 32, + 'doom_type': 84, + 'region': "The Labyrinth (E2M6) Blue"}, + 371255: {'name': 'The Labyrinth (E2M6) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 33, + 'doom_type': 86, + 'region': "The Labyrinth (E2M6) Green"}, + 371256: {'name': 'The Labyrinth (E2M6) - Silver Shield', + 'episode': 2, + 'check_sanity': True, + 'map': 6, + 'index': 34, + 'doom_type': 85, + 'region': "The Labyrinth (E2M6) Main"}, + 371257: {'name': 'The Labyrinth (E2M6) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 35, + 'doom_type': 30, + 'region': "The Labyrinth (E2M6) Main"}, + 371258: {'name': 'The Labyrinth (E2M6) - Map Scroll', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 282, + 'doom_type': 35, + 'region': "The Labyrinth (E2M6) Green"}, + 371259: {'name': 'The Labyrinth (E2M6) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 283, + 'doom_type': 31, + 'region': "The Labyrinth (E2M6) Green"}, + 371260: {'name': 'The Labyrinth (E2M6) - Tome of Power 2', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 284, + 'doom_type': 86, + 'region': "The Labyrinth (E2M6) Green"}, + 371261: {'name': 'The Labyrinth (E2M6) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 285, + 'doom_type': 36, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371262: {'name': 'The Labyrinth (E2M6) - Mystic Urn', + 'episode': 2, + 'check_sanity': True, + 'map': 6, + 'index': 336, + 'doom_type': 32, + 'region': "The Labyrinth (E2M6) Blue"}, + 371263: {'name': 'The Labyrinth (E2M6) - Phoenix Rod 2', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 422, + 'doom_type': 2003, + 'region': "The Labyrinth (E2M6) Blue"}, + 371264: {'name': 'The Labyrinth (E2M6) - Firemace', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 432, + 'doom_type': 2002, + 'region': "The Labyrinth (E2M6) Main"}, + 371265: {'name': 'The Labyrinth (E2M6) - Firemace 2', + 'episode': 2, + 'check_sanity': True, + 'map': 6, + 'index': 456, + 'doom_type': 2002, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371266: {'name': 'The Labyrinth (E2M6) - Firemace 3', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 457, + 'doom_type': 2002, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371267: {'name': 'The Labyrinth (E2M6) - Firemace 4', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 458, + 'doom_type': 2002, + 'region': "The Labyrinth (E2M6) Blue"}, + 371268: {'name': 'The Labyrinth (E2M6) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "The Labyrinth (E2M6) Blue"}, + 371269: {'name': 'The Great Hall (E2M7) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 8, + 'doom_type': 73, + 'region': "The Great Hall (E2M7) Yellow"}, + 371270: {'name': 'The Great Hall (E2M7) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 9, + 'doom_type': 80, + 'region': "The Great Hall (E2M7) Main"}, + 371271: {'name': 'The Great Hall (E2M7) - Blue key', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 11, + 'doom_type': 79, + 'region': "The Great Hall (E2M7) Green"}, + 371272: {'name': 'The Great Hall (E2M7) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 64, + 'doom_type': 30, + 'region': "The Great Hall (E2M7) Main"}, + 371273: {'name': 'The Great Hall (E2M7) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 76, + 'doom_type': 75, + 'region': "The Great Hall (E2M7) Main"}, + 371274: {'name': 'The Great Hall (E2M7) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 77, + 'doom_type': 84, + 'region': "The Great Hall (E2M7) Yellow"}, + 371275: {'name': 'The Great Hall (E2M7) - Mystic Urn', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 78, + 'doom_type': 32, + 'region': "The Great Hall (E2M7) Blue"}, + 371276: {'name': 'The Great Hall (E2M7) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 80, + 'doom_type': 86, + 'region': "The Great Hall (E2M7) Yellow"}, + 371277: {'name': 'The Great Hall (E2M7) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 81, + 'doom_type': 36, + 'region': "The Great Hall (E2M7) Yellow"}, + 371278: {'name': 'The Great Hall (E2M7) - Torch', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 82, + 'doom_type': 33, + 'region': "The Great Hall (E2M7) Main"}, + 371279: {'name': 'The Great Hall (E2M7) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 83, + 'doom_type': 8, + 'region': "The Great Hall (E2M7) Main"}, + 371280: {'name': 'The Great Hall (E2M7) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 84, + 'doom_type': 85, + 'region': "The Great Hall (E2M7) Main"}, + 371281: {'name': 'The Great Hall (E2M7) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 85, + 'doom_type': 31, + 'region': "The Great Hall (E2M7) Main"}, + 371282: {'name': 'The Great Hall (E2M7) - Map Scroll', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 86, + 'doom_type': 35, + 'region': "The Great Hall (E2M7) Yellow"}, + 371283: {'name': 'The Great Hall (E2M7) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 91, + 'doom_type': 2001, + 'region': "The Great Hall (E2M7) Main"}, + 371284: {'name': 'The Great Hall (E2M7) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 92, + 'doom_type': 2005, + 'region': "The Great Hall (E2M7) Main"}, + 371285: {'name': 'The Great Hall (E2M7) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 93, + 'doom_type': 53, + 'region': "The Great Hall (E2M7) Yellow"}, + 371286: {'name': 'The Great Hall (E2M7) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 94, + 'doom_type': 2004, + 'region': "The Great Hall (E2M7) Yellow"}, + 371287: {'name': 'The Great Hall (E2M7) - Phoenix Rod', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 95, + 'doom_type': 2003, + 'region': "The Great Hall (E2M7) Main"}, + 371288: {'name': 'The Great Hall (E2M7) - Tome of Power 2', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 477, + 'doom_type': 86, + 'region': "The Great Hall (E2M7) Main"}, + 371289: {'name': 'The Great Hall (E2M7) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "The Great Hall (E2M7) Blue"}, + 371290: {'name': 'The Portals of Chaos (E2M8) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 9, + 'doom_type': 2001, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371291: {'name': 'The Portals of Chaos (E2M8) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 10, + 'doom_type': 53, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371292: {'name': 'The Portals of Chaos (E2M8) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 11, + 'doom_type': 2005, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371293: {'name': 'The Portals of Chaos (E2M8) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 12, + 'doom_type': 2004, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371294: {'name': 'The Portals of Chaos (E2M8) - Phoenix Rod', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 13, + 'doom_type': 2003, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371295: {'name': 'The Portals of Chaos (E2M8) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 14, + 'doom_type': 86, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371296: {'name': 'The Portals of Chaos (E2M8) - Bag of Holding', + 'episode': 2, + 'check_sanity': True, + 'map': 8, + 'index': 18, + 'doom_type': 8, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371297: {'name': 'The Portals of Chaos (E2M8) - Mystic Urn', + 'episode': 2, + 'check_sanity': True, + 'map': 8, + 'index': 40, + 'doom_type': 32, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371298: {'name': 'The Portals of Chaos (E2M8) - Shadowsphere', + 'episode': 2, + 'check_sanity': True, + 'map': 8, + 'index': 41, + 'doom_type': 75, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371299: {'name': 'The Portals of Chaos (E2M8) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 42, + 'doom_type': 85, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371300: {'name': 'The Portals of Chaos (E2M8) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 43, + 'doom_type': 31, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371301: {'name': 'The Portals of Chaos (E2M8) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 44, + 'doom_type': 36, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371302: {'name': 'The Portals of Chaos (E2M8) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 272, + 'doom_type': 84, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371303: {'name': 'The Portals of Chaos (E2M8) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 274, + 'doom_type': 30, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371304: {'name': 'The Portals of Chaos (E2M8) - Mystic Urn 2', + 'episode': 2, + 'check_sanity': True, + 'map': 8, + 'index': 275, + 'doom_type': 32, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371305: {'name': 'The Portals of Chaos (E2M8) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371306: {'name': 'The Glacier (E2M9) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 6, + 'doom_type': 80, + 'region': "The Glacier (E2M9) Main"}, + 371307: {'name': 'The Glacier (E2M9) - Blue key', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 16, + 'doom_type': 79, + 'region': "The Glacier (E2M9) Green"}, + 371308: {'name': 'The Glacier (E2M9) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 17, + 'doom_type': 73, + 'region': "The Glacier (E2M9) Yellow"}, + 371309: {'name': 'The Glacier (E2M9) - Phoenix Rod', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 34, + 'doom_type': 2003, + 'region': "The Glacier (E2M9) Green"}, + 371310: {'name': 'The Glacier (E2M9) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': True, + 'map': 9, + 'index': 39, + 'doom_type': 2005, + 'region': "The Glacier (E2M9) Main"}, + 371311: {'name': 'The Glacier (E2M9) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 40, + 'doom_type': 2001, + 'region': "The Glacier (E2M9) Main"}, + 371312: {'name': 'The Glacier (E2M9) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 41, + 'doom_type': 53, + 'region': "The Glacier (E2M9) Yellow"}, + 371313: {'name': 'The Glacier (E2M9) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 42, + 'doom_type': 2004, + 'region': "The Glacier (E2M9) Green"}, + 371314: {'name': 'The Glacier (E2M9) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 43, + 'doom_type': 8, + 'region': "The Glacier (E2M9) Main"}, + 371315: {'name': 'The Glacier (E2M9) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 45, + 'doom_type': 86, + 'region': "The Glacier (E2M9) Main"}, + 371316: {'name': 'The Glacier (E2M9) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 46, + 'doom_type': 75, + 'region': "The Glacier (E2M9) Main"}, + 371317: {'name': 'The Glacier (E2M9) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 47, + 'doom_type': 84, + 'region': "The Glacier (E2M9) Green"}, + 371318: {'name': 'The Glacier (E2M9) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 48, + 'doom_type': 85, + 'region': "The Glacier (E2M9) Main"}, + 371319: {'name': 'The Glacier (E2M9) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 49, + 'doom_type': 31, + 'region': "The Glacier (E2M9) Green"}, + 371320: {'name': 'The Glacier (E2M9) - Mystic Urn', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 50, + 'doom_type': 32, + 'region': "The Glacier (E2M9) Blue"}, + 371321: {'name': 'The Glacier (E2M9) - Map Scroll', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 51, + 'doom_type': 35, + 'region': "The Glacier (E2M9) Blue"}, + 371322: {'name': 'The Glacier (E2M9) - Mystic Urn 2', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 52, + 'doom_type': 32, + 'region': "The Glacier (E2M9) Green"}, + 371323: {'name': 'The Glacier (E2M9) - Torch', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 53, + 'doom_type': 33, + 'region': "The Glacier (E2M9) Green"}, + 371324: {'name': 'The Glacier (E2M9) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 424, + 'doom_type': 36, + 'region': "The Glacier (E2M9) Yellow"}, + 371325: {'name': 'The Glacier (E2M9) - Dragon Claw 2', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 456, + 'doom_type': 53, + 'region': "The Glacier (E2M9) Main"}, + 371326: {'name': 'The Glacier (E2M9) - Tome of Power 2', + 'episode': 2, + 'check_sanity': True, + 'map': 9, + 'index': 457, + 'doom_type': 86, + 'region': "The Glacier (E2M9) Main"}, + 371327: {'name': 'The Glacier (E2M9) - Torch 2', + 'episode': 2, + 'check_sanity': True, + 'map': 9, + 'index': 458, + 'doom_type': 33, + 'region': "The Glacier (E2M9) Main"}, + 371328: {'name': 'The Glacier (E2M9) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 474, + 'doom_type': 30, + 'region': "The Glacier (E2M9) Main"}, + 371329: {'name': 'The Glacier (E2M9) - Firemace', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 479, + 'doom_type': 2002, + 'region': "The Glacier (E2M9) Main"}, + 371330: {'name': 'The Glacier (E2M9) - Firemace 2', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 501, + 'doom_type': 2002, + 'region': "The Glacier (E2M9) Yellow"}, + 371331: {'name': 'The Glacier (E2M9) - Firemace 3', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 502, + 'doom_type': 2002, + 'region': "The Glacier (E2M9) Blue"}, + 371332: {'name': 'The Glacier (E2M9) - Firemace 4', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 503, + 'doom_type': 2002, + 'region': "The Glacier (E2M9) Main"}, + 371333: {'name': 'The Glacier (E2M9) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "The Glacier (E2M9) Blue"}, + 371334: {'name': 'The Storehouse (E3M1) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 9, + 'doom_type': 80, + 'region': "The Storehouse (E3M1) Main"}, + 371335: {'name': 'The Storehouse (E3M1) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 10, + 'doom_type': 73, + 'region': "The Storehouse (E3M1) Yellow"}, + 371336: {'name': 'The Storehouse (E3M1) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 29, + 'doom_type': 8, + 'region': "The Storehouse (E3M1) Main"}, + 371337: {'name': 'The Storehouse (E3M1) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 38, + 'doom_type': 75, + 'region': "The Storehouse (E3M1) Main"}, + 371338: {'name': 'The Storehouse (E3M1) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 39, + 'doom_type': 84, + 'region': "The Storehouse (E3M1) Green"}, + 371339: {'name': 'The Storehouse (E3M1) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 40, + 'doom_type': 85, + 'region': "The Storehouse (E3M1) Main"}, + 371340: {'name': 'The Storehouse (E3M1) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 41, + 'doom_type': 35, + 'region': "The Storehouse (E3M1) Green"}, + 371341: {'name': 'The Storehouse (E3M1) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 42, + 'doom_type': 36, + 'region': "The Storehouse (E3M1) Main"}, + 371342: {'name': 'The Storehouse (E3M1) - Tome of Power', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 43, + 'doom_type': 86, + 'region': "The Storehouse (E3M1) Green"}, + 371343: {'name': 'The Storehouse (E3M1) - Torch', + 'episode': 3, + 'check_sanity': True, + 'map': 1, + 'index': 44, + 'doom_type': 33, + 'region': "The Storehouse (E3M1) Main"}, + 371344: {'name': 'The Storehouse (E3M1) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 45, + 'doom_type': 53, + 'region': "The Storehouse (E3M1) Main"}, + 371345: {'name': 'The Storehouse (E3M1) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 46, + 'doom_type': 2004, + 'region': "The Storehouse (E3M1) Green"}, + 371346: {'name': 'The Storehouse (E3M1) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 47, + 'doom_type': 2005, + 'region': "The Storehouse (E3M1) Main"}, + 371347: {'name': 'The Storehouse (E3M1) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "The Storehouse (E3M1) Green"}, + 371348: {'name': 'The Cesspool (E3M2) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 4, + 'doom_type': 80, + 'region': "The Cesspool (E3M2) Main"}, + 371349: {'name': 'The Cesspool (E3M2) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 19, + 'doom_type': 73, + 'region': "The Cesspool (E3M2) Yellow"}, + 371350: {'name': 'The Cesspool (E3M2) - Blue key', + 'episode': 3, + 'check_sanity': True, + 'map': 2, + 'index': 20, + 'doom_type': 79, + 'region': "The Cesspool (E3M2) Green"}, + 371351: {'name': 'The Cesspool (E3M2) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 144, + 'doom_type': 2001, + 'region': "The Cesspool (E3M2) Main"}, + 371352: {'name': 'The Cesspool (E3M2) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 145, + 'doom_type': 84, + 'region': "The Cesspool (E3M2) Green"}, + 371353: {'name': 'The Cesspool (E3M2) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 146, + 'doom_type': 2005, + 'region': "The Cesspool (E3M2) Green"}, + 371354: {'name': 'The Cesspool (E3M2) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 147, + 'doom_type': 53, + 'region': "The Cesspool (E3M2) Yellow"}, + 371355: {'name': 'The Cesspool (E3M2) - Phoenix Rod', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 148, + 'doom_type': 2003, + 'region': "The Cesspool (E3M2) Green"}, + 371356: {'name': 'The Cesspool (E3M2) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 149, + 'doom_type': 2004, + 'region': "The Cesspool (E3M2) Yellow"}, + 371357: {'name': 'The Cesspool (E3M2) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 150, + 'doom_type': 8, + 'region': "The Cesspool (E3M2) Yellow"}, + 371358: {'name': 'The Cesspool (E3M2) - Silver Shield', + 'episode': 3, + 'check_sanity': True, + 'map': 2, + 'index': 151, + 'doom_type': 85, + 'region': "The Cesspool (E3M2) Yellow"}, + 371359: {'name': 'The Cesspool (E3M2) - Silver Shield 2', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 152, + 'doom_type': 85, + 'region': "The Cesspool (E3M2) Main"}, + 371360: {'name': 'The Cesspool (E3M2) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 153, + 'doom_type': 30, + 'region': "The Cesspool (E3M2) Main"}, + 371361: {'name': 'The Cesspool (E3M2) - Morph Ovum 2', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 154, + 'doom_type': 30, + 'region': "The Cesspool (E3M2) Green"}, + 371362: {'name': 'The Cesspool (E3M2) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 164, + 'doom_type': 32, + 'region': "The Cesspool (E3M2) Main"}, + 371363: {'name': 'The Cesspool (E3M2) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 165, + 'doom_type': 75, + 'region': "The Cesspool (E3M2) Yellow"}, + 371364: {'name': 'The Cesspool (E3M2) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 166, + 'doom_type': 31, + 'region': "The Cesspool (E3M2) Green"}, + 371365: {'name': 'The Cesspool (E3M2) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 167, + 'doom_type': 35, + 'region': "The Cesspool (E3M2) Green"}, + 371366: {'name': 'The Cesspool (E3M2) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 168, + 'doom_type': 36, + 'region': "The Cesspool (E3M2) Green"}, + 371367: {'name': 'The Cesspool (E3M2) - Tome of Power', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 169, + 'doom_type': 86, + 'region': "The Cesspool (E3M2) Main"}, + 371368: {'name': 'The Cesspool (E3M2) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 170, + 'doom_type': 86, + 'region': "The Cesspool (E3M2) Green"}, + 371369: {'name': 'The Cesspool (E3M2) - Tome of Power 3', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 171, + 'doom_type': 86, + 'region': "The Cesspool (E3M2) Yellow"}, + 371370: {'name': 'The Cesspool (E3M2) - Torch', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 172, + 'doom_type': 33, + 'region': "The Cesspool (E3M2) Main"}, + 371371: {'name': 'The Cesspool (E3M2) - Bag of Holding 2', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 233, + 'doom_type': 8, + 'region': "The Cesspool (E3M2) Green"}, + 371372: {'name': 'The Cesspool (E3M2) - Firemace', + 'episode': 3, + 'check_sanity': True, + 'map': 2, + 'index': 555, + 'doom_type': 2002, + 'region': "The Cesspool (E3M2) Green"}, + 371373: {'name': 'The Cesspool (E3M2) - Firemace 2', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 556, + 'doom_type': 2002, + 'region': "The Cesspool (E3M2) Yellow"}, + 371374: {'name': 'The Cesspool (E3M2) - Firemace 3', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 557, + 'doom_type': 2002, + 'region': "The Cesspool (E3M2) Blue"}, + 371375: {'name': 'The Cesspool (E3M2) - Firemace 4', + 'episode': 3, + 'check_sanity': True, + 'map': 2, + 'index': 558, + 'doom_type': 2002, + 'region': "The Cesspool (E3M2) Main"}, + 371376: {'name': 'The Cesspool (E3M2) - Firemace 5', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 559, + 'doom_type': 2002, + 'region': "The Cesspool (E3M2) Yellow"}, + 371377: {'name': 'The Cesspool (E3M2) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "The Cesspool (E3M2) Blue"}, + 371378: {'name': 'The Confluence (E3M3) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 4, + 'doom_type': 80, + 'region': "The Confluence (E3M3) Main"}, + 371379: {'name': 'The Confluence (E3M3) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 7, + 'doom_type': 73, + 'region': "The Confluence (E3M3) Yellow"}, + 371380: {'name': 'The Confluence (E3M3) - Blue key', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 8, + 'doom_type': 79, + 'region': "The Confluence (E3M3) Green"}, + 371381: {'name': 'The Confluence (E3M3) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 43, + 'doom_type': 2004, + 'region': "The Confluence (E3M3) Blue"}, + 371382: {'name': 'The Confluence (E3M3) - Tome of Power', + 'episode': 3, + 'check_sanity': True, + 'map': 3, + 'index': 44, + 'doom_type': 86, + 'region': "The Confluence (E3M3) Blue"}, + 371383: {'name': 'The Confluence (E3M3) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 47, + 'doom_type': 53, + 'region': "The Confluence (E3M3) Green"}, + 371384: {'name': 'The Confluence (E3M3) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 48, + 'doom_type': 2001, + 'region': "The Confluence (E3M3) Yellow"}, + 371385: {'name': 'The Confluence (E3M3) - Hellstaff 2', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 49, + 'doom_type': 2004, + 'region': "The Confluence (E3M3) Blue"}, + 371386: {'name': 'The Confluence (E3M3) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 50, + 'doom_type': 2005, + 'region': "The Confluence (E3M3) Blue"}, + 371387: {'name': 'The Confluence (E3M3) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 51, + 'doom_type': 32, + 'region': "The Confluence (E3M3) Green"}, + 371388: {'name': 'The Confluence (E3M3) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 52, + 'doom_type': 86, + 'region': "The Confluence (E3M3) Green"}, + 371389: {'name': 'The Confluence (E3M3) - Tome of Power 3', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 53, + 'doom_type': 86, + 'region': "The Confluence (E3M3) Blue"}, + 371390: {'name': 'The Confluence (E3M3) - Tome of Power 4', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 54, + 'doom_type': 86, + 'region': "The Confluence (E3M3) Blue"}, + 371391: {'name': 'The Confluence (E3M3) - Tome of Power 5', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 55, + 'doom_type': 86, + 'region': "The Confluence (E3M3) Green"}, + 371392: {'name': 'The Confluence (E3M3) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 58, + 'doom_type': 8, + 'region': "The Confluence (E3M3) Green"}, + 371393: {'name': 'The Confluence (E3M3) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 60, + 'doom_type': 30, + 'region': "The Confluence (E3M3) Green"}, + 371394: {'name': 'The Confluence (E3M3) - Mystic Urn 2', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 72, + 'doom_type': 32, + 'region': "The Confluence (E3M3) Blue"}, + 371395: {'name': 'The Confluence (E3M3) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 73, + 'doom_type': 75, + 'region': "The Confluence (E3M3) Main"}, + 371396: {'name': 'The Confluence (E3M3) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': True, + 'map': 3, + 'index': 74, + 'doom_type': 84, + 'region': "The Confluence (E3M3) Yellow"}, + 371397: {'name': 'The Confluence (E3M3) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 75, + 'doom_type': 35, + 'region': "The Confluence (E3M3) Blue"}, + 371398: {'name': 'The Confluence (E3M3) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 76, + 'doom_type': 85, + 'region': "The Confluence (E3M3) Main"}, + 371399: {'name': 'The Confluence (E3M3) - Phoenix Rod', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 77, + 'doom_type': 2003, + 'region': "The Confluence (E3M3) Blue"}, + 371400: {'name': 'The Confluence (E3M3) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 78, + 'doom_type': 31, + 'region': "The Confluence (E3M3) Blue"}, + 371401: {'name': 'The Confluence (E3M3) - Silver Shield 2', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 79, + 'doom_type': 85, + 'region': "The Confluence (E3M3) Green"}, + 371402: {'name': 'The Confluence (E3M3) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 80, + 'doom_type': 36, + 'region': "The Confluence (E3M3) Green"}, + 371403: {'name': 'The Confluence (E3M3) - Torch', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 81, + 'doom_type': 33, + 'region': "The Confluence (E3M3) Green"}, + 371404: {'name': 'The Confluence (E3M3) - Firemace', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 622, + 'doom_type': 2002, + 'region': "The Confluence (E3M3) Green"}, + 371405: {'name': 'The Confluence (E3M3) - Firemace 2', + 'episode': 3, + 'check_sanity': True, + 'map': 3, + 'index': 623, + 'doom_type': 2002, + 'region': "The Confluence (E3M3) Green"}, + 371406: {'name': 'The Confluence (E3M3) - Firemace 3', + 'episode': 3, + 'check_sanity': True, + 'map': 3, + 'index': 624, + 'doom_type': 2002, + 'region': "The Confluence (E3M3) Yellow"}, + 371407: {'name': 'The Confluence (E3M3) - Firemace 4', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 625, + 'doom_type': 2002, + 'region': "The Confluence (E3M3) Blue"}, + 371408: {'name': 'The Confluence (E3M3) - Firemace 5', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 626, + 'doom_type': 2002, + 'region': "The Confluence (E3M3) Blue"}, + 371409: {'name': 'The Confluence (E3M3) - Firemace 6', + 'episode': 3, + 'check_sanity': True, + 'map': 3, + 'index': 627, + 'doom_type': 2002, + 'region': "The Confluence (E3M3) Blue"}, + 371410: {'name': 'The Confluence (E3M3) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "The Confluence (E3M3) Blue"}, + 371411: {'name': 'The Azure Fortress (E3M4) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 6, + 'doom_type': 80, + 'region': "The Azure Fortress (E3M4) Main"}, + 371412: {'name': 'The Azure Fortress (E3M4) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 21, + 'doom_type': 73, + 'region': "The Azure Fortress (E3M4) Yellow"}, + 371413: {'name': 'The Azure Fortress (E3M4) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 51, + 'doom_type': 53, + 'region': "The Azure Fortress (E3M4) Main"}, + 371414: {'name': 'The Azure Fortress (E3M4) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 52, + 'doom_type': 84, + 'region': "The Azure Fortress (E3M4) Main"}, + 371415: {'name': 'The Azure Fortress (E3M4) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 53, + 'doom_type': 2001, + 'region': "The Azure Fortress (E3M4) Main"}, + 371416: {'name': 'The Azure Fortress (E3M4) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 54, + 'doom_type': 2005, + 'region': "The Azure Fortress (E3M4) Main"}, + 371417: {'name': 'The Azure Fortress (E3M4) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 55, + 'doom_type': 2004, + 'region': "The Azure Fortress (E3M4) Yellow"}, + 371418: {'name': 'The Azure Fortress (E3M4) - Phoenix Rod', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 56, + 'doom_type': 2003, + 'region': "The Azure Fortress (E3M4) Green"}, + 371419: {'name': 'The Azure Fortress (E3M4) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 58, + 'doom_type': 8, + 'region': "The Azure Fortress (E3M4) Main"}, + 371420: {'name': 'The Azure Fortress (E3M4) - Bag of Holding 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 59, + 'doom_type': 8, + 'region': "The Azure Fortress (E3M4) Green"}, + 371421: {'name': 'The Azure Fortress (E3M4) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 60, + 'doom_type': 30, + 'region': "The Azure Fortress (E3M4) Main"}, + 371422: {'name': 'The Azure Fortress (E3M4) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 61, + 'doom_type': 32, + 'region': "The Azure Fortress (E3M4) Main"}, + 371423: {'name': 'The Azure Fortress (E3M4) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 62, + 'doom_type': 75, + 'region': "The Azure Fortress (E3M4) Main"}, + 371424: {'name': 'The Azure Fortress (E3M4) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 63, + 'doom_type': 85, + 'region': "The Azure Fortress (E3M4) Main"}, + 371425: {'name': 'The Azure Fortress (E3M4) - Silver Shield 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 64, + 'doom_type': 85, + 'region': "The Azure Fortress (E3M4) Green"}, + 371426: {'name': 'The Azure Fortress (E3M4) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 65, + 'doom_type': 31, + 'region': "The Azure Fortress (E3M4) Green"}, + 371427: {'name': 'The Azure Fortress (E3M4) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 66, + 'doom_type': 35, + 'region': "The Azure Fortress (E3M4) Green"}, + 371428: {'name': 'The Azure Fortress (E3M4) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 67, + 'doom_type': 36, + 'region': "The Azure Fortress (E3M4) Yellow"}, + 371429: {'name': 'The Azure Fortress (E3M4) - Tome of Power', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 68, + 'doom_type': 86, + 'region': "The Azure Fortress (E3M4) Green"}, + 371430: {'name': 'The Azure Fortress (E3M4) - Torch', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 69, + 'doom_type': 33, + 'region': "The Azure Fortress (E3M4) Main"}, + 371431: {'name': 'The Azure Fortress (E3M4) - Torch 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 70, + 'doom_type': 33, + 'region': "The Azure Fortress (E3M4) Green"}, + 371432: {'name': 'The Azure Fortress (E3M4) - Torch 3', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 71, + 'doom_type': 33, + 'region': "The Azure Fortress (E3M4) Yellow"}, + 371433: {'name': 'The Azure Fortress (E3M4) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 72, + 'doom_type': 86, + 'region': "The Azure Fortress (E3M4) Main"}, + 371434: {'name': 'The Azure Fortress (E3M4) - Tome of Power 3', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 73, + 'doom_type': 86, + 'region': "The Azure Fortress (E3M4) Main"}, + 371435: {'name': 'The Azure Fortress (E3M4) - Enchanted Shield 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 75, + 'doom_type': 31, + 'region': "The Azure Fortress (E3M4) Yellow"}, + 371436: {'name': 'The Azure Fortress (E3M4) - Morph Ovum 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 76, + 'doom_type': 30, + 'region': "The Azure Fortress (E3M4) Yellow"}, + 371437: {'name': 'The Azure Fortress (E3M4) - Mystic Urn 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 577, + 'doom_type': 32, + 'region': "The Azure Fortress (E3M4) Green"}, + 371438: {'name': 'The Azure Fortress (E3M4) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "The Azure Fortress (E3M4) Green"}, + 371439: {'name': 'The Ophidian Lair (E3M5) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 16, + 'doom_type': 80, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371440: {'name': 'The Ophidian Lair (E3M5) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 30, + 'doom_type': 73, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371441: {'name': 'The Ophidian Lair (E3M5) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 48, + 'doom_type': 2004, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371442: {'name': 'The Ophidian Lair (E3M5) - Phoenix Rod', + 'episode': 3, + 'check_sanity': True, + 'map': 5, + 'index': 49, + 'doom_type': 2003, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371443: {'name': 'The Ophidian Lair (E3M5) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 50, + 'doom_type': 53, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371444: {'name': 'The Ophidian Lair (E3M5) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 51, + 'doom_type': 2005, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371445: {'name': 'The Ophidian Lair (E3M5) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 52, + 'doom_type': 8, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371446: {'name': 'The Ophidian Lair (E3M5) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 53, + 'doom_type': 30, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371447: {'name': 'The Ophidian Lair (E3M5) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 62, + 'doom_type': 2001, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371448: {'name': 'The Ophidian Lair (E3M5) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 63, + 'doom_type': 32, + 'region': "The Ophidian Lair (E3M5) Green"}, + 371449: {'name': 'The Ophidian Lair (E3M5) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 64, + 'doom_type': 75, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371450: {'name': 'The Ophidian Lair (E3M5) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 65, + 'doom_type': 84, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371451: {'name': 'The Ophidian Lair (E3M5) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 66, + 'doom_type': 85, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371452: {'name': 'The Ophidian Lair (E3M5) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 67, + 'doom_type': 31, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371453: {'name': 'The Ophidian Lair (E3M5) - Silver Shield 2', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 68, + 'doom_type': 85, + 'region': "The Ophidian Lair (E3M5) Green"}, + 371454: {'name': 'The Ophidian Lair (E3M5) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 69, + 'doom_type': 35, + 'region': "The Ophidian Lair (E3M5) Green"}, + 371455: {'name': 'The Ophidian Lair (E3M5) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 70, + 'doom_type': 36, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371456: {'name': 'The Ophidian Lair (E3M5) - Torch', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 71, + 'doom_type': 33, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371457: {'name': 'The Ophidian Lair (E3M5) - Tome of Power', + 'episode': 3, + 'check_sanity': True, + 'map': 5, + 'index': 72, + 'doom_type': 86, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371458: {'name': 'The Ophidian Lair (E3M5) - Mystic Urn 2', + 'episode': 3, + 'check_sanity': True, + 'map': 5, + 'index': 73, + 'doom_type': 32, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371459: {'name': 'The Ophidian Lair (E3M5) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 74, + 'doom_type': 86, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371460: {'name': 'The Ophidian Lair (E3M5) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "The Ophidian Lair (E3M5) Green"}, + 371461: {'name': 'The Halls of Fear (E3M6) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 10, + 'doom_type': 80, + 'region': "The Halls of Fear (E3M6) Main"}, + 371462: {'name': 'The Halls of Fear (E3M6) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 12, + 'doom_type': 73, + 'region': "The Halls of Fear (E3M6) Yellow"}, + 371463: {'name': 'The Halls of Fear (E3M6) - Blue key', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 15, + 'doom_type': 79, + 'region': "The Halls of Fear (E3M6) Green"}, + 371464: {'name': 'The Halls of Fear (E3M6) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 31, + 'doom_type': 2004, + 'region': "The Halls of Fear (E3M6) Green"}, + 371465: {'name': 'The Halls of Fear (E3M6) - Phoenix Rod', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 32, + 'doom_type': 2003, + 'region': "The Halls of Fear (E3M6) Cyan"}, + 371466: {'name': 'The Halls of Fear (E3M6) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 33, + 'doom_type': 2001, + 'region': "The Halls of Fear (E3M6) Main"}, + 371467: {'name': 'The Halls of Fear (E3M6) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 34, + 'doom_type': 53, + 'region': "The Halls of Fear (E3M6) Main"}, + 371468: {'name': 'The Halls of Fear (E3M6) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 35, + 'doom_type': 2005, + 'region': "The Halls of Fear (E3M6) Yellow"}, + 371469: {'name': 'The Halls of Fear (E3M6) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 38, + 'doom_type': 36, + 'region': "The Halls of Fear (E3M6) Green"}, + 371470: {'name': 'The Halls of Fear (E3M6) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 40, + 'doom_type': 8, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371471: {'name': 'The Halls of Fear (E3M6) - Bag of Holding 2', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 41, + 'doom_type': 8, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371472: {'name': 'The Halls of Fear (E3M6) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 42, + 'doom_type': 30, + 'region': "The Halls of Fear (E3M6) Yellow"}, + 371473: {'name': 'The Halls of Fear (E3M6) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 51, + 'doom_type': 32, + 'region': "The Halls of Fear (E3M6) Yellow"}, + 371474: {'name': 'The Halls of Fear (E3M6) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 52, + 'doom_type': 75, + 'region': "The Halls of Fear (E3M6) Green"}, + 371475: {'name': 'The Halls of Fear (E3M6) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': True, + 'map': 6, + 'index': 53, + 'doom_type': 84, + 'region': "The Halls of Fear (E3M6) Main"}, + 371476: {'name': 'The Halls of Fear (E3M6) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 54, + 'doom_type': 85, + 'region': "The Halls of Fear (E3M6) Yellow"}, + 371477: {'name': 'The Halls of Fear (E3M6) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 55, + 'doom_type': 31, + 'region': "The Halls of Fear (E3M6) Cyan"}, + 371478: {'name': 'The Halls of Fear (E3M6) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 56, + 'doom_type': 35, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371479: {'name': 'The Halls of Fear (E3M6) - Tome of Power', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 57, + 'doom_type': 86, + 'region': "The Halls of Fear (E3M6) Cyan"}, + 371480: {'name': 'The Halls of Fear (E3M6) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 58, + 'doom_type': 86, + 'region': "The Halls of Fear (E3M6) Green"}, + 371481: {'name': 'The Halls of Fear (E3M6) - Mystic Urn 2', + 'episode': 3, + 'check_sanity': True, + 'map': 6, + 'index': 59, + 'doom_type': 32, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371482: {'name': 'The Halls of Fear (E3M6) - Bag of Holding 3', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 363, + 'doom_type': 8, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371483: {'name': 'The Halls of Fear (E3M6) - Tome of Power 3', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 364, + 'doom_type': 86, + 'region': "The Halls of Fear (E3M6) Main"}, + 371484: {'name': 'The Halls of Fear (E3M6) - Firemace', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 468, + 'doom_type': 2002, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371485: {'name': 'The Halls of Fear (E3M6) - Hellstaff 2', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 472, + 'doom_type': 2004, + 'region': "The Halls of Fear (E3M6) Main"}, + 371486: {'name': 'The Halls of Fear (E3M6) - Firemace 2', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 506, + 'doom_type': 2002, + 'region': "The Halls of Fear (E3M6) Green"}, + 371487: {'name': 'The Halls of Fear (E3M6) - Firemace 3', + 'episode': 3, + 'check_sanity': True, + 'map': 6, + 'index': 507, + 'doom_type': 2002, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371488: {'name': 'The Halls of Fear (E3M6) - Firemace 4', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 508, + 'doom_type': 2002, + 'region': "The Halls of Fear (E3M6) Main"}, + 371489: {'name': 'The Halls of Fear (E3M6) - Firemace 5', + 'episode': 3, + 'check_sanity': True, + 'map': 6, + 'index': 509, + 'doom_type': 2002, + 'region': "The Halls of Fear (E3M6) Green"}, + 371490: {'name': 'The Halls of Fear (E3M6) - Firemace 6', + 'episode': 3, + 'check_sanity': True, + 'map': 6, + 'index': 510, + 'doom_type': 2002, + 'region': "The Halls of Fear (E3M6) Green"}, + 371491: {'name': 'The Halls of Fear (E3M6) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371492: {'name': 'The Chasm (E3M7) - Blue key', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 5, + 'doom_type': 79, + 'region': "The Chasm (E3M7) Green"}, + 371493: {'name': 'The Chasm (E3M7) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 12, + 'doom_type': 73, + 'region': "The Chasm (E3M7) Yellow"}, + 371494: {'name': 'The Chasm (E3M7) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 26, + 'doom_type': 80, + 'region': "The Chasm (E3M7) Main"}, + 371495: {'name': 'The Chasm (E3M7) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 254, + 'doom_type': 2001, + 'region': "The Chasm (E3M7) Main"}, + 371496: {'name': 'The Chasm (E3M7) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 255, + 'doom_type': 2004, + 'region': "The Chasm (E3M7) Yellow"}, + 371497: {'name': 'The Chasm (E3M7) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 256, + 'doom_type': 2005, + 'region': "The Chasm (E3M7) Green"}, + 371498: {'name': 'The Chasm (E3M7) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 257, + 'doom_type': 53, + 'region': "The Chasm (E3M7) Main"}, + 371499: {'name': 'The Chasm (E3M7) - Phoenix Rod', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 259, + 'doom_type': 2003, + 'region': "The Chasm (E3M7) Green"}, + 371500: {'name': 'The Chasm (E3M7) - Shadowsphere', + 'episode': 3, + 'check_sanity': True, + 'map': 7, + 'index': 260, + 'doom_type': 75, + 'region': "The Chasm (E3M7) Green"}, + 371501: {'name': 'The Chasm (E3M7) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 262, + 'doom_type': 8, + 'region': "The Chasm (E3M7) Main"}, + 371502: {'name': 'The Chasm (E3M7) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 268, + 'doom_type': 85, + 'region': "The Chasm (E3M7) Main"}, + 371503: {'name': 'The Chasm (E3M7) - Tome of Power', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 269, + 'doom_type': 86, + 'region': "The Chasm (E3M7) Main"}, + 371504: {'name': 'The Chasm (E3M7) - Torch', + 'episode': 3, + 'check_sanity': True, + 'map': 7, + 'index': 270, + 'doom_type': 33, + 'region': "The Chasm (E3M7) Yellow"}, + 371505: {'name': 'The Chasm (E3M7) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 278, + 'doom_type': 30, + 'region': "The Chasm (E3M7) Yellow"}, + 371506: {'name': 'The Chasm (E3M7) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 282, + 'doom_type': 84, + 'region': "The Chasm (E3M7) Green"}, + 371507: {'name': 'The Chasm (E3M7) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 283, + 'doom_type': 32, + 'region': "The Chasm (E3M7) Green"}, + 371508: {'name': 'The Chasm (E3M7) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 284, + 'doom_type': 31, + 'region': "The Chasm (E3M7) Green"}, + 371509: {'name': 'The Chasm (E3M7) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 285, + 'doom_type': 35, + 'region': "The Chasm (E3M7) Green"}, + 371510: {'name': 'The Chasm (E3M7) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 286, + 'doom_type': 36, + 'region': "The Chasm (E3M7) Green"}, + 371511: {'name': 'The Chasm (E3M7) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 287, + 'doom_type': 86, + 'region': "The Chasm (E3M7) Green"}, + 371512: {'name': 'The Chasm (E3M7) - Tome of Power 3', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 288, + 'doom_type': 86, + 'region': "The Chasm (E3M7) Green"}, + 371513: {'name': 'The Chasm (E3M7) - Torch 2', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 289, + 'doom_type': 33, + 'region': "The Chasm (E3M7) Green"}, + 371514: {'name': 'The Chasm (E3M7) - Shadowsphere 2', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 337, + 'doom_type': 75, + 'region': "The Chasm (E3M7) Main"}, + 371515: {'name': 'The Chasm (E3M7) - Bag of Holding 2', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 660, + 'doom_type': 8, + 'region': "The Chasm (E3M7) Main"}, + 371516: {'name': 'The Chasm (E3M7) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "The Chasm (E3M7) Blue"}, + 371517: {'name': "D'Sparil'S Keep (E3M8) - Phoenix Rod", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 55, + 'doom_type': 2003, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371518: {'name': "D'Sparil'S Keep (E3M8) - Ethereal Crossbow", + 'episode': 3, + 'check_sanity': True, + 'map': 8, + 'index': 56, + 'doom_type': 2001, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371519: {'name': "D'Sparil'S Keep (E3M8) - Dragon Claw", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 57, + 'doom_type': 53, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371520: {'name': "D'Sparil'S Keep (E3M8) - Gauntlets of the Necromancer", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 58, + 'doom_type': 2005, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371521: {'name': "D'Sparil'S Keep (E3M8) - Hellstaff", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 59, + 'doom_type': 2004, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371522: {'name': "D'Sparil'S Keep (E3M8) - Bag of Holding", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 63, + 'doom_type': 8, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371523: {'name': "D'Sparil'S Keep (E3M8) - Mystic Urn", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 64, + 'doom_type': 32, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371524: {'name': "D'Sparil'S Keep (E3M8) - Ring of Invincibility", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 65, + 'doom_type': 84, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371525: {'name': "D'Sparil'S Keep (E3M8) - Shadowsphere", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 66, + 'doom_type': 75, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371526: {'name': "D'Sparil'S Keep (E3M8) - Silver Shield", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 67, + 'doom_type': 85, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371527: {'name': "D'Sparil'S Keep (E3M8) - Enchanted Shield", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 68, + 'doom_type': 31, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371528: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 69, + 'doom_type': 86, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371529: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power 2", + 'episode': 3, + 'check_sanity': True, + 'map': 8, + 'index': 70, + 'doom_type': 86, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371530: {'name': "D'Sparil'S Keep (E3M8) - Chaos Device", + 'episode': 3, + 'check_sanity': True, + 'map': 8, + 'index': 71, + 'doom_type': 36, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371531: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power 3", + 'episode': 3, + 'check_sanity': True, + 'map': 8, + 'index': 245, + 'doom_type': 86, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371532: {'name': "D'Sparil'S Keep (E3M8) - Exit", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371533: {'name': 'The Aquifier (E3M9) - Blue key', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 12, + 'doom_type': 79, + 'region': "The Aquifier (E3M9) Green"}, + 371534: {'name': 'The Aquifier (E3M9) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 13, + 'doom_type': 73, + 'region': "The Aquifier (E3M9) Yellow"}, + 371535: {'name': 'The Aquifier (E3M9) - Yellow key', + 'episode': 3, + 'check_sanity': True, + 'map': 9, + 'index': 14, + 'doom_type': 80, + 'region': "The Aquifier (E3M9) Main"}, + 371536: {'name': 'The Aquifier (E3M9) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 141, + 'doom_type': 2001, + 'region': "The Aquifier (E3M9) Main"}, + 371537: {'name': 'The Aquifier (E3M9) - Phoenix Rod', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 142, + 'doom_type': 2003, + 'region': "The Aquifier (E3M9) Yellow"}, + 371538: {'name': 'The Aquifier (E3M9) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 143, + 'doom_type': 53, + 'region': "The Aquifier (E3M9) Green"}, + 371539: {'name': 'The Aquifier (E3M9) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 144, + 'doom_type': 2004, + 'region': "The Aquifier (E3M9) Green"}, + 371540: {'name': 'The Aquifier (E3M9) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 145, + 'doom_type': 2005, + 'region': "The Aquifier (E3M9) Green"}, + 371541: {'name': 'The Aquifier (E3M9) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 148, + 'doom_type': 84, + 'region': "The Aquifier (E3M9) Yellow"}, + 371542: {'name': 'The Aquifier (E3M9) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 149, + 'doom_type': 32, + 'region': "The Aquifier (E3M9) Green"}, + 371543: {'name': 'The Aquifier (E3M9) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 151, + 'doom_type': 85, + 'region': "The Aquifier (E3M9) Main"}, + 371544: {'name': 'The Aquifier (E3M9) - Tome of Power', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 152, + 'doom_type': 86, + 'region': "The Aquifier (E3M9) Main"}, + 371545: {'name': 'The Aquifier (E3M9) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 153, + 'doom_type': 8, + 'region': "The Aquifier (E3M9) Yellow"}, + 371546: {'name': 'The Aquifier (E3M9) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 154, + 'doom_type': 30, + 'region': "The Aquifier (E3M9) Green"}, + 371547: {'name': 'The Aquifier (E3M9) - Map Scroll', + 'episode': 3, + 'check_sanity': True, + 'map': 9, + 'index': 155, + 'doom_type': 35, + 'region': "The Aquifier (E3M9) Green"}, + 371548: {'name': 'The Aquifier (E3M9) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 156, + 'doom_type': 36, + 'region': "The Aquifier (E3M9) Yellow"}, + 371549: {'name': 'The Aquifier (E3M9) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 157, + 'doom_type': 31, + 'region': "The Aquifier (E3M9) Green"}, + 371550: {'name': 'The Aquifier (E3M9) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 158, + 'doom_type': 86, + 'region': "The Aquifier (E3M9) Green"}, + 371551: {'name': 'The Aquifier (E3M9) - Torch', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 159, + 'doom_type': 33, + 'region': "The Aquifier (E3M9) Main"}, + 371552: {'name': 'The Aquifier (E3M9) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 160, + 'doom_type': 75, + 'region': "The Aquifier (E3M9) Green"}, + 371553: {'name': 'The Aquifier (E3M9) - Silver Shield 2', + 'episode': 3, + 'check_sanity': True, + 'map': 9, + 'index': 374, + 'doom_type': 85, + 'region': "The Aquifier (E3M9) Green"}, + 371554: {'name': 'The Aquifier (E3M9) - Firemace', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 478, + 'doom_type': 2002, + 'region': "The Aquifier (E3M9) Green"}, + 371555: {'name': 'The Aquifier (E3M9) - Firemace 2', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 526, + 'doom_type': 2002, + 'region': "The Aquifier (E3M9) Green"}, + 371556: {'name': 'The Aquifier (E3M9) - Firemace 3', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 527, + 'doom_type': 2002, + 'region': "The Aquifier (E3M9) Green"}, + 371557: {'name': 'The Aquifier (E3M9) - Firemace 4', + 'episode': 3, + 'check_sanity': True, + 'map': 9, + 'index': 528, + 'doom_type': 2002, + 'region': "The Aquifier (E3M9) Yellow"}, + 371558: {'name': 'The Aquifier (E3M9) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "The Aquifier (E3M9) Blue"}, + 371559: {'name': 'Catafalque (E4M1) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 4, + 'doom_type': 80, + 'region': "Catafalque (E4M1) Main"}, + 371560: {'name': 'Catafalque (E4M1) - Green key', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 10, + 'doom_type': 73, + 'region': "Catafalque (E4M1) Yellow"}, + 371561: {'name': 'Catafalque (E4M1) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 100, + 'doom_type': 2001, + 'region': "Catafalque (E4M1) Main"}, + 371562: {'name': 'Catafalque (E4M1) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 101, + 'doom_type': 2005, + 'region': "Catafalque (E4M1) Yellow"}, + 371563: {'name': 'Catafalque (E4M1) - Dragon Claw', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 102, + 'doom_type': 53, + 'region': "Catafalque (E4M1) Yellow"}, + 371564: {'name': 'Catafalque (E4M1) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 103, + 'doom_type': 2004, + 'region': "Catafalque (E4M1) Green"}, + 371565: {'name': 'Catafalque (E4M1) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 114, + 'doom_type': 75, + 'region': "Catafalque (E4M1) Yellow"}, + 371566: {'name': 'Catafalque (E4M1) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 115, + 'doom_type': 84, + 'region': "Catafalque (E4M1) Green"}, + 371567: {'name': 'Catafalque (E4M1) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 116, + 'doom_type': 85, + 'region': "Catafalque (E4M1) Main"}, + 371568: {'name': 'Catafalque (E4M1) - Map Scroll', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 117, + 'doom_type': 35, + 'region': "Catafalque (E4M1) Green"}, + 371569: {'name': 'Catafalque (E4M1) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 118, + 'doom_type': 36, + 'region': "Catafalque (E4M1) Yellow"}, + 371570: {'name': 'Catafalque (E4M1) - Tome of Power', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 119, + 'doom_type': 86, + 'region': "Catafalque (E4M1) Yellow"}, + 371571: {'name': 'Catafalque (E4M1) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 120, + 'doom_type': 86, + 'region': "Catafalque (E4M1) Main"}, + 371572: {'name': 'Catafalque (E4M1) - Torch', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 121, + 'doom_type': 33, + 'region': "Catafalque (E4M1) Yellow"}, + 371573: {'name': 'Catafalque (E4M1) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 122, + 'doom_type': 8, + 'region': "Catafalque (E4M1) Main"}, + 371574: {'name': 'Catafalque (E4M1) - Morph Ovum', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 123, + 'doom_type': 30, + 'region': "Catafalque (E4M1) Main"}, + 371575: {'name': 'Catafalque (E4M1) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "Catafalque (E4M1) Green"}, + 371576: {'name': 'Blockhouse (E4M2) - Green key', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 18, + 'doom_type': 73, + 'region': "Blockhouse (E4M2) Yellow"}, + 371577: {'name': 'Blockhouse (E4M2) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 19, + 'doom_type': 80, + 'region': "Blockhouse (E4M2) Main"}, + 371578: {'name': 'Blockhouse (E4M2) - Blue key', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 25, + 'doom_type': 79, + 'region': "Blockhouse (E4M2) Green"}, + 371579: {'name': 'Blockhouse (E4M2) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 46, + 'doom_type': 2005, + 'region': "Blockhouse (E4M2) Main"}, + 371580: {'name': 'Blockhouse (E4M2) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 47, + 'doom_type': 2001, + 'region': "Blockhouse (E4M2) Main"}, + 371581: {'name': 'Blockhouse (E4M2) - Dragon Claw', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 48, + 'doom_type': 53, + 'region': "Blockhouse (E4M2) Main"}, + 371582: {'name': 'Blockhouse (E4M2) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 49, + 'doom_type': 2004, + 'region': "Blockhouse (E4M2) Main"}, + 371583: {'name': 'Blockhouse (E4M2) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 50, + 'doom_type': 2003, + 'region': "Blockhouse (E4M2) Main"}, + 371584: {'name': 'Blockhouse (E4M2) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 58, + 'doom_type': 8, + 'region': "Blockhouse (E4M2) Main"}, + 371585: {'name': 'Blockhouse (E4M2) - Mystic Urn', + 'episode': 4, + 'check_sanity': True, + 'map': 2, + 'index': 67, + 'doom_type': 32, + 'region': "Blockhouse (E4M2) Main"}, + 371586: {'name': 'Blockhouse (E4M2) - Silver Shield', + 'episode': 4, + 'check_sanity': True, + 'map': 2, + 'index': 68, + 'doom_type': 85, + 'region': "Blockhouse (E4M2) Main"}, + 371587: {'name': 'Blockhouse (E4M2) - Morph Ovum', + 'episode': 4, + 'check_sanity': True, + 'map': 2, + 'index': 69, + 'doom_type': 30, + 'region': "Blockhouse (E4M2) Main"}, + 371588: {'name': 'Blockhouse (E4M2) - Tome of Power', + 'episode': 4, + 'check_sanity': True, + 'map': 2, + 'index': 70, + 'doom_type': 86, + 'region': "Blockhouse (E4M2) Main"}, + 371589: {'name': 'Blockhouse (E4M2) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 71, + 'doom_type': 36, + 'region': "Blockhouse (E4M2) Green"}, + 371590: {'name': 'Blockhouse (E4M2) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 72, + 'doom_type': 84, + 'region': "Blockhouse (E4M2) Green"}, + 371591: {'name': 'Blockhouse (E4M2) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 73, + 'doom_type': 8, + 'region': "Blockhouse (E4M2) Green"}, + 371592: {'name': 'Blockhouse (E4M2) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 74, + 'doom_type': 31, + 'region': "Blockhouse (E4M2) Yellow"}, + 371593: {'name': 'Blockhouse (E4M2) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 75, + 'doom_type': 75, + 'region': "Blockhouse (E4M2) Main"}, + 371594: {'name': 'Blockhouse (E4M2) - Ring of Invincibility 2', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 226, + 'doom_type': 84, + 'region': "Blockhouse (E4M2) Lake"}, + 371595: {'name': 'Blockhouse (E4M2) - Shadowsphere 2', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 227, + 'doom_type': 75, + 'region': "Blockhouse (E4M2) Lake"}, + 371596: {'name': 'Blockhouse (E4M2) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "Blockhouse (E4M2) Blue"}, + 371597: {'name': 'Ambulatory (E4M3) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 10, + 'doom_type': 80, + 'region': "Ambulatory (E4M3) Main"}, + 371598: {'name': 'Ambulatory (E4M3) - Green key', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 11, + 'doom_type': 73, + 'region': "Ambulatory (E4M3) Yellow"}, + 371599: {'name': 'Ambulatory (E4M3) - Blue key', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 12, + 'doom_type': 79, + 'region': "Ambulatory (E4M3) Green"}, + 371600: {'name': 'Ambulatory (E4M3) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': True, + 'map': 3, + 'index': 265, + 'doom_type': 2001, + 'region': "Ambulatory (E4M3) Main"}, + 371601: {'name': 'Ambulatory (E4M3) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': True, + 'map': 3, + 'index': 266, + 'doom_type': 2005, + 'region': "Ambulatory (E4M3) Main"}, + 371602: {'name': 'Ambulatory (E4M3) - Dragon Claw', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 267, + 'doom_type': 53, + 'region': "Ambulatory (E4M3) Yellow"}, + 371603: {'name': 'Ambulatory (E4M3) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 268, + 'doom_type': 2004, + 'region': "Ambulatory (E4M3) Green"}, + 371604: {'name': 'Ambulatory (E4M3) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 269, + 'doom_type': 2003, + 'region': "Ambulatory (E4M3) Blue"}, + 371605: {'name': 'Ambulatory (E4M3) - Tome of Power', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 270, + 'doom_type': 86, + 'region': "Ambulatory (E4M3) Main"}, + 371606: {'name': 'Ambulatory (E4M3) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 271, + 'doom_type': 85, + 'region': "Ambulatory (E4M3) Yellow"}, + 371607: {'name': 'Ambulatory (E4M3) - Map Scroll', + 'episode': 4, + 'check_sanity': True, + 'map': 3, + 'index': 272, + 'doom_type': 35, + 'region': "Ambulatory (E4M3) Yellow"}, + 371608: {'name': 'Ambulatory (E4M3) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 273, + 'doom_type': 8, + 'region': "Ambulatory (E4M3) Yellow"}, + 371609: {'name': 'Ambulatory (E4M3) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 274, + 'doom_type': 75, + 'region': "Ambulatory (E4M3) Yellow"}, + 371610: {'name': 'Ambulatory (E4M3) - Morph Ovum', + 'episode': 4, + 'check_sanity': True, + 'map': 3, + 'index': 275, + 'doom_type': 30, + 'region': "Ambulatory (E4M3) Yellow"}, + 371611: {'name': 'Ambulatory (E4M3) - Torch', + 'episode': 4, + 'check_sanity': True, + 'map': 3, + 'index': 276, + 'doom_type': 33, + 'region': "Ambulatory (E4M3) Green"}, + 371612: {'name': 'Ambulatory (E4M3) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 277, + 'doom_type': 86, + 'region': "Ambulatory (E4M3) Green"}, + 371613: {'name': 'Ambulatory (E4M3) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 278, + 'doom_type': 31, + 'region': "Ambulatory (E4M3) Blue"}, + 371614: {'name': 'Ambulatory (E4M3) - Mystic Urn', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 279, + 'doom_type': 32, + 'region': "Ambulatory (E4M3) Blue"}, + 371615: {'name': 'Ambulatory (E4M3) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 281, + 'doom_type': 84, + 'region': "Ambulatory (E4M3) Blue"}, + 371616: {'name': 'Ambulatory (E4M3) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 282, + 'doom_type': 36, + 'region': "Ambulatory (E4M3) Green"}, + 371617: {'name': 'Ambulatory (E4M3) - Ring of Invincibility 2', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 283, + 'doom_type': 84, + 'region': "Ambulatory (E4M3) Green"}, + 371618: {'name': 'Ambulatory (E4M3) - Morph Ovum 2', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 284, + 'doom_type': 30, + 'region': "Ambulatory (E4M3) Blue"}, + 371619: {'name': 'Ambulatory (E4M3) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 285, + 'doom_type': 8, + 'region': "Ambulatory (E4M3) Yellow"}, + 371620: {'name': 'Ambulatory (E4M3) - Firemace', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 297, + 'doom_type': 2002, + 'region': "Ambulatory (E4M3) Green"}, + 371621: {'name': 'Ambulatory (E4M3) - Firemace 2', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 298, + 'doom_type': 2002, + 'region': "Ambulatory (E4M3) Yellow"}, + 371622: {'name': 'Ambulatory (E4M3) - Firemace 3', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 299, + 'doom_type': 2002, + 'region': "Ambulatory (E4M3) Yellow"}, + 371623: {'name': 'Ambulatory (E4M3) - Firemace 4', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 300, + 'doom_type': 2002, + 'region': "Ambulatory (E4M3) Blue"}, + 371624: {'name': 'Ambulatory (E4M3) - Firemace 5', + 'episode': 4, + 'check_sanity': True, + 'map': 3, + 'index': 301, + 'doom_type': 2002, + 'region': "Ambulatory (E4M3) Green"}, + 371625: {'name': 'Ambulatory (E4M3) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "Ambulatory (E4M3) Blue"}, + 371626: {'name': 'Sepulcher (E4M4) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 27, + 'doom_type': 85, + 'region': "Sepulcher (E4M4) Main"}, + 371627: {'name': 'Sepulcher (E4M4) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 28, + 'doom_type': 2004, + 'region': "Sepulcher (E4M4) Main"}, + 371628: {'name': 'Sepulcher (E4M4) - Dragon Claw', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 29, + 'doom_type': 53, + 'region': "Sepulcher (E4M4) Main"}, + 371629: {'name': 'Sepulcher (E4M4) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 30, + 'doom_type': 2001, + 'region': "Sepulcher (E4M4) Main"}, + 371630: {'name': 'Sepulcher (E4M4) - Tome of Power', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 31, + 'doom_type': 86, + 'region': "Sepulcher (E4M4) Main"}, + 371631: {'name': 'Sepulcher (E4M4) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 40, + 'doom_type': 75, + 'region': "Sepulcher (E4M4) Main"}, + 371632: {'name': 'Sepulcher (E4M4) - Mystic Urn', + 'episode': 4, + 'check_sanity': True, + 'map': 4, + 'index': 41, + 'doom_type': 32, + 'region': "Sepulcher (E4M4) Main"}, + 371633: {'name': 'Sepulcher (E4M4) - Chaos Device', + 'episode': 4, + 'check_sanity': True, + 'map': 4, + 'index': 50, + 'doom_type': 36, + 'region': "Sepulcher (E4M4) Main"}, + 371634: {'name': 'Sepulcher (E4M4) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 51, + 'doom_type': 31, + 'region': "Sepulcher (E4M4) Main"}, + 371635: {'name': 'Sepulcher (E4M4) - Morph Ovum', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 65, + 'doom_type': 30, + 'region': "Sepulcher (E4M4) Main"}, + 371636: {'name': 'Sepulcher (E4M4) - Torch', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 66, + 'doom_type': 33, + 'region': "Sepulcher (E4M4) Main"}, + 371637: {'name': 'Sepulcher (E4M4) - Firemace', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 67, + 'doom_type': 2002, + 'region': "Sepulcher (E4M4) Main"}, + 371638: {'name': 'Sepulcher (E4M4) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 74, + 'doom_type': 2003, + 'region': "Sepulcher (E4M4) Main"}, + 371639: {'name': 'Sepulcher (E4M4) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 137, + 'doom_type': 84, + 'region': "Sepulcher (E4M4) Main"}, + 371640: {'name': 'Sepulcher (E4M4) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 138, + 'doom_type': 8, + 'region': "Sepulcher (E4M4) Main"}, + 371641: {'name': 'Sepulcher (E4M4) - Ethereal Crossbow 2', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 199, + 'doom_type': 2001, + 'region': "Sepulcher (E4M4) Main"}, + 371642: {'name': 'Sepulcher (E4M4) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 235, + 'doom_type': 8, + 'region': "Sepulcher (E4M4) Main"}, + 371643: {'name': 'Sepulcher (E4M4) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 239, + 'doom_type': 86, + 'region': "Sepulcher (E4M4) Main"}, + 371644: {'name': 'Sepulcher (E4M4) - Torch 2', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 243, + 'doom_type': 33, + 'region': "Sepulcher (E4M4) Main"}, + 371645: {'name': 'Sepulcher (E4M4) - Silver Shield 2', + 'episode': 4, + 'check_sanity': True, + 'map': 4, + 'index': 244, + 'doom_type': 85, + 'region': "Sepulcher (E4M4) Main"}, + 371646: {'name': 'Sepulcher (E4M4) - Firemace 2', + 'episode': 4, + 'check_sanity': True, + 'map': 4, + 'index': 307, + 'doom_type': 2002, + 'region': "Sepulcher (E4M4) Main"}, + 371647: {'name': 'Sepulcher (E4M4) - Firemace 3', + 'episode': 4, + 'check_sanity': True, + 'map': 4, + 'index': 308, + 'doom_type': 2002, + 'region': "Sepulcher (E4M4) Main"}, + 371648: {'name': 'Sepulcher (E4M4) - Firemace 4', + 'episode': 4, + 'check_sanity': True, + 'map': 4, + 'index': 309, + 'doom_type': 2002, + 'region': "Sepulcher (E4M4) Main"}, + 371649: {'name': 'Sepulcher (E4M4) - Firemace 5', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 310, + 'doom_type': 2002, + 'region': "Sepulcher (E4M4) Main"}, + 371650: {'name': 'Sepulcher (E4M4) - Dragon Claw 2', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 325, + 'doom_type': 53, + 'region': "Sepulcher (E4M4) Main"}, + 371651: {'name': 'Sepulcher (E4M4) - Phoenix Rod 2', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 339, + 'doom_type': 2003, + 'region': "Sepulcher (E4M4) Main"}, + 371652: {'name': 'Sepulcher (E4M4) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "Sepulcher (E4M4) Main"}, + 371653: {'name': 'Great Stair (E4M5) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 3, + 'doom_type': 2001, + 'region': "Great Stair (E4M5) Main"}, + 371654: {'name': 'Great Stair (E4M5) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 27, + 'doom_type': 80, + 'region': "Great Stair (E4M5) Main"}, + 371655: {'name': 'Great Stair (E4M5) - Dragon Claw', + 'episode': 4, + 'check_sanity': True, + 'map': 5, + 'index': 58, + 'doom_type': 53, + 'region': "Great Stair (E4M5) Yellow"}, + 371656: {'name': 'Great Stair (E4M5) - Green key', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 64, + 'doom_type': 73, + 'region': "Great Stair (E4M5) Yellow"}, + 371657: {'name': 'Great Stair (E4M5) - Blue key', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 71, + 'doom_type': 79, + 'region': "Great Stair (E4M5) Green"}, + 371658: {'name': 'Great Stair (E4M5) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 78, + 'doom_type': 85, + 'region': "Great Stair (E4M5) Main"}, + 371659: {'name': 'Great Stair (E4M5) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 90, + 'doom_type': 2005, + 'region': "Great Stair (E4M5) Main"}, + 371660: {'name': 'Great Stair (E4M5) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 91, + 'doom_type': 2004, + 'region': "Great Stair (E4M5) Yellow"}, + 371661: {'name': 'Great Stair (E4M5) - Phoenix Rod', + 'episode': 4, + 'check_sanity': True, + 'map': 5, + 'index': 92, + 'doom_type': 2003, + 'region': "Great Stair (E4M5) Green"}, + 371662: {'name': 'Great Stair (E4M5) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 93, + 'doom_type': 8, + 'region': "Great Stair (E4M5) Main"}, + 371663: {'name': 'Great Stair (E4M5) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 94, + 'doom_type': 8, + 'region': "Great Stair (E4M5) Green"}, + 371664: {'name': 'Great Stair (E4M5) - Morph Ovum', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 95, + 'doom_type': 30, + 'region': "Great Stair (E4M5) Main"}, + 371665: {'name': 'Great Stair (E4M5) - Mystic Urn', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 110, + 'doom_type': 32, + 'region': "Great Stair (E4M5) Yellow"}, + 371666: {'name': 'Great Stair (E4M5) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 111, + 'doom_type': 75, + 'region': "Great Stair (E4M5) Yellow"}, + 371667: {'name': 'Great Stair (E4M5) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 112, + 'doom_type': 84, + 'region': "Great Stair (E4M5) Main"}, + 371668: {'name': 'Great Stair (E4M5) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 113, + 'doom_type': 31, + 'region': "Great Stair (E4M5) Green"}, + 371669: {'name': 'Great Stair (E4M5) - Map Scroll', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 114, + 'doom_type': 35, + 'region': "Great Stair (E4M5) Green"}, + 371670: {'name': 'Great Stair (E4M5) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 115, + 'doom_type': 36, + 'region': "Great Stair (E4M5) Main"}, + 371671: {'name': 'Great Stair (E4M5) - Tome of Power', + 'episode': 4, + 'check_sanity': True, + 'map': 5, + 'index': 116, + 'doom_type': 86, + 'region': "Great Stair (E4M5) Main"}, + 371672: {'name': 'Great Stair (E4M5) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 117, + 'doom_type': 86, + 'region': "Great Stair (E4M5) Yellow"}, + 371673: {'name': 'Great Stair (E4M5) - Torch', + 'episode': 4, + 'check_sanity': True, + 'map': 5, + 'index': 118, + 'doom_type': 33, + 'region': "Great Stair (E4M5) Main"}, + 371674: {'name': 'Great Stair (E4M5) - Firemace', + 'episode': 4, + 'check_sanity': True, + 'map': 5, + 'index': 123, + 'doom_type': 2002, + 'region': "Great Stair (E4M5) Main"}, + 371675: {'name': 'Great Stair (E4M5) - Firemace 2', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 124, + 'doom_type': 2002, + 'region': "Great Stair (E4M5) Main"}, + 371676: {'name': 'Great Stair (E4M5) - Firemace 3', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 125, + 'doom_type': 2002, + 'region': "Great Stair (E4M5) Yellow"}, + 371677: {'name': 'Great Stair (E4M5) - Firemace 4', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 126, + 'doom_type': 2002, + 'region': "Great Stair (E4M5) Blue"}, + 371678: {'name': 'Great Stair (E4M5) - Firemace 5', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 127, + 'doom_type': 2002, + 'region': "Great Stair (E4M5) Yellow"}, + 371679: {'name': 'Great Stair (E4M5) - Mystic Urn 2', + 'episode': 4, + 'check_sanity': True, + 'map': 5, + 'index': 507, + 'doom_type': 32, + 'region': "Great Stair (E4M5) Green"}, + 371680: {'name': 'Great Stair (E4M5) - Tome of Power 3', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 508, + 'doom_type': 86, + 'region': "Great Stair (E4M5) Green"}, + 371681: {'name': 'Great Stair (E4M5) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "Great Stair (E4M5) Blue"}, + 371682: {'name': 'Halls of the Apostate (E4M6) - Green key', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 17, + 'doom_type': 73, + 'region': "Halls of the Apostate (E4M6) Yellow"}, + 371683: {'name': 'Halls of the Apostate (E4M6) - Blue key', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 18, + 'doom_type': 79, + 'region': "Halls of the Apostate (E4M6) Green"}, + 371684: {'name': 'Halls of the Apostate (E4M6) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 59, + 'doom_type': 2005, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371685: {'name': 'Halls of the Apostate (E4M6) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 60, + 'doom_type': 2001, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371686: {'name': 'Halls of the Apostate (E4M6) - Dragon Claw', + 'episode': 4, + 'check_sanity': True, + 'map': 6, + 'index': 61, + 'doom_type': 53, + 'region': "Halls of the Apostate (E4M6) Yellow"}, + 371687: {'name': 'Halls of the Apostate (E4M6) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 62, + 'doom_type': 2004, + 'region': "Halls of the Apostate (E4M6) Green"}, + 371688: {'name': 'Halls of the Apostate (E4M6) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 63, + 'doom_type': 2003, + 'region': "Halls of the Apostate (E4M6) Blue"}, + 371689: {'name': 'Halls of the Apostate (E4M6) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 68, + 'doom_type': 8, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371690: {'name': 'Halls of the Apostate (E4M6) - Morph Ovum', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 79, + 'doom_type': 30, + 'region': "Halls of the Apostate (E4M6) Yellow"}, + 371691: {'name': 'Halls of the Apostate (E4M6) - Mystic Urn', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 80, + 'doom_type': 32, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371692: {'name': 'Halls of the Apostate (E4M6) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 81, + 'doom_type': 75, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371693: {'name': 'Halls of the Apostate (E4M6) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 82, + 'doom_type': 85, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371694: {'name': 'Halls of the Apostate (E4M6) - Silver Shield 2', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 83, + 'doom_type': 85, + 'region': "Halls of the Apostate (E4M6) Blue"}, + 371695: {'name': 'Halls of the Apostate (E4M6) - Enchanted Shield', + 'episode': 4, + 'check_sanity': True, + 'map': 6, + 'index': 84, + 'doom_type': 31, + 'region': "Halls of the Apostate (E4M6) Green"}, + 371696: {'name': 'Halls of the Apostate (E4M6) - Map Scroll', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 85, + 'doom_type': 35, + 'region': "Halls of the Apostate (E4M6) Green"}, + 371697: {'name': 'Halls of the Apostate (E4M6) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 86, + 'doom_type': 36, + 'region': "Halls of the Apostate (E4M6) Yellow"}, + 371698: {'name': 'Halls of the Apostate (E4M6) - Tome of Power', + 'episode': 4, + 'check_sanity': True, + 'map': 6, + 'index': 87, + 'doom_type': 86, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371699: {'name': 'Halls of the Apostate (E4M6) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 88, + 'doom_type': 86, + 'region': "Halls of the Apostate (E4M6) Blue"}, + 371700: {'name': 'Halls of the Apostate (E4M6) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 89, + 'doom_type': 8, + 'region': "Halls of the Apostate (E4M6) Green"}, + 371701: {'name': 'Halls of the Apostate (E4M6) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 108, + 'doom_type': 84, + 'region': "Halls of the Apostate (E4M6) Yellow"}, + 371702: {'name': 'Halls of the Apostate (E4M6) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 420, + 'doom_type': 80, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371703: {'name': 'Halls of the Apostate (E4M6) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "Halls of the Apostate (E4M6) Blue"}, + 371704: {'name': 'Ramparts of Perdition (E4M7) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 28, + 'doom_type': 80, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371705: {'name': 'Ramparts of Perdition (E4M7) - Green key', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 33, + 'doom_type': 73, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371706: {'name': 'Ramparts of Perdition (E4M7) - Blue key', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 36, + 'doom_type': 79, + 'region': "Ramparts of Perdition (E4M7) Green"}, + 371707: {'name': 'Ramparts of Perdition (E4M7) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 39, + 'doom_type': 84, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371708: {'name': 'Ramparts of Perdition (E4M7) - Mystic Urn', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 40, + 'doom_type': 32, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371709: {'name': 'Ramparts of Perdition (E4M7) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 124, + 'doom_type': 2005, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371710: {'name': 'Ramparts of Perdition (E4M7) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 125, + 'doom_type': 2001, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371711: {'name': 'Ramparts of Perdition (E4M7) - Dragon Claw', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 126, + 'doom_type': 53, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371712: {'name': 'Ramparts of Perdition (E4M7) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 127, + 'doom_type': 2003, + 'region': "Ramparts of Perdition (E4M7) Green"}, + 371713: {'name': 'Ramparts of Perdition (E4M7) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 128, + 'doom_type': 2004, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371714: {'name': 'Ramparts of Perdition (E4M7) - Ethereal Crossbow 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 129, + 'doom_type': 2001, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371715: {'name': 'Ramparts of Perdition (E4M7) - Dragon Claw 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 130, + 'doom_type': 53, + 'region': "Ramparts of Perdition (E4M7) Blue"}, + 371716: {'name': 'Ramparts of Perdition (E4M7) - Phoenix Rod 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 131, + 'doom_type': 2003, + 'region': "Ramparts of Perdition (E4M7) Blue"}, + 371717: {'name': 'Ramparts of Perdition (E4M7) - Hellstaff 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 132, + 'doom_type': 2004, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371718: {'name': 'Ramparts of Perdition (E4M7) - Firemace', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 133, + 'doom_type': 2002, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371719: {'name': 'Ramparts of Perdition (E4M7) - Firemace 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 134, + 'doom_type': 2002, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371720: {'name': 'Ramparts of Perdition (E4M7) - Firemace 3', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 135, + 'doom_type': 2002, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371721: {'name': 'Ramparts of Perdition (E4M7) - Firemace 4', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 136, + 'doom_type': 2002, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371722: {'name': 'Ramparts of Perdition (E4M7) - Firemace 5', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 137, + 'doom_type': 2002, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371723: {'name': 'Ramparts of Perdition (E4M7) - Firemace 6', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 138, + 'doom_type': 2002, + 'region': "Ramparts of Perdition (E4M7) Blue"}, + 371724: {'name': 'Ramparts of Perdition (E4M7) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 140, + 'doom_type': 8, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371725: {'name': 'Ramparts of Perdition (E4M7) - Tome of Power', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 141, + 'doom_type': 86, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371726: {'name': 'Ramparts of Perdition (E4M7) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 142, + 'doom_type': 8, + 'region': "Ramparts of Perdition (E4M7) Green"}, + 371727: {'name': 'Ramparts of Perdition (E4M7) - Morph Ovum', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 143, + 'doom_type': 30, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371728: {'name': 'Ramparts of Perdition (E4M7) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 153, + 'doom_type': 75, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371729: {'name': 'Ramparts of Perdition (E4M7) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 154, + 'doom_type': 85, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371730: {'name': 'Ramparts of Perdition (E4M7) - Silver Shield 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 155, + 'doom_type': 85, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371731: {'name': 'Ramparts of Perdition (E4M7) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 156, + 'doom_type': 31, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371732: {'name': 'Ramparts of Perdition (E4M7) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 157, + 'doom_type': 86, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371733: {'name': 'Ramparts of Perdition (E4M7) - Tome of Power 3', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 158, + 'doom_type': 86, + 'region': "Ramparts of Perdition (E4M7) Blue"}, + 371734: {'name': 'Ramparts of Perdition (E4M7) - Torch', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 159, + 'doom_type': 33, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371735: {'name': 'Ramparts of Perdition (E4M7) - Torch 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 160, + 'doom_type': 33, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371736: {'name': 'Ramparts of Perdition (E4M7) - Mystic Urn 2', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 161, + 'doom_type': 32, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371737: {'name': 'Ramparts of Perdition (E4M7) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 162, + 'doom_type': 36, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371738: {'name': 'Ramparts of Perdition (E4M7) - Map Scroll', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 163, + 'doom_type': 35, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371739: {'name': 'Ramparts of Perdition (E4M7) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "Ramparts of Perdition (E4M7) Blue"}, + 371740: {'name': 'Shattered Bridge (E4M8) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 5, + 'doom_type': 80, + 'region': "Shattered Bridge (E4M8) Main"}, + 371741: {'name': 'Shattered Bridge (E4M8) - Dragon Claw', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 58, + 'doom_type': 53, + 'region': "Shattered Bridge (E4M8) Main"}, + 371742: {'name': 'Shattered Bridge (E4M8) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 79, + 'doom_type': 2003, + 'region': "Shattered Bridge (E4M8) Boss"}, + 371743: {'name': 'Shattered Bridge (E4M8) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 80, + 'doom_type': 2001, + 'region': "Shattered Bridge (E4M8) Main"}, + 371744: {'name': 'Shattered Bridge (E4M8) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 81, + 'doom_type': 2005, + 'region': "Shattered Bridge (E4M8) Main"}, + 371745: {'name': 'Shattered Bridge (E4M8) - Hellstaff', + 'episode': 4, + 'check_sanity': True, + 'map': 8, + 'index': 82, + 'doom_type': 2004, + 'region': "Shattered Bridge (E4M8) Main"}, + 371746: {'name': 'Shattered Bridge (E4M8) - Bag of Holding', + 'episode': 4, + 'check_sanity': True, + 'map': 8, + 'index': 96, + 'doom_type': 8, + 'region': "Shattered Bridge (E4M8) Main"}, + 371747: {'name': 'Shattered Bridge (E4M8) - Morph Ovum', + 'episode': 4, + 'check_sanity': True, + 'map': 8, + 'index': 97, + 'doom_type': 30, + 'region': "Shattered Bridge (E4M8) Main"}, + 371748: {'name': 'Shattered Bridge (E4M8) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 98, + 'doom_type': 85, + 'region': "Shattered Bridge (E4M8) Main"}, + 371749: {'name': 'Shattered Bridge (E4M8) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 108, + 'doom_type': 8, + 'region': "Shattered Bridge (E4M8) Main"}, + 371750: {'name': 'Shattered Bridge (E4M8) - Mystic Urn', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 109, + 'doom_type': 32, + 'region': "Shattered Bridge (E4M8) Main"}, + 371751: {'name': 'Shattered Bridge (E4M8) - Shadowsphere', + 'episode': 4, + 'check_sanity': True, + 'map': 8, + 'index': 110, + 'doom_type': 75, + 'region': "Shattered Bridge (E4M8) Main"}, + 371752: {'name': 'Shattered Bridge (E4M8) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 111, + 'doom_type': 84, + 'region': "Shattered Bridge (E4M8) Main"}, + 371753: {'name': 'Shattered Bridge (E4M8) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 112, + 'doom_type': 36, + 'region': "Shattered Bridge (E4M8) Main"}, + 371754: {'name': 'Shattered Bridge (E4M8) - Tome of Power', + 'episode': 4, + 'check_sanity': True, + 'map': 8, + 'index': 113, + 'doom_type': 86, + 'region': "Shattered Bridge (E4M8) Main"}, + 371755: {'name': 'Shattered Bridge (E4M8) - Torch', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 114, + 'doom_type': 33, + 'region': "Shattered Bridge (E4M8) Main"}, + 371756: {'name': 'Shattered Bridge (E4M8) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 115, + 'doom_type': 86, + 'region': "Shattered Bridge (E4M8) Main"}, + 371757: {'name': 'Shattered Bridge (E4M8) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 118, + 'doom_type': 31, + 'region': "Shattered Bridge (E4M8) Main"}, + 371758: {'name': 'Shattered Bridge (E4M8) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "Shattered Bridge (E4M8) Boss"}, + 371759: {'name': 'Mausoleum (E4M9) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 50, + 'doom_type': 80, + 'region': "Mausoleum (E4M9) Main"}, + 371760: {'name': 'Mausoleum (E4M9) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 59, + 'doom_type': 2005, + 'region': "Mausoleum (E4M9) Main"}, + 371761: {'name': 'Mausoleum (E4M9) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 60, + 'doom_type': 2001, + 'region': "Mausoleum (E4M9) Main"}, + 371762: {'name': 'Mausoleum (E4M9) - Dragon Claw', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 61, + 'doom_type': 53, + 'region': "Mausoleum (E4M9) Main"}, + 371763: {'name': 'Mausoleum (E4M9) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 62, + 'doom_type': 2004, + 'region': "Mausoleum (E4M9) Main"}, + 371764: {'name': 'Mausoleum (E4M9) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 63, + 'doom_type': 2003, + 'region': "Mausoleum (E4M9) Main"}, + 371765: {'name': 'Mausoleum (E4M9) - Firemace', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 64, + 'doom_type': 2002, + 'region': "Mausoleum (E4M9) Main"}, + 371766: {'name': 'Mausoleum (E4M9) - Firemace 2', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 65, + 'doom_type': 2002, + 'region': "Mausoleum (E4M9) Main"}, + 371767: {'name': 'Mausoleum (E4M9) - Firemace 3', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 66, + 'doom_type': 2002, + 'region': "Mausoleum (E4M9) Main"}, + 371768: {'name': 'Mausoleum (E4M9) - Firemace 4', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 67, + 'doom_type': 2002, + 'region': "Mausoleum (E4M9) Main"}, + 371769: {'name': 'Mausoleum (E4M9) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 68, + 'doom_type': 8, + 'region': "Mausoleum (E4M9) Main"}, + 371770: {'name': 'Mausoleum (E4M9) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 69, + 'doom_type': 8, + 'region': "Mausoleum (E4M9) Main"}, + 371771: {'name': 'Mausoleum (E4M9) - Bag of Holding 3', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 70, + 'doom_type': 8, + 'region': "Mausoleum (E4M9) Main"}, + 371772: {'name': 'Mausoleum (E4M9) - Bag of Holding 4', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 71, + 'doom_type': 8, + 'region': "Mausoleum (E4M9) Yellow"}, + 371773: {'name': 'Mausoleum (E4M9) - Morph Ovum', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 79, + 'doom_type': 30, + 'region': "Mausoleum (E4M9) Main"}, + 371774: {'name': 'Mausoleum (E4M9) - Mystic Urn', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 81, + 'doom_type': 32, + 'region': "Mausoleum (E4M9) Main"}, + 371775: {'name': 'Mausoleum (E4M9) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 82, + 'doom_type': 75, + 'region': "Mausoleum (E4M9) Main"}, + 371776: {'name': 'Mausoleum (E4M9) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 83, + 'doom_type': 84, + 'region': "Mausoleum (E4M9) Main"}, + 371777: {'name': 'Mausoleum (E4M9) - Silver Shield', + 'episode': 4, + 'check_sanity': True, + 'map': 9, + 'index': 84, + 'doom_type': 85, + 'region': "Mausoleum (E4M9) Main"}, + 371778: {'name': 'Mausoleum (E4M9) - Silver Shield 2', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 85, + 'doom_type': 85, + 'region': "Mausoleum (E4M9) Main"}, + 371779: {'name': 'Mausoleum (E4M9) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 86, + 'doom_type': 31, + 'region': "Mausoleum (E4M9) Yellow"}, + 371780: {'name': 'Mausoleum (E4M9) - Map Scroll', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 87, + 'doom_type': 35, + 'region': "Mausoleum (E4M9) Yellow"}, + 371781: {'name': 'Mausoleum (E4M9) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 88, + 'doom_type': 36, + 'region': "Mausoleum (E4M9) Main"}, + 371782: {'name': 'Mausoleum (E4M9) - Torch', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 89, + 'doom_type': 33, + 'region': "Mausoleum (E4M9) Main"}, + 371783: {'name': 'Mausoleum (E4M9) - Torch 2', + 'episode': 4, + 'check_sanity': True, + 'map': 9, + 'index': 90, + 'doom_type': 33, + 'region': "Mausoleum (E4M9) Main"}, + 371784: {'name': 'Mausoleum (E4M9) - Tome of Power', + 'episode': 4, + 'check_sanity': True, + 'map': 9, + 'index': 91, + 'doom_type': 86, + 'region': "Mausoleum (E4M9) Main"}, + 371785: {'name': 'Mausoleum (E4M9) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 93, + 'doom_type': 86, + 'region': "Mausoleum (E4M9) Main"}, + 371786: {'name': 'Mausoleum (E4M9) - Tome of Power 3', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 94, + 'doom_type': 86, + 'region': "Mausoleum (E4M9) Main"}, + 371787: {'name': 'Mausoleum (E4M9) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "Mausoleum (E4M9) Yellow"}, + 371788: {'name': 'Ochre Cliffs (E5M1) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 4, + 'doom_type': 80, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371789: {'name': 'Ochre Cliffs (E5M1) - Blue key', + 'episode': 5, + 'check_sanity': True, + 'map': 1, + 'index': 7, + 'doom_type': 79, + 'region': "Ochre Cliffs (E5M1) Green"}, + 371790: {'name': 'Ochre Cliffs (E5M1) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 9, + 'doom_type': 73, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371791: {'name': 'Ochre Cliffs (E5M1) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 92, + 'doom_type': 2005, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371792: {'name': 'Ochre Cliffs (E5M1) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 93, + 'doom_type': 2001, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371793: {'name': 'Ochre Cliffs (E5M1) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 94, + 'doom_type': 53, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371794: {'name': 'Ochre Cliffs (E5M1) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 95, + 'doom_type': 2004, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371795: {'name': 'Ochre Cliffs (E5M1) - Phoenix Rod', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 96, + 'doom_type': 2003, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371796: {'name': 'Ochre Cliffs (E5M1) - Firemace', + 'episode': 5, + 'check_sanity': True, + 'map': 1, + 'index': 97, + 'doom_type': 2002, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371797: {'name': 'Ochre Cliffs (E5M1) - Firemace 2', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 98, + 'doom_type': 2002, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371798: {'name': 'Ochre Cliffs (E5M1) - Firemace 3', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 99, + 'doom_type': 2002, + 'region': "Ochre Cliffs (E5M1) Green"}, + 371799: {'name': 'Ochre Cliffs (E5M1) - Firemace 4', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 100, + 'doom_type': 2002, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371800: {'name': 'Ochre Cliffs (E5M1) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 101, + 'doom_type': 8, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371801: {'name': 'Ochre Cliffs (E5M1) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 102, + 'doom_type': 30, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371802: {'name': 'Ochre Cliffs (E5M1) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 112, + 'doom_type': 32, + 'region': "Ochre Cliffs (E5M1) Green"}, + 371803: {'name': 'Ochre Cliffs (E5M1) - Shadowsphere', + 'episode': 5, + 'check_sanity': True, + 'map': 1, + 'index': 113, + 'doom_type': 75, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371804: {'name': 'Ochre Cliffs (E5M1) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 114, + 'doom_type': 84, + 'region': "Ochre Cliffs (E5M1) Blue"}, + 371805: {'name': 'Ochre Cliffs (E5M1) - Silver Shield', + 'episode': 5, + 'check_sanity': True, + 'map': 1, + 'index': 115, + 'doom_type': 85, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371806: {'name': 'Ochre Cliffs (E5M1) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 116, + 'doom_type': 31, + 'region': "Ochre Cliffs (E5M1) Blue"}, + 371807: {'name': 'Ochre Cliffs (E5M1) - Map Scroll', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 117, + 'doom_type': 35, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371808: {'name': 'Ochre Cliffs (E5M1) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 118, + 'doom_type': 36, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371809: {'name': 'Ochre Cliffs (E5M1) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 119, + 'doom_type': 33, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371810: {'name': 'Ochre Cliffs (E5M1) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 120, + 'doom_type': 86, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371811: {'name': 'Ochre Cliffs (E5M1) - Tome of Power 2', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 121, + 'doom_type': 86, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371812: {'name': 'Ochre Cliffs (E5M1) - Tome of Power 3', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 122, + 'doom_type': 86, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371813: {'name': 'Ochre Cliffs (E5M1) - Bag of Holding 2', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 129, + 'doom_type': 8, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371814: {'name': 'Ochre Cliffs (E5M1) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "Ochre Cliffs (E5M1) Blue"}, + 371815: {'name': 'Rapids (E5M2) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 2, + 'doom_type': 73, + 'region': "Rapids (E5M2) Yellow"}, + 371816: {'name': 'Rapids (E5M2) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 3, + 'doom_type': 80, + 'region': "Rapids (E5M2) Main"}, + 371817: {'name': 'Rapids (E5M2) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 34, + 'doom_type': 2001, + 'region': "Rapids (E5M2) Main"}, + 371818: {'name': 'Rapids (E5M2) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 35, + 'doom_type': 2005, + 'region': "Rapids (E5M2) Main"}, + 371819: {'name': 'Rapids (E5M2) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 36, + 'doom_type': 53, + 'region': "Rapids (E5M2) Yellow"}, + 371820: {'name': 'Rapids (E5M2) - Hellstaff', + 'episode': 5, + 'check_sanity': True, + 'map': 2, + 'index': 37, + 'doom_type': 2004, + 'region': "Rapids (E5M2) Yellow"}, + 371821: {'name': 'Rapids (E5M2) - Phoenix Rod', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 38, + 'doom_type': 2003, + 'region': "Rapids (E5M2) Green"}, + 371822: {'name': 'Rapids (E5M2) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 39, + 'doom_type': 8, + 'region': "Rapids (E5M2) Yellow"}, + 371823: {'name': 'Rapids (E5M2) - Bag of Holding 2', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 40, + 'doom_type': 8, + 'region': "Rapids (E5M2) Yellow"}, + 371824: {'name': 'Rapids (E5M2) - Bag of Holding 3', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 41, + 'doom_type': 8, + 'region': "Rapids (E5M2) Yellow"}, + 371825: {'name': 'Rapids (E5M2) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 42, + 'doom_type': 30, + 'region': "Rapids (E5M2) Yellow"}, + 371826: {'name': 'Rapids (E5M2) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 50, + 'doom_type': 32, + 'region': "Rapids (E5M2) Green"}, + 371827: {'name': 'Rapids (E5M2) - Shadowsphere', + 'episode': 5, + 'check_sanity': True, + 'map': 2, + 'index': 51, + 'doom_type': 75, + 'region': "Rapids (E5M2) Yellow"}, + 371828: {'name': 'Rapids (E5M2) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 52, + 'doom_type': 84, + 'region': "Rapids (E5M2) Green"}, + 371829: {'name': 'Rapids (E5M2) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 53, + 'doom_type': 85, + 'region': "Rapids (E5M2) Main"}, + 371830: {'name': 'Rapids (E5M2) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 54, + 'doom_type': 31, + 'region': "Rapids (E5M2) Yellow"}, + 371831: {'name': 'Rapids (E5M2) - Map Scroll', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 55, + 'doom_type': 35, + 'region': "Rapids (E5M2) Yellow"}, + 371832: {'name': 'Rapids (E5M2) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 56, + 'doom_type': 86, + 'region': "Rapids (E5M2) Yellow"}, + 371833: {'name': 'Rapids (E5M2) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 57, + 'doom_type': 36, + 'region': "Rapids (E5M2) Green"}, + 371834: {'name': 'Rapids (E5M2) - Tome of Power 2', + 'episode': 5, + 'check_sanity': True, + 'map': 2, + 'index': 58, + 'doom_type': 86, + 'region': "Rapids (E5M2) Yellow"}, + 371835: {'name': 'Rapids (E5M2) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 59, + 'doom_type': 33, + 'region': "Rapids (E5M2) Main"}, + 371836: {'name': 'Rapids (E5M2) - Enchanted Shield 2', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 66, + 'doom_type': 31, + 'region': "Rapids (E5M2) Main"}, + 371837: {'name': 'Rapids (E5M2) - Hellstaff 2', + 'episode': 5, + 'check_sanity': True, + 'map': 2, + 'index': 67, + 'doom_type': 2004, + 'region': "Rapids (E5M2) Main"}, + 371838: {'name': 'Rapids (E5M2) - Phoenix Rod 2', + 'episode': 5, + 'check_sanity': True, + 'map': 2, + 'index': 68, + 'doom_type': 2003, + 'region': "Rapids (E5M2) Main"}, + 371839: {'name': 'Rapids (E5M2) - Firemace', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 71, + 'doom_type': 2002, + 'region': "Rapids (E5M2) Main"}, + 371840: {'name': 'Rapids (E5M2) - Firemace 2', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 72, + 'doom_type': 2002, + 'region': "Rapids (E5M2) Green"}, + 371841: {'name': 'Rapids (E5M2) - Firemace 3', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 73, + 'doom_type': 2002, + 'region': "Rapids (E5M2) Green"}, + 371842: {'name': 'Rapids (E5M2) - Firemace 4', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 74, + 'doom_type': 2002, + 'region': "Rapids (E5M2) Yellow"}, + 371843: {'name': 'Rapids (E5M2) - Firemace 5', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 75, + 'doom_type': 2002, + 'region': "Rapids (E5M2) Green"}, + 371844: {'name': 'Rapids (E5M2) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "Rapids (E5M2) Green"}, + 371845: {'name': 'Quay (E5M3) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 12, + 'doom_type': 73, + 'region': "Quay (E5M3) Yellow"}, + 371846: {'name': 'Quay (E5M3) - Blue key', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 13, + 'doom_type': 79, + 'region': "Quay (E5M3) Green"}, + 371847: {'name': 'Quay (E5M3) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 15, + 'doom_type': 80, + 'region': "Quay (E5M3) Main"}, + 371848: {'name': 'Quay (E5M3) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 212, + 'doom_type': 2001, + 'region': "Quay (E5M3) Main"}, + 371849: {'name': 'Quay (E5M3) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 213, + 'doom_type': 2005, + 'region': "Quay (E5M3) Main"}, + 371850: {'name': 'Quay (E5M3) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 214, + 'doom_type': 53, + 'region': "Quay (E5M3) Yellow"}, + 371851: {'name': 'Quay (E5M3) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 215, + 'doom_type': 2004, + 'region': "Quay (E5M3) Green"}, + 371852: {'name': 'Quay (E5M3) - Phoenix Rod', + 'episode': 5, + 'check_sanity': True, + 'map': 3, + 'index': 216, + 'doom_type': 2003, + 'region': "Quay (E5M3) Blue"}, + 371853: {'name': 'Quay (E5M3) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 217, + 'doom_type': 8, + 'region': "Quay (E5M3) Main"}, + 371854: {'name': 'Quay (E5M3) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 218, + 'doom_type': 30, + 'region': "Quay (E5M3) Blue"}, + 371855: {'name': 'Quay (E5M3) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 229, + 'doom_type': 32, + 'region': "Quay (E5M3) Green"}, + 371856: {'name': 'Quay (E5M3) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 230, + 'doom_type': 31, + 'region': "Quay (E5M3) Green"}, + 371857: {'name': 'Quay (E5M3) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 231, + 'doom_type': 84, + 'region': "Quay (E5M3) Main"}, + 371858: {'name': 'Quay (E5M3) - Shadowsphere', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 232, + 'doom_type': 75, + 'region': "Quay (E5M3) Main"}, + 371859: {'name': 'Quay (E5M3) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 233, + 'doom_type': 85, + 'region': "Quay (E5M3) Main"}, + 371860: {'name': 'Quay (E5M3) - Silver Shield 2', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 234, + 'doom_type': 85, + 'region': "Quay (E5M3) Blue"}, + 371861: {'name': 'Quay (E5M3) - Map Scroll', + 'episode': 5, + 'check_sanity': True, + 'map': 3, + 'index': 235, + 'doom_type': 35, + 'region': "Quay (E5M3) Blue"}, + 371862: {'name': 'Quay (E5M3) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 236, + 'doom_type': 36, + 'region': "Quay (E5M3) Blue"}, + 371863: {'name': 'Quay (E5M3) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 237, + 'doom_type': 86, + 'region': "Quay (E5M3) Main"}, + 371864: {'name': 'Quay (E5M3) - Tome of Power 2', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 238, + 'doom_type': 86, + 'region': "Quay (E5M3) Green"}, + 371865: {'name': 'Quay (E5M3) - Tome of Power 3', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 239, + 'doom_type': 86, + 'region': "Quay (E5M3) Blue"}, + 371866: {'name': 'Quay (E5M3) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 240, + 'doom_type': 33, + 'region': "Quay (E5M3) Green"}, + 371867: {'name': 'Quay (E5M3) - Firemace', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 242, + 'doom_type': 2002, + 'region': "Quay (E5M3) Blue"}, + 371868: {'name': 'Quay (E5M3) - Firemace 2', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 243, + 'doom_type': 2002, + 'region': "Quay (E5M3) Main"}, + 371869: {'name': 'Quay (E5M3) - Firemace 3', + 'episode': 5, + 'check_sanity': True, + 'map': 3, + 'index': 244, + 'doom_type': 2002, + 'region': "Quay (E5M3) Yellow"}, + 371870: {'name': 'Quay (E5M3) - Firemace 4', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 245, + 'doom_type': 2002, + 'region': "Quay (E5M3) Yellow"}, + 371871: {'name': 'Quay (E5M3) - Firemace 5', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 246, + 'doom_type': 2002, + 'region': "Quay (E5M3) Green"}, + 371872: {'name': 'Quay (E5M3) - Firemace 6', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 247, + 'doom_type': 2002, + 'region': "Quay (E5M3) Blue"}, + 371873: {'name': 'Quay (E5M3) - Bag of Holding 2', + 'episode': 5, + 'check_sanity': True, + 'map': 3, + 'index': 252, + 'doom_type': 8, + 'region': "Quay (E5M3) Yellow"}, + 371874: {'name': 'Quay (E5M3) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "Quay (E5M3) Blue"}, + 371875: {'name': 'Courtyard (E5M4) - Blue key', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 3, + 'doom_type': 79, + 'region': "Courtyard (E5M4) Main"}, + 371876: {'name': 'Courtyard (E5M4) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 16, + 'doom_type': 80, + 'region': "Courtyard (E5M4) Main"}, + 371877: {'name': 'Courtyard (E5M4) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 21, + 'doom_type': 73, + 'region': "Courtyard (E5M4) Kakis"}, + 371878: {'name': 'Courtyard (E5M4) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 84, + 'doom_type': 2005, + 'region': "Courtyard (E5M4) Main"}, + 371879: {'name': 'Courtyard (E5M4) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 85, + 'doom_type': 2001, + 'region': "Courtyard (E5M4) Main"}, + 371880: {'name': 'Courtyard (E5M4) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 86, + 'doom_type': 53, + 'region': "Courtyard (E5M4) Main"}, + 371881: {'name': 'Courtyard (E5M4) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 87, + 'doom_type': 2004, + 'region': "Courtyard (E5M4) Kakis"}, + 371882: {'name': 'Courtyard (E5M4) - Phoenix Rod', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 88, + 'doom_type': 2003, + 'region': "Courtyard (E5M4) Main"}, + 371883: {'name': 'Courtyard (E5M4) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 89, + 'doom_type': 30, + 'region': "Courtyard (E5M4) Main"}, + 371884: {'name': 'Courtyard (E5M4) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 90, + 'doom_type': 8, + 'region': "Courtyard (E5M4) Main"}, + 371885: {'name': 'Courtyard (E5M4) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 91, + 'doom_type': 85, + 'region': "Courtyard (E5M4) Main"}, + 371886: {'name': 'Courtyard (E5M4) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 103, + 'doom_type': 32, + 'region': "Courtyard (E5M4) Main"}, + 371887: {'name': 'Courtyard (E5M4) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 104, + 'doom_type': 84, + 'region': "Courtyard (E5M4) Kakis"}, + 371888: {'name': 'Courtyard (E5M4) - Shadowsphere', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 105, + 'doom_type': 75, + 'region': "Courtyard (E5M4) Main"}, + 371889: {'name': 'Courtyard (E5M4) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 106, + 'doom_type': 31, + 'region': "Courtyard (E5M4) Blue"}, + 371890: {'name': 'Courtyard (E5M4) - Map Scroll', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 107, + 'doom_type': 35, + 'region': "Courtyard (E5M4) Kakis"}, + 371891: {'name': 'Courtyard (E5M4) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 108, + 'doom_type': 36, + 'region': "Courtyard (E5M4) Main"}, + 371892: {'name': 'Courtyard (E5M4) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 109, + 'doom_type': 86, + 'region': "Courtyard (E5M4) Main"}, + 371893: {'name': 'Courtyard (E5M4) - Tome of Power 2', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 110, + 'doom_type': 86, + 'region': "Courtyard (E5M4) Blue"}, + 371894: {'name': 'Courtyard (E5M4) - Tome of Power 3', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 111, + 'doom_type': 86, + 'region': "Courtyard (E5M4) Kakis"}, + 371895: {'name': 'Courtyard (E5M4) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 112, + 'doom_type': 33, + 'region': "Courtyard (E5M4) Main"}, + 371896: {'name': 'Courtyard (E5M4) - Bag of Holding 2', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 213, + 'doom_type': 8, + 'region': "Courtyard (E5M4) Blue"}, + 371897: {'name': 'Courtyard (E5M4) - Silver Shield 2', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 219, + 'doom_type': 85, + 'region': "Courtyard (E5M4) Kakis"}, + 371898: {'name': 'Courtyard (E5M4) - Bag of Holding 3', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 272, + 'doom_type': 8, + 'region': "Courtyard (E5M4) Main"}, + 371899: {'name': 'Courtyard (E5M4) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "Courtyard (E5M4) Blue"}, + 371900: {'name': 'Hydratyr (E5M5) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 3, + 'doom_type': 80, + 'region': "Hydratyr (E5M5) Main"}, + 371901: {'name': 'Hydratyr (E5M5) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 5, + 'doom_type': 73, + 'region': "Hydratyr (E5M5) Yellow"}, + 371902: {'name': 'Hydratyr (E5M5) - Blue key', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 11, + 'doom_type': 79, + 'region': "Hydratyr (E5M5) Green"}, + 371903: {'name': 'Hydratyr (E5M5) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 238, + 'doom_type': 2001, + 'region': "Hydratyr (E5M5) Main"}, + 371904: {'name': 'Hydratyr (E5M5) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 239, + 'doom_type': 2005, + 'region': "Hydratyr (E5M5) Yellow"}, + 371905: {'name': 'Hydratyr (E5M5) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 240, + 'doom_type': 2004, + 'region': "Hydratyr (E5M5) Yellow"}, + 371906: {'name': 'Hydratyr (E5M5) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 241, + 'doom_type': 53, + 'region': "Hydratyr (E5M5) Yellow"}, + 371907: {'name': 'Hydratyr (E5M5) - Phoenix Rod', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 242, + 'doom_type': 2003, + 'region': "Hydratyr (E5M5) Green"}, + 371908: {'name': 'Hydratyr (E5M5) - Firemace', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 243, + 'doom_type': 2002, + 'region': "Hydratyr (E5M5) Green"}, + 371909: {'name': 'Hydratyr (E5M5) - Firemace 2', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 244, + 'doom_type': 2002, + 'region': "Hydratyr (E5M5) Green"}, + 371910: {'name': 'Hydratyr (E5M5) - Firemace 3', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 245, + 'doom_type': 2002, + 'region': "Hydratyr (E5M5) Green"}, + 371911: {'name': 'Hydratyr (E5M5) - Firemace 4', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 246, + 'doom_type': 2002, + 'region': "Hydratyr (E5M5) Green"}, + 371912: {'name': 'Hydratyr (E5M5) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 248, + 'doom_type': 8, + 'region': "Hydratyr (E5M5) Main"}, + 371913: {'name': 'Hydratyr (E5M5) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 259, + 'doom_type': 30, + 'region': "Hydratyr (E5M5) Green"}, + 371914: {'name': 'Hydratyr (E5M5) - Bag of Holding 2', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 260, + 'doom_type': 8, + 'region': "Hydratyr (E5M5) Green"}, + 371915: {'name': 'Hydratyr (E5M5) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 261, + 'doom_type': 32, + 'region': "Hydratyr (E5M5) Blue"}, + 371916: {'name': 'Hydratyr (E5M5) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 262, + 'doom_type': 86, + 'region': "Hydratyr (E5M5) Main"}, + 371917: {'name': 'Hydratyr (E5M5) - Shadowsphere', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 263, + 'doom_type': 75, + 'region': "Hydratyr (E5M5) Main"}, + 371918: {'name': 'Hydratyr (E5M5) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 264, + 'doom_type': 84, + 'region': "Hydratyr (E5M5) Yellow"}, + 371919: {'name': 'Hydratyr (E5M5) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 265, + 'doom_type': 36, + 'region': "Hydratyr (E5M5) Yellow"}, + 371920: {'name': 'Hydratyr (E5M5) - Map Scroll', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 266, + 'doom_type': 35, + 'region': "Hydratyr (E5M5) Yellow"}, + 371921: {'name': 'Hydratyr (E5M5) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 267, + 'doom_type': 31, + 'region': "Hydratyr (E5M5) Green"}, + 371922: {'name': 'Hydratyr (E5M5) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 268, + 'doom_type': 33, + 'region': "Hydratyr (E5M5) Main"}, + 371923: {'name': 'Hydratyr (E5M5) - Tome of Power 2', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 269, + 'doom_type': 86, + 'region': "Hydratyr (E5M5) Blue"}, + 371924: {'name': 'Hydratyr (E5M5) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 270, + 'doom_type': 85, + 'region': "Hydratyr (E5M5) Blue"}, + 371925: {'name': 'Hydratyr (E5M5) - Silver Shield 2', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 271, + 'doom_type': 85, + 'region': "Hydratyr (E5M5) Main"}, + 371926: {'name': 'Hydratyr (E5M5) - Tome of Power 3', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 272, + 'doom_type': 86, + 'region': "Hydratyr (E5M5) Yellow"}, + 371927: {'name': 'Hydratyr (E5M5) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "Hydratyr (E5M5) Blue"}, + 371928: {'name': 'Colonnade (E5M6) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 8, + 'doom_type': 80, + 'region': "Colonnade (E5M6) Main"}, + 371929: {'name': 'Colonnade (E5M6) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 9, + 'doom_type': 73, + 'region': "Colonnade (E5M6) Yellow"}, + 371930: {'name': 'Colonnade (E5M6) - Blue key', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 10, + 'doom_type': 79, + 'region': "Colonnade (E5M6) Green"}, + 371931: {'name': 'Colonnade (E5M6) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 91, + 'doom_type': 53, + 'region': "Colonnade (E5M6) Main"}, + 371932: {'name': 'Colonnade (E5M6) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 92, + 'doom_type': 2004, + 'region': "Colonnade (E5M6) Yellow"}, + 371933: {'name': 'Colonnade (E5M6) - Phoenix Rod', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 93, + 'doom_type': 2003, + 'region': "Colonnade (E5M6) Green"}, + 371934: {'name': 'Colonnade (E5M6) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 94, + 'doom_type': 2005, + 'region': "Colonnade (E5M6) Yellow"}, + 371935: {'name': 'Colonnade (E5M6) - Firemace', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 95, + 'doom_type': 2002, + 'region': "Colonnade (E5M6) Yellow"}, + 371936: {'name': 'Colonnade (E5M6) - Firemace 2', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 96, + 'doom_type': 2002, + 'region': "Colonnade (E5M6) Yellow"}, + 371937: {'name': 'Colonnade (E5M6) - Firemace 3', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 97, + 'doom_type': 2002, + 'region': "Colonnade (E5M6) Yellow"}, + 371938: {'name': 'Colonnade (E5M6) - Firemace 4', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 98, + 'doom_type': 2002, + 'region': "Colonnade (E5M6) Main"}, + 371939: {'name': 'Colonnade (E5M6) - Firemace 5', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 99, + 'doom_type': 2002, + 'region': "Colonnade (E5M6) Main"}, + 371940: {'name': 'Colonnade (E5M6) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 100, + 'doom_type': 31, + 'region': "Colonnade (E5M6) Yellow"}, + 371941: {'name': 'Colonnade (E5M6) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 101, + 'doom_type': 86, + 'region': "Colonnade (E5M6) Yellow"}, + 371942: {'name': 'Colonnade (E5M6) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 102, + 'doom_type': 85, + 'region': "Colonnade (E5M6) Main"}, + 371943: {'name': 'Colonnade (E5M6) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 103, + 'doom_type': 30, + 'region': "Colonnade (E5M6) Main"}, + 371944: {'name': 'Colonnade (E5M6) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 104, + 'doom_type': 36, + 'region': "Colonnade (E5M6) Main"}, + 371945: {'name': 'Colonnade (E5M6) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 105, + 'doom_type': 8, + 'region': "Colonnade (E5M6) Main"}, + 371946: {'name': 'Colonnade (E5M6) - Bag of Holding 2', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 106, + 'doom_type': 8, + 'region': "Colonnade (E5M6) Green"}, + 371947: {'name': 'Colonnade (E5M6) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 121, + 'doom_type': 32, + 'region': "Colonnade (E5M6) Yellow"}, + 371948: {'name': 'Colonnade (E5M6) - Shadowsphere', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 122, + 'doom_type': 75, + 'region': "Colonnade (E5M6) Yellow"}, + 371949: {'name': 'Colonnade (E5M6) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 123, + 'doom_type': 84, + 'region': "Colonnade (E5M6) Main"}, + 371950: {'name': 'Colonnade (E5M6) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 124, + 'doom_type': 33, + 'region': "Colonnade (E5M6) Yellow"}, + 371951: {'name': 'Colonnade (E5M6) - Map Scroll', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 125, + 'doom_type': 35, + 'region': "Colonnade (E5M6) Yellow"}, + 371952: {'name': 'Colonnade (E5M6) - Tome of Power 2', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 126, + 'doom_type': 86, + 'region': "Colonnade (E5M6) Yellow"}, + 371953: {'name': 'Colonnade (E5M6) - Mystic Urn 2', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 127, + 'doom_type': 32, + 'region': "Colonnade (E5M6) Blue"}, + 371954: {'name': 'Colonnade (E5M6) - Ring of Invincibility 2', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 128, + 'doom_type': 84, + 'region': "Colonnade (E5M6) Blue"}, + 371955: {'name': 'Colonnade (E5M6) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 348, + 'doom_type': 2001, + 'region': "Colonnade (E5M6) Main"}, + 371956: {'name': 'Colonnade (E5M6) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "Colonnade (E5M6) Blue"}, + 371957: {'name': 'Foetid Manse (E5M7) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 7, + 'doom_type': 31, + 'region': "Foetid Manse (E5M7) Blue"}, + 371958: {'name': 'Foetid Manse (E5M7) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 8, + 'doom_type': 32, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371959: {'name': 'Foetid Manse (E5M7) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 9, + 'doom_type': 30, + 'region': "Foetid Manse (E5M7) Green"}, + 371960: {'name': 'Foetid Manse (E5M7) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 12, + 'doom_type': 73, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371961: {'name': 'Foetid Manse (E5M7) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 15, + 'doom_type': 80, + 'region': "Foetid Manse (E5M7) Main"}, + 371962: {'name': 'Foetid Manse (E5M7) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 218, + 'doom_type': 2001, + 'region': "Foetid Manse (E5M7) Main"}, + 371963: {'name': 'Foetid Manse (E5M7) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 219, + 'doom_type': 2005, + 'region': "Foetid Manse (E5M7) Main"}, + 371964: {'name': 'Foetid Manse (E5M7) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 220, + 'doom_type': 53, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371965: {'name': 'Foetid Manse (E5M7) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 221, + 'doom_type': 2004, + 'region': "Foetid Manse (E5M7) Green"}, + 371966: {'name': 'Foetid Manse (E5M7) - Phoenix Rod', + 'episode': 5, + 'check_sanity': True, + 'map': 7, + 'index': 222, + 'doom_type': 2003, + 'region': "Foetid Manse (E5M7) Green"}, + 371967: {'name': 'Foetid Manse (E5M7) - Shadowsphere', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 223, + 'doom_type': 75, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371968: {'name': 'Foetid Manse (E5M7) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 224, + 'doom_type': 84, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371969: {'name': 'Foetid Manse (E5M7) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 225, + 'doom_type': 85, + 'region': "Foetid Manse (E5M7) Green"}, + 371970: {'name': 'Foetid Manse (E5M7) - Map Scroll', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 234, + 'doom_type': 35, + 'region': "Foetid Manse (E5M7) Green"}, + 371971: {'name': 'Foetid Manse (E5M7) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 235, + 'doom_type': 86, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371972: {'name': 'Foetid Manse (E5M7) - Tome of Power 2', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 236, + 'doom_type': 86, + 'region': "Foetid Manse (E5M7) Green"}, + 371973: {'name': 'Foetid Manse (E5M7) - Tome of Power 3', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 237, + 'doom_type': 86, + 'region': "Foetid Manse (E5M7) Green"}, + 371974: {'name': 'Foetid Manse (E5M7) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 238, + 'doom_type': 33, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371975: {'name': 'Foetid Manse (E5M7) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 239, + 'doom_type': 36, + 'region': "Foetid Manse (E5M7) Green"}, + 371976: {'name': 'Foetid Manse (E5M7) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 240, + 'doom_type': 8, + 'region': "Foetid Manse (E5M7) Green"}, + 371977: {'name': 'Foetid Manse (E5M7) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "Foetid Manse (E5M7) Blue"}, + 371978: {'name': 'Field of Judgement (E5M8) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 18, + 'doom_type': 2004, + 'region': "Field of Judgement (E5M8) Main"}, + 371979: {'name': 'Field of Judgement (E5M8) - Phoenix Rod', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 19, + 'doom_type': 2003, + 'region': "Field of Judgement (E5M8) Main"}, + 371980: {'name': 'Field of Judgement (E5M8) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 20, + 'doom_type': 2001, + 'region': "Field of Judgement (E5M8) Main"}, + 371981: {'name': 'Field of Judgement (E5M8) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 21, + 'doom_type': 53, + 'region': "Field of Judgement (E5M8) Main"}, + 371982: {'name': 'Field of Judgement (E5M8) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 22, + 'doom_type': 2005, + 'region': "Field of Judgement (E5M8) Main"}, + 371983: {'name': 'Field of Judgement (E5M8) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 23, + 'doom_type': 32, + 'region': "Field of Judgement (E5M8) Main"}, + 371984: {'name': 'Field of Judgement (E5M8) - Shadowsphere', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 24, + 'doom_type': 75, + 'region': "Field of Judgement (E5M8) Main"}, + 371985: {'name': 'Field of Judgement (E5M8) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 25, + 'doom_type': 31, + 'region': "Field of Judgement (E5M8) Main"}, + 371986: {'name': 'Field of Judgement (E5M8) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 26, + 'doom_type': 84, + 'region': "Field of Judgement (E5M8) Main"}, + 371987: {'name': 'Field of Judgement (E5M8) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 27, + 'doom_type': 86, + 'region': "Field of Judgement (E5M8) Main"}, + 371988: {'name': 'Field of Judgement (E5M8) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 28, + 'doom_type': 36, + 'region': "Field of Judgement (E5M8) Main"}, + 371989: {'name': 'Field of Judgement (E5M8) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 29, + 'doom_type': 85, + 'region': "Field of Judgement (E5M8) Main"}, + 371990: {'name': 'Field of Judgement (E5M8) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 62, + 'doom_type': 8, + 'region': "Field of Judgement (E5M8) Main"}, + 371991: {'name': 'Field of Judgement (E5M8) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "Field of Judgement (E5M8) Main"}, + 371992: {'name': "Skein of D'Sparil (E5M9) - Blue key", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 0, + 'doom_type': 79, + 'region': "Skein of D'Sparil (E5M9) Green"}, + 371993: {'name': "Skein of D'Sparil (E5M9) - Green key", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 1, + 'doom_type': 73, + 'region': "Skein of D'Sparil (E5M9) Yellow"}, + 371994: {'name': "Skein of D'Sparil (E5M9) - Yellow key", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 13, + 'doom_type': 80, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 371995: {'name': "Skein of D'Sparil (E5M9) - Ethereal Crossbow", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 21, + 'doom_type': 2001, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 371996: {'name': "Skein of D'Sparil (E5M9) - Dragon Claw", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 44, + 'doom_type': 53, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 371997: {'name': "Skein of D'Sparil (E5M9) - Gauntlets of the Necromancer", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 45, + 'doom_type': 2005, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 371998: {'name': "Skein of D'Sparil (E5M9) - Hellstaff", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 46, + 'doom_type': 2004, + 'region': "Skein of D'Sparil (E5M9) Yellow"}, + 371999: {'name': "Skein of D'Sparil (E5M9) - Phoenix Rod", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 47, + 'doom_type': 2003, + 'region': "Skein of D'Sparil (E5M9) Blue"}, + 372000: {'name': "Skein of D'Sparil (E5M9) - Bag of Holding", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 48, + 'doom_type': 8, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 372001: {'name': "Skein of D'Sparil (E5M9) - Silver Shield", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 51, + 'doom_type': 85, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 372002: {'name': "Skein of D'Sparil (E5M9) - Tome of Power", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 52, + 'doom_type': 86, + 'region': "Skein of D'Sparil (E5M9) Green"}, + 372003: {'name': "Skein of D'Sparil (E5M9) - Torch", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 53, + 'doom_type': 33, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 372004: {'name': "Skein of D'Sparil (E5M9) - Morph Ovum", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 54, + 'doom_type': 30, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 372005: {'name': "Skein of D'Sparil (E5M9) - Shadowsphere", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 64, + 'doom_type': 75, + 'region': "Skein of D'Sparil (E5M9) Yellow"}, + 372006: {'name': "Skein of D'Sparil (E5M9) - Chaos Device", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 65, + 'doom_type': 36, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 372007: {'name': "Skein of D'Sparil (E5M9) - Ring of Invincibility", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 66, + 'doom_type': 84, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 372008: {'name': "Skein of D'Sparil (E5M9) - Enchanted Shield", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 67, + 'doom_type': 31, + 'region': "Skein of D'Sparil (E5M9) Blue"}, + 372009: {'name': "Skein of D'Sparil (E5M9) - Mystic Urn", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 68, + 'doom_type': 32, + 'region': "Skein of D'Sparil (E5M9) Blue"}, + 372010: {'name': "Skein of D'Sparil (E5M9) - Tome of Power 2", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 69, + 'doom_type': 86, + 'region': "Skein of D'Sparil (E5M9) Green"}, + 372011: {'name': "Skein of D'Sparil (E5M9) - Map Scroll", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 70, + 'doom_type': 35, + 'region': "Skein of D'Sparil (E5M9) Green"}, + 372012: {'name': "Skein of D'Sparil (E5M9) - Bag of Holding 2", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 243, + 'doom_type': 8, + 'region': "Skein of D'Sparil (E5M9) Blue"}, + 372013: {'name': "Skein of D'Sparil (E5M9) - Exit", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "Skein of D'Sparil (E5M9) Blue"}, +} + + +location_name_groups: Dict[str, Set[str]] = { + 'Ambulatory (E4M3)': { + 'Ambulatory (E4M3) - Bag of Holding', + 'Ambulatory (E4M3) - Bag of Holding 2', + 'Ambulatory (E4M3) - Blue key', + 'Ambulatory (E4M3) - Chaos Device', + 'Ambulatory (E4M3) - Dragon Claw', + 'Ambulatory (E4M3) - Enchanted Shield', + 'Ambulatory (E4M3) - Ethereal Crossbow', + 'Ambulatory (E4M3) - Exit', + 'Ambulatory (E4M3) - Firemace', + 'Ambulatory (E4M3) - Firemace 2', + 'Ambulatory (E4M3) - Firemace 3', + 'Ambulatory (E4M3) - Firemace 4', + 'Ambulatory (E4M3) - Firemace 5', + 'Ambulatory (E4M3) - Gauntlets of the Necromancer', + 'Ambulatory (E4M3) - Green key', + 'Ambulatory (E4M3) - Hellstaff', + 'Ambulatory (E4M3) - Map Scroll', + 'Ambulatory (E4M3) - Morph Ovum', + 'Ambulatory (E4M3) - Morph Ovum 2', + 'Ambulatory (E4M3) - Mystic Urn', + 'Ambulatory (E4M3) - Phoenix Rod', + 'Ambulatory (E4M3) - Ring of Invincibility', + 'Ambulatory (E4M3) - Ring of Invincibility 2', + 'Ambulatory (E4M3) - Shadowsphere', + 'Ambulatory (E4M3) - Silver Shield', + 'Ambulatory (E4M3) - Tome of Power', + 'Ambulatory (E4M3) - Tome of Power 2', + 'Ambulatory (E4M3) - Torch', + 'Ambulatory (E4M3) - Yellow key', + }, + 'Blockhouse (E4M2)': { + 'Blockhouse (E4M2) - Bag of Holding', + 'Blockhouse (E4M2) - Bag of Holding 2', + 'Blockhouse (E4M2) - Blue key', + 'Blockhouse (E4M2) - Chaos Device', + 'Blockhouse (E4M2) - Dragon Claw', + 'Blockhouse (E4M2) - Enchanted Shield', + 'Blockhouse (E4M2) - Ethereal Crossbow', + 'Blockhouse (E4M2) - Exit', + 'Blockhouse (E4M2) - Gauntlets of the Necromancer', + 'Blockhouse (E4M2) - Green key', + 'Blockhouse (E4M2) - Hellstaff', + 'Blockhouse (E4M2) - Morph Ovum', + 'Blockhouse (E4M2) - Mystic Urn', + 'Blockhouse (E4M2) - Phoenix Rod', + 'Blockhouse (E4M2) - Ring of Invincibility', + 'Blockhouse (E4M2) - Ring of Invincibility 2', + 'Blockhouse (E4M2) - Shadowsphere', + 'Blockhouse (E4M2) - Shadowsphere 2', + 'Blockhouse (E4M2) - Silver Shield', + 'Blockhouse (E4M2) - Tome of Power', + 'Blockhouse (E4M2) - Yellow key', + }, + 'Catafalque (E4M1)': { + 'Catafalque (E4M1) - Bag of Holding', + 'Catafalque (E4M1) - Chaos Device', + 'Catafalque (E4M1) - Dragon Claw', + 'Catafalque (E4M1) - Ethereal Crossbow', + 'Catafalque (E4M1) - Exit', + 'Catafalque (E4M1) - Gauntlets of the Necromancer', + 'Catafalque (E4M1) - Green key', + 'Catafalque (E4M1) - Hellstaff', + 'Catafalque (E4M1) - Map Scroll', + 'Catafalque (E4M1) - Morph Ovum', + 'Catafalque (E4M1) - Ring of Invincibility', + 'Catafalque (E4M1) - Shadowsphere', + 'Catafalque (E4M1) - Silver Shield', + 'Catafalque (E4M1) - Tome of Power', + 'Catafalque (E4M1) - Tome of Power 2', + 'Catafalque (E4M1) - Torch', + 'Catafalque (E4M1) - Yellow key', + }, + 'Colonnade (E5M6)': { + 'Colonnade (E5M6) - Bag of Holding', + 'Colonnade (E5M6) - Bag of Holding 2', + 'Colonnade (E5M6) - Blue key', + 'Colonnade (E5M6) - Chaos Device', + 'Colonnade (E5M6) - Dragon Claw', + 'Colonnade (E5M6) - Enchanted Shield', + 'Colonnade (E5M6) - Ethereal Crossbow', + 'Colonnade (E5M6) - Exit', + 'Colonnade (E5M6) - Firemace', + 'Colonnade (E5M6) - Firemace 2', + 'Colonnade (E5M6) - Firemace 3', + 'Colonnade (E5M6) - Firemace 4', + 'Colonnade (E5M6) - Firemace 5', + 'Colonnade (E5M6) - Gauntlets of the Necromancer', + 'Colonnade (E5M6) - Green key', + 'Colonnade (E5M6) - Hellstaff', + 'Colonnade (E5M6) - Map Scroll', + 'Colonnade (E5M6) - Morph Ovum', + 'Colonnade (E5M6) - Mystic Urn', + 'Colonnade (E5M6) - Mystic Urn 2', + 'Colonnade (E5M6) - Phoenix Rod', + 'Colonnade (E5M6) - Ring of Invincibility', + 'Colonnade (E5M6) - Ring of Invincibility 2', + 'Colonnade (E5M6) - Shadowsphere', + 'Colonnade (E5M6) - Silver Shield', + 'Colonnade (E5M6) - Tome of Power', + 'Colonnade (E5M6) - Tome of Power 2', + 'Colonnade (E5M6) - Torch', + 'Colonnade (E5M6) - Yellow key', + }, + 'Courtyard (E5M4)': { + 'Courtyard (E5M4) - Bag of Holding', + 'Courtyard (E5M4) - Bag of Holding 2', + 'Courtyard (E5M4) - Bag of Holding 3', + 'Courtyard (E5M4) - Blue key', + 'Courtyard (E5M4) - Chaos Device', + 'Courtyard (E5M4) - Dragon Claw', + 'Courtyard (E5M4) - Enchanted Shield', + 'Courtyard (E5M4) - Ethereal Crossbow', + 'Courtyard (E5M4) - Exit', + 'Courtyard (E5M4) - Gauntlets of the Necromancer', + 'Courtyard (E5M4) - Green key', + 'Courtyard (E5M4) - Hellstaff', + 'Courtyard (E5M4) - Map Scroll', + 'Courtyard (E5M4) - Morph Ovum', + 'Courtyard (E5M4) - Mystic Urn', + 'Courtyard (E5M4) - Phoenix Rod', + 'Courtyard (E5M4) - Ring of Invincibility', + 'Courtyard (E5M4) - Shadowsphere', + 'Courtyard (E5M4) - Silver Shield', + 'Courtyard (E5M4) - Silver Shield 2', + 'Courtyard (E5M4) - Tome of Power', + 'Courtyard (E5M4) - Tome of Power 2', + 'Courtyard (E5M4) - Tome of Power 3', + 'Courtyard (E5M4) - Torch', + 'Courtyard (E5M4) - Yellow key', + }, + "D'Sparil'S Keep (E3M8)": { + "D'Sparil'S Keep (E3M8) - Bag of Holding", + "D'Sparil'S Keep (E3M8) - Chaos Device", + "D'Sparil'S Keep (E3M8) - Dragon Claw", + "D'Sparil'S Keep (E3M8) - Enchanted Shield", + "D'Sparil'S Keep (E3M8) - Ethereal Crossbow", + "D'Sparil'S Keep (E3M8) - Exit", + "D'Sparil'S Keep (E3M8) - Gauntlets of the Necromancer", + "D'Sparil'S Keep (E3M8) - Hellstaff", + "D'Sparil'S Keep (E3M8) - Mystic Urn", + "D'Sparil'S Keep (E3M8) - Phoenix Rod", + "D'Sparil'S Keep (E3M8) - Ring of Invincibility", + "D'Sparil'S Keep (E3M8) - Shadowsphere", + "D'Sparil'S Keep (E3M8) - Silver Shield", + "D'Sparil'S Keep (E3M8) - Tome of Power", + "D'Sparil'S Keep (E3M8) - Tome of Power 2", + "D'Sparil'S Keep (E3M8) - Tome of Power 3", + }, + 'Field of Judgement (E5M8)': { + 'Field of Judgement (E5M8) - Bag of Holding', + 'Field of Judgement (E5M8) - Chaos Device', + 'Field of Judgement (E5M8) - Dragon Claw', + 'Field of Judgement (E5M8) - Enchanted Shield', + 'Field of Judgement (E5M8) - Ethereal Crossbow', + 'Field of Judgement (E5M8) - Exit', + 'Field of Judgement (E5M8) - Gauntlets of the Necromancer', + 'Field of Judgement (E5M8) - Hellstaff', + 'Field of Judgement (E5M8) - Mystic Urn', + 'Field of Judgement (E5M8) - Phoenix Rod', + 'Field of Judgement (E5M8) - Ring of Invincibility', + 'Field of Judgement (E5M8) - Shadowsphere', + 'Field of Judgement (E5M8) - Silver Shield', + 'Field of Judgement (E5M8) - Tome of Power', + }, + 'Foetid Manse (E5M7)': { + 'Foetid Manse (E5M7) - Bag of Holding', + 'Foetid Manse (E5M7) - Chaos Device', + 'Foetid Manse (E5M7) - Dragon Claw', + 'Foetid Manse (E5M7) - Enchanted Shield', + 'Foetid Manse (E5M7) - Ethereal Crossbow', + 'Foetid Manse (E5M7) - Exit', + 'Foetid Manse (E5M7) - Gauntlets of the Necromancer', + 'Foetid Manse (E5M7) - Green key', + 'Foetid Manse (E5M7) - Hellstaff', + 'Foetid Manse (E5M7) - Map Scroll', + 'Foetid Manse (E5M7) - Morph Ovum', + 'Foetid Manse (E5M7) - Mystic Urn', + 'Foetid Manse (E5M7) - Phoenix Rod', + 'Foetid Manse (E5M7) - Ring of Invincibility', + 'Foetid Manse (E5M7) - Shadowsphere', + 'Foetid Manse (E5M7) - Silver Shield', + 'Foetid Manse (E5M7) - Tome of Power', + 'Foetid Manse (E5M7) - Tome of Power 2', + 'Foetid Manse (E5M7) - Tome of Power 3', + 'Foetid Manse (E5M7) - Torch', + 'Foetid Manse (E5M7) - Yellow key', + }, + 'Great Stair (E4M5)': { + 'Great Stair (E4M5) - Bag of Holding', + 'Great Stair (E4M5) - Bag of Holding 2', + 'Great Stair (E4M5) - Blue key', + 'Great Stair (E4M5) - Chaos Device', + 'Great Stair (E4M5) - Dragon Claw', + 'Great Stair (E4M5) - Enchanted Shield', + 'Great Stair (E4M5) - Ethereal Crossbow', + 'Great Stair (E4M5) - Exit', + 'Great Stair (E4M5) - Firemace', + 'Great Stair (E4M5) - Firemace 2', + 'Great Stair (E4M5) - Firemace 3', + 'Great Stair (E4M5) - Firemace 4', + 'Great Stair (E4M5) - Firemace 5', + 'Great Stair (E4M5) - Gauntlets of the Necromancer', + 'Great Stair (E4M5) - Green key', + 'Great Stair (E4M5) - Hellstaff', + 'Great Stair (E4M5) - Map Scroll', + 'Great Stair (E4M5) - Morph Ovum', + 'Great Stair (E4M5) - Mystic Urn', + 'Great Stair (E4M5) - Mystic Urn 2', + 'Great Stair (E4M5) - Phoenix Rod', + 'Great Stair (E4M5) - Ring of Invincibility', + 'Great Stair (E4M5) - Shadowsphere', + 'Great Stair (E4M5) - Silver Shield', + 'Great Stair (E4M5) - Tome of Power', + 'Great Stair (E4M5) - Tome of Power 2', + 'Great Stair (E4M5) - Tome of Power 3', + 'Great Stair (E4M5) - Torch', + 'Great Stair (E4M5) - Yellow key', + }, + 'Halls of the Apostate (E4M6)': { + 'Halls of the Apostate (E4M6) - Bag of Holding', + 'Halls of the Apostate (E4M6) - Bag of Holding 2', + 'Halls of the Apostate (E4M6) - Blue key', + 'Halls of the Apostate (E4M6) - Chaos Device', + 'Halls of the Apostate (E4M6) - Dragon Claw', + 'Halls of the Apostate (E4M6) - Enchanted Shield', + 'Halls of the Apostate (E4M6) - Ethereal Crossbow', + 'Halls of the Apostate (E4M6) - Exit', + 'Halls of the Apostate (E4M6) - Gauntlets of the Necromancer', + 'Halls of the Apostate (E4M6) - Green key', + 'Halls of the Apostate (E4M6) - Hellstaff', + 'Halls of the Apostate (E4M6) - Map Scroll', + 'Halls of the Apostate (E4M6) - Morph Ovum', + 'Halls of the Apostate (E4M6) - Mystic Urn', + 'Halls of the Apostate (E4M6) - Phoenix Rod', + 'Halls of the Apostate (E4M6) - Ring of Invincibility', + 'Halls of the Apostate (E4M6) - Shadowsphere', + 'Halls of the Apostate (E4M6) - Silver Shield', + 'Halls of the Apostate (E4M6) - Silver Shield 2', + 'Halls of the Apostate (E4M6) - Tome of Power', + 'Halls of the Apostate (E4M6) - Tome of Power 2', + 'Halls of the Apostate (E4M6) - Yellow key', + }, + "Hell's Maw (E1M8)": { + "Hell's Maw (E1M8) - Bag of Holding", + "Hell's Maw (E1M8) - Bag of Holding 2", + "Hell's Maw (E1M8) - Dragon Claw", + "Hell's Maw (E1M8) - Ethereal Crossbow", + "Hell's Maw (E1M8) - Exit", + "Hell's Maw (E1M8) - Gauntlets of the Necromancer", + "Hell's Maw (E1M8) - Morph Ovum", + "Hell's Maw (E1M8) - Ring of Invincibility", + "Hell's Maw (E1M8) - Ring of Invincibility 2", + "Hell's Maw (E1M8) - Ring of Invincibility 3", + "Hell's Maw (E1M8) - Shadowsphere", + "Hell's Maw (E1M8) - Silver Shield", + "Hell's Maw (E1M8) - Tome of Power", + "Hell's Maw (E1M8) - Tome of Power 2", + }, + 'Hydratyr (E5M5)': { + 'Hydratyr (E5M5) - Bag of Holding', + 'Hydratyr (E5M5) - Bag of Holding 2', + 'Hydratyr (E5M5) - Blue key', + 'Hydratyr (E5M5) - Chaos Device', + 'Hydratyr (E5M5) - Dragon Claw', + 'Hydratyr (E5M5) - Enchanted Shield', + 'Hydratyr (E5M5) - Ethereal Crossbow', + 'Hydratyr (E5M5) - Exit', + 'Hydratyr (E5M5) - Firemace', + 'Hydratyr (E5M5) - Firemace 2', + 'Hydratyr (E5M5) - Firemace 3', + 'Hydratyr (E5M5) - Firemace 4', + 'Hydratyr (E5M5) - Gauntlets of the Necromancer', + 'Hydratyr (E5M5) - Green key', + 'Hydratyr (E5M5) - Hellstaff', + 'Hydratyr (E5M5) - Map Scroll', + 'Hydratyr (E5M5) - Morph Ovum', + 'Hydratyr (E5M5) - Mystic Urn', + 'Hydratyr (E5M5) - Phoenix Rod', + 'Hydratyr (E5M5) - Ring of Invincibility', + 'Hydratyr (E5M5) - Shadowsphere', + 'Hydratyr (E5M5) - Silver Shield', + 'Hydratyr (E5M5) - Silver Shield 2', + 'Hydratyr (E5M5) - Tome of Power', + 'Hydratyr (E5M5) - Tome of Power 2', + 'Hydratyr (E5M5) - Tome of Power 3', + 'Hydratyr (E5M5) - Torch', + 'Hydratyr (E5M5) - Yellow key', + }, + 'Mausoleum (E4M9)': { + 'Mausoleum (E4M9) - Bag of Holding', + 'Mausoleum (E4M9) - Bag of Holding 2', + 'Mausoleum (E4M9) - Bag of Holding 3', + 'Mausoleum (E4M9) - Bag of Holding 4', + 'Mausoleum (E4M9) - Chaos Device', + 'Mausoleum (E4M9) - Dragon Claw', + 'Mausoleum (E4M9) - Enchanted Shield', + 'Mausoleum (E4M9) - Ethereal Crossbow', + 'Mausoleum (E4M9) - Exit', + 'Mausoleum (E4M9) - Firemace', + 'Mausoleum (E4M9) - Firemace 2', + 'Mausoleum (E4M9) - Firemace 3', + 'Mausoleum (E4M9) - Firemace 4', + 'Mausoleum (E4M9) - Gauntlets of the Necromancer', + 'Mausoleum (E4M9) - Hellstaff', + 'Mausoleum (E4M9) - Map Scroll', + 'Mausoleum (E4M9) - Morph Ovum', + 'Mausoleum (E4M9) - Mystic Urn', + 'Mausoleum (E4M9) - Phoenix Rod', + 'Mausoleum (E4M9) - Ring of Invincibility', + 'Mausoleum (E4M9) - Shadowsphere', + 'Mausoleum (E4M9) - Silver Shield', + 'Mausoleum (E4M9) - Silver Shield 2', + 'Mausoleum (E4M9) - Tome of Power', + 'Mausoleum (E4M9) - Tome of Power 2', + 'Mausoleum (E4M9) - Tome of Power 3', + 'Mausoleum (E4M9) - Torch', + 'Mausoleum (E4M9) - Torch 2', + 'Mausoleum (E4M9) - Yellow key', + }, + 'Ochre Cliffs (E5M1)': { + 'Ochre Cliffs (E5M1) - Bag of Holding', + 'Ochre Cliffs (E5M1) - Bag of Holding 2', + 'Ochre Cliffs (E5M1) - Blue key', + 'Ochre Cliffs (E5M1) - Chaos Device', + 'Ochre Cliffs (E5M1) - Dragon Claw', + 'Ochre Cliffs (E5M1) - Enchanted Shield', + 'Ochre Cliffs (E5M1) - Ethereal Crossbow', + 'Ochre Cliffs (E5M1) - Exit', + 'Ochre Cliffs (E5M1) - Firemace', + 'Ochre Cliffs (E5M1) - Firemace 2', + 'Ochre Cliffs (E5M1) - Firemace 3', + 'Ochre Cliffs (E5M1) - Firemace 4', + 'Ochre Cliffs (E5M1) - Gauntlets of the Necromancer', + 'Ochre Cliffs (E5M1) - Green key', + 'Ochre Cliffs (E5M1) - Hellstaff', + 'Ochre Cliffs (E5M1) - Map Scroll', + 'Ochre Cliffs (E5M1) - Morph Ovum', + 'Ochre Cliffs (E5M1) - Mystic Urn', + 'Ochre Cliffs (E5M1) - Phoenix Rod', + 'Ochre Cliffs (E5M1) - Ring of Invincibility', + 'Ochre Cliffs (E5M1) - Shadowsphere', + 'Ochre Cliffs (E5M1) - Silver Shield', + 'Ochre Cliffs (E5M1) - Tome of Power', + 'Ochre Cliffs (E5M1) - Tome of Power 2', + 'Ochre Cliffs (E5M1) - Tome of Power 3', + 'Ochre Cliffs (E5M1) - Torch', + 'Ochre Cliffs (E5M1) - Yellow key', + }, + 'Quay (E5M3)': { + 'Quay (E5M3) - Bag of Holding', + 'Quay (E5M3) - Bag of Holding 2', + 'Quay (E5M3) - Blue key', + 'Quay (E5M3) - Chaos Device', + 'Quay (E5M3) - Dragon Claw', + 'Quay (E5M3) - Enchanted Shield', + 'Quay (E5M3) - Ethereal Crossbow', + 'Quay (E5M3) - Exit', + 'Quay (E5M3) - Firemace', + 'Quay (E5M3) - Firemace 2', + 'Quay (E5M3) - Firemace 3', + 'Quay (E5M3) - Firemace 4', + 'Quay (E5M3) - Firemace 5', + 'Quay (E5M3) - Firemace 6', + 'Quay (E5M3) - Gauntlets of the Necromancer', + 'Quay (E5M3) - Green key', + 'Quay (E5M3) - Hellstaff', + 'Quay (E5M3) - Map Scroll', + 'Quay (E5M3) - Morph Ovum', + 'Quay (E5M3) - Mystic Urn', + 'Quay (E5M3) - Phoenix Rod', + 'Quay (E5M3) - Ring of Invincibility', + 'Quay (E5M3) - Shadowsphere', + 'Quay (E5M3) - Silver Shield', + 'Quay (E5M3) - Silver Shield 2', + 'Quay (E5M3) - Tome of Power', + 'Quay (E5M3) - Tome of Power 2', + 'Quay (E5M3) - Tome of Power 3', + 'Quay (E5M3) - Torch', + 'Quay (E5M3) - Yellow key', + }, + 'Ramparts of Perdition (E4M7)': { + 'Ramparts of Perdition (E4M7) - Bag of Holding', + 'Ramparts of Perdition (E4M7) - Bag of Holding 2', + 'Ramparts of Perdition (E4M7) - Blue key', + 'Ramparts of Perdition (E4M7) - Chaos Device', + 'Ramparts of Perdition (E4M7) - Dragon Claw', + 'Ramparts of Perdition (E4M7) - Dragon Claw 2', + 'Ramparts of Perdition (E4M7) - Enchanted Shield', + 'Ramparts of Perdition (E4M7) - Ethereal Crossbow', + 'Ramparts of Perdition (E4M7) - Ethereal Crossbow 2', + 'Ramparts of Perdition (E4M7) - Exit', + 'Ramparts of Perdition (E4M7) - Firemace', + 'Ramparts of Perdition (E4M7) - Firemace 2', + 'Ramparts of Perdition (E4M7) - Firemace 3', + 'Ramparts of Perdition (E4M7) - Firemace 4', + 'Ramparts of Perdition (E4M7) - Firemace 5', + 'Ramparts of Perdition (E4M7) - Firemace 6', + 'Ramparts of Perdition (E4M7) - Gauntlets of the Necromancer', + 'Ramparts of Perdition (E4M7) - Green key', + 'Ramparts of Perdition (E4M7) - Hellstaff', + 'Ramparts of Perdition (E4M7) - Hellstaff 2', + 'Ramparts of Perdition (E4M7) - Map Scroll', + 'Ramparts of Perdition (E4M7) - Morph Ovum', + 'Ramparts of Perdition (E4M7) - Mystic Urn', + 'Ramparts of Perdition (E4M7) - Mystic Urn 2', + 'Ramparts of Perdition (E4M7) - Phoenix Rod', + 'Ramparts of Perdition (E4M7) - Phoenix Rod 2', + 'Ramparts of Perdition (E4M7) - Ring of Invincibility', + 'Ramparts of Perdition (E4M7) - Shadowsphere', + 'Ramparts of Perdition (E4M7) - Silver Shield', + 'Ramparts of Perdition (E4M7) - Silver Shield 2', + 'Ramparts of Perdition (E4M7) - Tome of Power', + 'Ramparts of Perdition (E4M7) - Tome of Power 2', + 'Ramparts of Perdition (E4M7) - Tome of Power 3', + 'Ramparts of Perdition (E4M7) - Torch', + 'Ramparts of Perdition (E4M7) - Torch 2', + 'Ramparts of Perdition (E4M7) - Yellow key', + }, + 'Rapids (E5M2)': { + 'Rapids (E5M2) - Bag of Holding', + 'Rapids (E5M2) - Bag of Holding 2', + 'Rapids (E5M2) - Bag of Holding 3', + 'Rapids (E5M2) - Chaos Device', + 'Rapids (E5M2) - Dragon Claw', + 'Rapids (E5M2) - Enchanted Shield', + 'Rapids (E5M2) - Enchanted Shield 2', + 'Rapids (E5M2) - Ethereal Crossbow', + 'Rapids (E5M2) - Exit', + 'Rapids (E5M2) - Firemace', + 'Rapids (E5M2) - Firemace 2', + 'Rapids (E5M2) - Firemace 3', + 'Rapids (E5M2) - Firemace 4', + 'Rapids (E5M2) - Firemace 5', + 'Rapids (E5M2) - Gauntlets of the Necromancer', + 'Rapids (E5M2) - Green key', + 'Rapids (E5M2) - Hellstaff', + 'Rapids (E5M2) - Hellstaff 2', + 'Rapids (E5M2) - Map Scroll', + 'Rapids (E5M2) - Morph Ovum', + 'Rapids (E5M2) - Mystic Urn', + 'Rapids (E5M2) - Phoenix Rod', + 'Rapids (E5M2) - Phoenix Rod 2', + 'Rapids (E5M2) - Ring of Invincibility', + 'Rapids (E5M2) - Shadowsphere', + 'Rapids (E5M2) - Silver Shield', + 'Rapids (E5M2) - Tome of Power', + 'Rapids (E5M2) - Tome of Power 2', + 'Rapids (E5M2) - Torch', + 'Rapids (E5M2) - Yellow key', + }, + 'Sepulcher (E4M4)': { + 'Sepulcher (E4M4) - Bag of Holding', + 'Sepulcher (E4M4) - Bag of Holding 2', + 'Sepulcher (E4M4) - Chaos Device', + 'Sepulcher (E4M4) - Dragon Claw', + 'Sepulcher (E4M4) - Dragon Claw 2', + 'Sepulcher (E4M4) - Enchanted Shield', + 'Sepulcher (E4M4) - Ethereal Crossbow', + 'Sepulcher (E4M4) - Ethereal Crossbow 2', + 'Sepulcher (E4M4) - Exit', + 'Sepulcher (E4M4) - Firemace', + 'Sepulcher (E4M4) - Firemace 2', + 'Sepulcher (E4M4) - Firemace 3', + 'Sepulcher (E4M4) - Firemace 4', + 'Sepulcher (E4M4) - Firemace 5', + 'Sepulcher (E4M4) - Hellstaff', + 'Sepulcher (E4M4) - Morph Ovum', + 'Sepulcher (E4M4) - Mystic Urn', + 'Sepulcher (E4M4) - Phoenix Rod', + 'Sepulcher (E4M4) - Phoenix Rod 2', + 'Sepulcher (E4M4) - Ring of Invincibility', + 'Sepulcher (E4M4) - Shadowsphere', + 'Sepulcher (E4M4) - Silver Shield', + 'Sepulcher (E4M4) - Silver Shield 2', + 'Sepulcher (E4M4) - Tome of Power', + 'Sepulcher (E4M4) - Tome of Power 2', + 'Sepulcher (E4M4) - Torch', + 'Sepulcher (E4M4) - Torch 2', + }, + 'Shattered Bridge (E4M8)': { + 'Shattered Bridge (E4M8) - Bag of Holding', + 'Shattered Bridge (E4M8) - Bag of Holding 2', + 'Shattered Bridge (E4M8) - Chaos Device', + 'Shattered Bridge (E4M8) - Dragon Claw', + 'Shattered Bridge (E4M8) - Enchanted Shield', + 'Shattered Bridge (E4M8) - Ethereal Crossbow', + 'Shattered Bridge (E4M8) - Exit', + 'Shattered Bridge (E4M8) - Gauntlets of the Necromancer', + 'Shattered Bridge (E4M8) - Hellstaff', + 'Shattered Bridge (E4M8) - Morph Ovum', + 'Shattered Bridge (E4M8) - Mystic Urn', + 'Shattered Bridge (E4M8) - Phoenix Rod', + 'Shattered Bridge (E4M8) - Ring of Invincibility', + 'Shattered Bridge (E4M8) - Shadowsphere', + 'Shattered Bridge (E4M8) - Silver Shield', + 'Shattered Bridge (E4M8) - Tome of Power', + 'Shattered Bridge (E4M8) - Tome of Power 2', + 'Shattered Bridge (E4M8) - Torch', + 'Shattered Bridge (E4M8) - Yellow key', + }, + "Skein of D'Sparil (E5M9)": { + "Skein of D'Sparil (E5M9) - Bag of Holding", + "Skein of D'Sparil (E5M9) - Bag of Holding 2", + "Skein of D'Sparil (E5M9) - Blue key", + "Skein of D'Sparil (E5M9) - Chaos Device", + "Skein of D'Sparil (E5M9) - Dragon Claw", + "Skein of D'Sparil (E5M9) - Enchanted Shield", + "Skein of D'Sparil (E5M9) - Ethereal Crossbow", + "Skein of D'Sparil (E5M9) - Exit", + "Skein of D'Sparil (E5M9) - Gauntlets of the Necromancer", + "Skein of D'Sparil (E5M9) - Green key", + "Skein of D'Sparil (E5M9) - Hellstaff", + "Skein of D'Sparil (E5M9) - Map Scroll", + "Skein of D'Sparil (E5M9) - Morph Ovum", + "Skein of D'Sparil (E5M9) - Mystic Urn", + "Skein of D'Sparil (E5M9) - Phoenix Rod", + "Skein of D'Sparil (E5M9) - Ring of Invincibility", + "Skein of D'Sparil (E5M9) - Shadowsphere", + "Skein of D'Sparil (E5M9) - Silver Shield", + "Skein of D'Sparil (E5M9) - Tome of Power", + "Skein of D'Sparil (E5M9) - Tome of Power 2", + "Skein of D'Sparil (E5M9) - Torch", + "Skein of D'Sparil (E5M9) - Yellow key", + }, + 'The Aquifier (E3M9)': { + 'The Aquifier (E3M9) - Bag of Holding', + 'The Aquifier (E3M9) - Blue key', + 'The Aquifier (E3M9) - Chaos Device', + 'The Aquifier (E3M9) - Dragon Claw', + 'The Aquifier (E3M9) - Enchanted Shield', + 'The Aquifier (E3M9) - Ethereal Crossbow', + 'The Aquifier (E3M9) - Exit', + 'The Aquifier (E3M9) - Firemace', + 'The Aquifier (E3M9) - Firemace 2', + 'The Aquifier (E3M9) - Firemace 3', + 'The Aquifier (E3M9) - Firemace 4', + 'The Aquifier (E3M9) - Gauntlets of the Necromancer', + 'The Aquifier (E3M9) - Green key', + 'The Aquifier (E3M9) - Hellstaff', + 'The Aquifier (E3M9) - Map Scroll', + 'The Aquifier (E3M9) - Morph Ovum', + 'The Aquifier (E3M9) - Mystic Urn', + 'The Aquifier (E3M9) - Phoenix Rod', + 'The Aquifier (E3M9) - Ring of Invincibility', + 'The Aquifier (E3M9) - Shadowsphere', + 'The Aquifier (E3M9) - Silver Shield', + 'The Aquifier (E3M9) - Silver Shield 2', + 'The Aquifier (E3M9) - Tome of Power', + 'The Aquifier (E3M9) - Tome of Power 2', + 'The Aquifier (E3M9) - Torch', + 'The Aquifier (E3M9) - Yellow key', + }, + 'The Azure Fortress (E3M4)': { + 'The Azure Fortress (E3M4) - Bag of Holding', + 'The Azure Fortress (E3M4) - Bag of Holding 2', + 'The Azure Fortress (E3M4) - Chaos Device', + 'The Azure Fortress (E3M4) - Dragon Claw', + 'The Azure Fortress (E3M4) - Enchanted Shield', + 'The Azure Fortress (E3M4) - Enchanted Shield 2', + 'The Azure Fortress (E3M4) - Ethereal Crossbow', + 'The Azure Fortress (E3M4) - Exit', + 'The Azure Fortress (E3M4) - Gauntlets of the Necromancer', + 'The Azure Fortress (E3M4) - Green key', + 'The Azure Fortress (E3M4) - Hellstaff', + 'The Azure Fortress (E3M4) - Map Scroll', + 'The Azure Fortress (E3M4) - Morph Ovum', + 'The Azure Fortress (E3M4) - Morph Ovum 2', + 'The Azure Fortress (E3M4) - Mystic Urn', + 'The Azure Fortress (E3M4) - Mystic Urn 2', + 'The Azure Fortress (E3M4) - Phoenix Rod', + 'The Azure Fortress (E3M4) - Ring of Invincibility', + 'The Azure Fortress (E3M4) - Shadowsphere', + 'The Azure Fortress (E3M4) - Silver Shield', + 'The Azure Fortress (E3M4) - Silver Shield 2', + 'The Azure Fortress (E3M4) - Tome of Power', + 'The Azure Fortress (E3M4) - Tome of Power 2', + 'The Azure Fortress (E3M4) - Tome of Power 3', + 'The Azure Fortress (E3M4) - Torch', + 'The Azure Fortress (E3M4) - Torch 2', + 'The Azure Fortress (E3M4) - Torch 3', + 'The Azure Fortress (E3M4) - Yellow key', + }, + 'The Catacombs (E2M5)': { + 'The Catacombs (E2M5) - Bag of Holding', + 'The Catacombs (E2M5) - Blue key', + 'The Catacombs (E2M5) - Chaos Device', + 'The Catacombs (E2M5) - Dragon Claw', + 'The Catacombs (E2M5) - Enchanted Shield', + 'The Catacombs (E2M5) - Ethereal Crossbow', + 'The Catacombs (E2M5) - Exit', + 'The Catacombs (E2M5) - Gauntlets of the Necromancer', + 'The Catacombs (E2M5) - Green key', + 'The Catacombs (E2M5) - Hellstaff', + 'The Catacombs (E2M5) - Map Scroll', + 'The Catacombs (E2M5) - Morph Ovum', + 'The Catacombs (E2M5) - Mystic Urn', + 'The Catacombs (E2M5) - Phoenix Rod', + 'The Catacombs (E2M5) - Ring of Invincibility', + 'The Catacombs (E2M5) - Shadowsphere', + 'The Catacombs (E2M5) - Silver Shield', + 'The Catacombs (E2M5) - Tome of Power', + 'The Catacombs (E2M5) - Tome of Power 2', + 'The Catacombs (E2M5) - Tome of Power 3', + 'The Catacombs (E2M5) - Torch', + 'The Catacombs (E2M5) - Yellow key', + }, + 'The Cathedral (E1M6)': { + 'The Cathedral (E1M6) - Bag of Holding', + 'The Cathedral (E1M6) - Bag of Holding 2', + 'The Cathedral (E1M6) - Bag of Holding 3', + 'The Cathedral (E1M6) - Dragon Claw', + 'The Cathedral (E1M6) - Ethereal Crossbow', + 'The Cathedral (E1M6) - Exit', + 'The Cathedral (E1M6) - Gauntlets of the Necromancer', + 'The Cathedral (E1M6) - Green key', + 'The Cathedral (E1M6) - Map Scroll', + 'The Cathedral (E1M6) - Morph Ovum', + 'The Cathedral (E1M6) - Ring of Invincibility', + 'The Cathedral (E1M6) - Ring of Invincibility 2', + 'The Cathedral (E1M6) - Shadowsphere', + 'The Cathedral (E1M6) - Silver Shield', + 'The Cathedral (E1M6) - Silver Shield 2', + 'The Cathedral (E1M6) - Silver Shield 3', + 'The Cathedral (E1M6) - Tome of Power', + 'The Cathedral (E1M6) - Tome of Power 2', + 'The Cathedral (E1M6) - Tome of Power 3', + 'The Cathedral (E1M6) - Tome of Power 4', + 'The Cathedral (E1M6) - Torch', + 'The Cathedral (E1M6) - Yellow key', + }, + 'The Cesspool (E3M2)': { + 'The Cesspool (E3M2) - Bag of Holding', + 'The Cesspool (E3M2) - Bag of Holding 2', + 'The Cesspool (E3M2) - Blue key', + 'The Cesspool (E3M2) - Chaos Device', + 'The Cesspool (E3M2) - Dragon Claw', + 'The Cesspool (E3M2) - Enchanted Shield', + 'The Cesspool (E3M2) - Ethereal Crossbow', + 'The Cesspool (E3M2) - Exit', + 'The Cesspool (E3M2) - Firemace', + 'The Cesspool (E3M2) - Firemace 2', + 'The Cesspool (E3M2) - Firemace 3', + 'The Cesspool (E3M2) - Firemace 4', + 'The Cesspool (E3M2) - Firemace 5', + 'The Cesspool (E3M2) - Gauntlets of the Necromancer', + 'The Cesspool (E3M2) - Green key', + 'The Cesspool (E3M2) - Hellstaff', + 'The Cesspool (E3M2) - Map Scroll', + 'The Cesspool (E3M2) - Morph Ovum', + 'The Cesspool (E3M2) - Morph Ovum 2', + 'The Cesspool (E3M2) - Mystic Urn', + 'The Cesspool (E3M2) - Phoenix Rod', + 'The Cesspool (E3M2) - Ring of Invincibility', + 'The Cesspool (E3M2) - Shadowsphere', + 'The Cesspool (E3M2) - Silver Shield', + 'The Cesspool (E3M2) - Silver Shield 2', + 'The Cesspool (E3M2) - Tome of Power', + 'The Cesspool (E3M2) - Tome of Power 2', + 'The Cesspool (E3M2) - Tome of Power 3', + 'The Cesspool (E3M2) - Torch', + 'The Cesspool (E3M2) - Yellow key', + }, + 'The Chasm (E3M7)': { + 'The Chasm (E3M7) - Bag of Holding', + 'The Chasm (E3M7) - Bag of Holding 2', + 'The Chasm (E3M7) - Blue key', + 'The Chasm (E3M7) - Chaos Device', + 'The Chasm (E3M7) - Dragon Claw', + 'The Chasm (E3M7) - Enchanted Shield', + 'The Chasm (E3M7) - Ethereal Crossbow', + 'The Chasm (E3M7) - Exit', + 'The Chasm (E3M7) - Gauntlets of the Necromancer', + 'The Chasm (E3M7) - Green key', + 'The Chasm (E3M7) - Hellstaff', + 'The Chasm (E3M7) - Map Scroll', + 'The Chasm (E3M7) - Morph Ovum', + 'The Chasm (E3M7) - Mystic Urn', + 'The Chasm (E3M7) - Phoenix Rod', + 'The Chasm (E3M7) - Ring of Invincibility', + 'The Chasm (E3M7) - Shadowsphere', + 'The Chasm (E3M7) - Shadowsphere 2', + 'The Chasm (E3M7) - Silver Shield', + 'The Chasm (E3M7) - Tome of Power', + 'The Chasm (E3M7) - Tome of Power 2', + 'The Chasm (E3M7) - Tome of Power 3', + 'The Chasm (E3M7) - Torch', + 'The Chasm (E3M7) - Torch 2', + 'The Chasm (E3M7) - Yellow key', + }, + 'The Citadel (E1M5)': { + 'The Citadel (E1M5) - Bag of Holding', + 'The Citadel (E1M5) - Blue key', + 'The Citadel (E1M5) - Dragon Claw', + 'The Citadel (E1M5) - Ethereal Crossbow', + 'The Citadel (E1M5) - Exit', + 'The Citadel (E1M5) - Gauntlets of the Necromancer', + 'The Citadel (E1M5) - Green key', + 'The Citadel (E1M5) - Map Scroll', + 'The Citadel (E1M5) - Morph Ovum', + 'The Citadel (E1M5) - Ring of Invincibility', + 'The Citadel (E1M5) - Shadowsphere', + 'The Citadel (E1M5) - Silver Shield', + 'The Citadel (E1M5) - Silver Shield 2', + 'The Citadel (E1M5) - Tome of Power', + 'The Citadel (E1M5) - Tome of Power 2', + 'The Citadel (E1M5) - Tome of Power 3', + 'The Citadel (E1M5) - Tome of Power 4', + 'The Citadel (E1M5) - Tome of Power 5', + 'The Citadel (E1M5) - Torch', + 'The Citadel (E1M5) - Torch 2', + 'The Citadel (E1M5) - Yellow key', + }, + 'The Confluence (E3M3)': { + 'The Confluence (E3M3) - Bag of Holding', + 'The Confluence (E3M3) - Blue key', + 'The Confluence (E3M3) - Chaos Device', + 'The Confluence (E3M3) - Dragon Claw', + 'The Confluence (E3M3) - Enchanted Shield', + 'The Confluence (E3M3) - Ethereal Crossbow', + 'The Confluence (E3M3) - Exit', + 'The Confluence (E3M3) - Firemace', + 'The Confluence (E3M3) - Firemace 2', + 'The Confluence (E3M3) - Firemace 3', + 'The Confluence (E3M3) - Firemace 4', + 'The Confluence (E3M3) - Firemace 5', + 'The Confluence (E3M3) - Firemace 6', + 'The Confluence (E3M3) - Gauntlets of the Necromancer', + 'The Confluence (E3M3) - Green key', + 'The Confluence (E3M3) - Hellstaff', + 'The Confluence (E3M3) - Hellstaff 2', + 'The Confluence (E3M3) - Map Scroll', + 'The Confluence (E3M3) - Morph Ovum', + 'The Confluence (E3M3) - Mystic Urn', + 'The Confluence (E3M3) - Mystic Urn 2', + 'The Confluence (E3M3) - Phoenix Rod', + 'The Confluence (E3M3) - Ring of Invincibility', + 'The Confluence (E3M3) - Shadowsphere', + 'The Confluence (E3M3) - Silver Shield', + 'The Confluence (E3M3) - Silver Shield 2', + 'The Confluence (E3M3) - Tome of Power', + 'The Confluence (E3M3) - Tome of Power 2', + 'The Confluence (E3M3) - Tome of Power 3', + 'The Confluence (E3M3) - Tome of Power 4', + 'The Confluence (E3M3) - Tome of Power 5', + 'The Confluence (E3M3) - Torch', + 'The Confluence (E3M3) - Yellow key', + }, + 'The Crater (E2M1)': { + 'The Crater (E2M1) - Bag of Holding', + 'The Crater (E2M1) - Dragon Claw', + 'The Crater (E2M1) - Ethereal Crossbow', + 'The Crater (E2M1) - Exit', + 'The Crater (E2M1) - Green key', + 'The Crater (E2M1) - Hellstaff', + 'The Crater (E2M1) - Mystic Urn', + 'The Crater (E2M1) - Shadowsphere', + 'The Crater (E2M1) - Silver Shield', + 'The Crater (E2M1) - Tome of Power', + 'The Crater (E2M1) - Torch', + 'The Crater (E2M1) - Yellow key', + }, + 'The Crypts (E1M7)': { + 'The Crypts (E1M7) - Bag of Holding', + 'The Crypts (E1M7) - Blue key', + 'The Crypts (E1M7) - Dragon Claw', + 'The Crypts (E1M7) - Ethereal Crossbow', + 'The Crypts (E1M7) - Exit', + 'The Crypts (E1M7) - Gauntlets of the Necromancer', + 'The Crypts (E1M7) - Green key', + 'The Crypts (E1M7) - Map Scroll', + 'The Crypts (E1M7) - Morph Ovum', + 'The Crypts (E1M7) - Ring of Invincibility', + 'The Crypts (E1M7) - Shadowsphere', + 'The Crypts (E1M7) - Silver Shield', + 'The Crypts (E1M7) - Silver Shield 2', + 'The Crypts (E1M7) - Tome of Power', + 'The Crypts (E1M7) - Tome of Power 2', + 'The Crypts (E1M7) - Torch', + 'The Crypts (E1M7) - Torch 2', + 'The Crypts (E1M7) - Yellow key', + }, + 'The Docks (E1M1)': { + 'The Docks (E1M1) - Bag of Holding', + 'The Docks (E1M1) - Ethereal Crossbow', + 'The Docks (E1M1) - Exit', + 'The Docks (E1M1) - Gauntlets of the Necromancer', + 'The Docks (E1M1) - Silver Shield', + 'The Docks (E1M1) - Tome of Power', + 'The Docks (E1M1) - Yellow key', + }, + 'The Dungeons (E1M2)': { + 'The Dungeons (E1M2) - Bag of Holding', + 'The Dungeons (E1M2) - Blue key', + 'The Dungeons (E1M2) - Dragon Claw', + 'The Dungeons (E1M2) - Ethereal Crossbow', + 'The Dungeons (E1M2) - Exit', + 'The Dungeons (E1M2) - Gauntlets of the Necromancer', + 'The Dungeons (E1M2) - Green key', + 'The Dungeons (E1M2) - Map Scroll', + 'The Dungeons (E1M2) - Ring of Invincibility', + 'The Dungeons (E1M2) - Shadowsphere', + 'The Dungeons (E1M2) - Silver Shield', + 'The Dungeons (E1M2) - Silver Shield 2', + 'The Dungeons (E1M2) - Tome of Power', + 'The Dungeons (E1M2) - Tome of Power 2', + 'The Dungeons (E1M2) - Torch', + 'The Dungeons (E1M2) - Yellow key', + }, + 'The Gatehouse (E1M3)': { + 'The Gatehouse (E1M3) - Bag of Holding', + 'The Gatehouse (E1M3) - Dragon Claw', + 'The Gatehouse (E1M3) - Ethereal Crossbow', + 'The Gatehouse (E1M3) - Exit', + 'The Gatehouse (E1M3) - Gauntlets of the Necromancer', + 'The Gatehouse (E1M3) - Green key', + 'The Gatehouse (E1M3) - Morph Ovum', + 'The Gatehouse (E1M3) - Ring of Invincibility', + 'The Gatehouse (E1M3) - Shadowsphere', + 'The Gatehouse (E1M3) - Silver Shield', + 'The Gatehouse (E1M3) - Tome of Power', + 'The Gatehouse (E1M3) - Tome of Power 2', + 'The Gatehouse (E1M3) - Tome of Power 3', + 'The Gatehouse (E1M3) - Torch', + 'The Gatehouse (E1M3) - Yellow key', + }, + 'The Glacier (E2M9)': { + 'The Glacier (E2M9) - Bag of Holding', + 'The Glacier (E2M9) - Blue key', + 'The Glacier (E2M9) - Chaos Device', + 'The Glacier (E2M9) - Dragon Claw', + 'The Glacier (E2M9) - Dragon Claw 2', + 'The Glacier (E2M9) - Enchanted Shield', + 'The Glacier (E2M9) - Ethereal Crossbow', + 'The Glacier (E2M9) - Exit', + 'The Glacier (E2M9) - Firemace', + 'The Glacier (E2M9) - Firemace 2', + 'The Glacier (E2M9) - Firemace 3', + 'The Glacier (E2M9) - Firemace 4', + 'The Glacier (E2M9) - Gauntlets of the Necromancer', + 'The Glacier (E2M9) - Green key', + 'The Glacier (E2M9) - Hellstaff', + 'The Glacier (E2M9) - Map Scroll', + 'The Glacier (E2M9) - Morph Ovum', + 'The Glacier (E2M9) - Mystic Urn', + 'The Glacier (E2M9) - Mystic Urn 2', + 'The Glacier (E2M9) - Phoenix Rod', + 'The Glacier (E2M9) - Ring of Invincibility', + 'The Glacier (E2M9) - Shadowsphere', + 'The Glacier (E2M9) - Silver Shield', + 'The Glacier (E2M9) - Tome of Power', + 'The Glacier (E2M9) - Tome of Power 2', + 'The Glacier (E2M9) - Torch', + 'The Glacier (E2M9) - Torch 2', + 'The Glacier (E2M9) - Yellow key', + }, + 'The Graveyard (E1M9)': { + 'The Graveyard (E1M9) - Bag of Holding', + 'The Graveyard (E1M9) - Blue key', + 'The Graveyard (E1M9) - Dragon Claw', + 'The Graveyard (E1M9) - Dragon Claw 2', + 'The Graveyard (E1M9) - Ethereal Crossbow', + 'The Graveyard (E1M9) - Exit', + 'The Graveyard (E1M9) - Green key', + 'The Graveyard (E1M9) - Map Scroll', + 'The Graveyard (E1M9) - Morph Ovum', + 'The Graveyard (E1M9) - Ring of Invincibility', + 'The Graveyard (E1M9) - Shadowsphere', + 'The Graveyard (E1M9) - Silver Shield', + 'The Graveyard (E1M9) - Tome of Power', + 'The Graveyard (E1M9) - Tome of Power 2', + 'The Graveyard (E1M9) - Torch', + 'The Graveyard (E1M9) - Yellow key', + }, + 'The Great Hall (E2M7)': { + 'The Great Hall (E2M7) - Bag of Holding', + 'The Great Hall (E2M7) - Blue key', + 'The Great Hall (E2M7) - Chaos Device', + 'The Great Hall (E2M7) - Dragon Claw', + 'The Great Hall (E2M7) - Enchanted Shield', + 'The Great Hall (E2M7) - Ethereal Crossbow', + 'The Great Hall (E2M7) - Exit', + 'The Great Hall (E2M7) - Gauntlets of the Necromancer', + 'The Great Hall (E2M7) - Green key', + 'The Great Hall (E2M7) - Hellstaff', + 'The Great Hall (E2M7) - Map Scroll', + 'The Great Hall (E2M7) - Morph Ovum', + 'The Great Hall (E2M7) - Mystic Urn', + 'The Great Hall (E2M7) - Phoenix Rod', + 'The Great Hall (E2M7) - Ring of Invincibility', + 'The Great Hall (E2M7) - Shadowsphere', + 'The Great Hall (E2M7) - Silver Shield', + 'The Great Hall (E2M7) - Tome of Power', + 'The Great Hall (E2M7) - Tome of Power 2', + 'The Great Hall (E2M7) - Torch', + 'The Great Hall (E2M7) - Yellow key', + }, + 'The Guard Tower (E1M4)': { + 'The Guard Tower (E1M4) - Bag of Holding', + 'The Guard Tower (E1M4) - Dragon Claw', + 'The Guard Tower (E1M4) - Ethereal Crossbow', + 'The Guard Tower (E1M4) - Exit', + 'The Guard Tower (E1M4) - Gauntlets of the Necromancer', + 'The Guard Tower (E1M4) - Green key', + 'The Guard Tower (E1M4) - Map Scroll', + 'The Guard Tower (E1M4) - Morph Ovum', + 'The Guard Tower (E1M4) - Shadowsphere', + 'The Guard Tower (E1M4) - Silver Shield', + 'The Guard Tower (E1M4) - Tome of Power', + 'The Guard Tower (E1M4) - Tome of Power 2', + 'The Guard Tower (E1M4) - Tome of Power 3', + 'The Guard Tower (E1M4) - Torch', + 'The Guard Tower (E1M4) - Yellow key', + }, + 'The Halls of Fear (E3M6)': { + 'The Halls of Fear (E3M6) - Bag of Holding', + 'The Halls of Fear (E3M6) - Bag of Holding 2', + 'The Halls of Fear (E3M6) - Bag of Holding 3', + 'The Halls of Fear (E3M6) - Blue key', + 'The Halls of Fear (E3M6) - Chaos Device', + 'The Halls of Fear (E3M6) - Dragon Claw', + 'The Halls of Fear (E3M6) - Enchanted Shield', + 'The Halls of Fear (E3M6) - Ethereal Crossbow', + 'The Halls of Fear (E3M6) - Exit', + 'The Halls of Fear (E3M6) - Firemace', + 'The Halls of Fear (E3M6) - Firemace 2', + 'The Halls of Fear (E3M6) - Firemace 3', + 'The Halls of Fear (E3M6) - Firemace 4', + 'The Halls of Fear (E3M6) - Firemace 5', + 'The Halls of Fear (E3M6) - Firemace 6', + 'The Halls of Fear (E3M6) - Gauntlets of the Necromancer', + 'The Halls of Fear (E3M6) - Green key', + 'The Halls of Fear (E3M6) - Hellstaff', + 'The Halls of Fear (E3M6) - Hellstaff 2', + 'The Halls of Fear (E3M6) - Map Scroll', + 'The Halls of Fear (E3M6) - Morph Ovum', + 'The Halls of Fear (E3M6) - Mystic Urn', + 'The Halls of Fear (E3M6) - Mystic Urn 2', + 'The Halls of Fear (E3M6) - Phoenix Rod', + 'The Halls of Fear (E3M6) - Ring of Invincibility', + 'The Halls of Fear (E3M6) - Shadowsphere', + 'The Halls of Fear (E3M6) - Silver Shield', + 'The Halls of Fear (E3M6) - Tome of Power', + 'The Halls of Fear (E3M6) - Tome of Power 2', + 'The Halls of Fear (E3M6) - Tome of Power 3', + 'The Halls of Fear (E3M6) - Yellow key', + }, + 'The Ice Grotto (E2M4)': { + 'The Ice Grotto (E2M4) - Bag of Holding', + 'The Ice Grotto (E2M4) - Bag of Holding 2', + 'The Ice Grotto (E2M4) - Blue key', + 'The Ice Grotto (E2M4) - Chaos Device', + 'The Ice Grotto (E2M4) - Dragon Claw', + 'The Ice Grotto (E2M4) - Enchanted Shield', + 'The Ice Grotto (E2M4) - Ethereal Crossbow', + 'The Ice Grotto (E2M4) - Exit', + 'The Ice Grotto (E2M4) - Gauntlets of the Necromancer', + 'The Ice Grotto (E2M4) - Green key', + 'The Ice Grotto (E2M4) - Hellstaff', + 'The Ice Grotto (E2M4) - Map Scroll', + 'The Ice Grotto (E2M4) - Morph Ovum', + 'The Ice Grotto (E2M4) - Mystic Urn', + 'The Ice Grotto (E2M4) - Phoenix Rod', + 'The Ice Grotto (E2M4) - Shadowsphere', + 'The Ice Grotto (E2M4) - Shadowsphere 2', + 'The Ice Grotto (E2M4) - Silver Shield', + 'The Ice Grotto (E2M4) - Tome of Power', + 'The Ice Grotto (E2M4) - Tome of Power 2', + 'The Ice Grotto (E2M4) - Tome of Power 3', + 'The Ice Grotto (E2M4) - Torch', + 'The Ice Grotto (E2M4) - Yellow key', + }, + 'The Labyrinth (E2M6)': { + 'The Labyrinth (E2M6) - Bag of Holding', + 'The Labyrinth (E2M6) - Blue key', + 'The Labyrinth (E2M6) - Chaos Device', + 'The Labyrinth (E2M6) - Dragon Claw', + 'The Labyrinth (E2M6) - Enchanted Shield', + 'The Labyrinth (E2M6) - Ethereal Crossbow', + 'The Labyrinth (E2M6) - Exit', + 'The Labyrinth (E2M6) - Firemace', + 'The Labyrinth (E2M6) - Firemace 2', + 'The Labyrinth (E2M6) - Firemace 3', + 'The Labyrinth (E2M6) - Firemace 4', + 'The Labyrinth (E2M6) - Gauntlets of the Necromancer', + 'The Labyrinth (E2M6) - Green key', + 'The Labyrinth (E2M6) - Hellstaff', + 'The Labyrinth (E2M6) - Map Scroll', + 'The Labyrinth (E2M6) - Morph Ovum', + 'The Labyrinth (E2M6) - Mystic Urn', + 'The Labyrinth (E2M6) - Phoenix Rod', + 'The Labyrinth (E2M6) - Phoenix Rod 2', + 'The Labyrinth (E2M6) - Ring of Invincibility', + 'The Labyrinth (E2M6) - Shadowsphere', + 'The Labyrinth (E2M6) - Silver Shield', + 'The Labyrinth (E2M6) - Tome of Power', + 'The Labyrinth (E2M6) - Tome of Power 2', + 'The Labyrinth (E2M6) - Yellow key', + }, + 'The Lava Pits (E2M2)': { + 'The Lava Pits (E2M2) - Bag of Holding', + 'The Lava Pits (E2M2) - Bag of Holding 2', + 'The Lava Pits (E2M2) - Chaos Device', + 'The Lava Pits (E2M2) - Dragon Claw', + 'The Lava Pits (E2M2) - Enchanted Shield', + 'The Lava Pits (E2M2) - Ethereal Crossbow', + 'The Lava Pits (E2M2) - Exit', + 'The Lava Pits (E2M2) - Gauntlets of the Necromancer', + 'The Lava Pits (E2M2) - Green key', + 'The Lava Pits (E2M2) - Hellstaff', + 'The Lava Pits (E2M2) - Map Scroll', + 'The Lava Pits (E2M2) - Morph Ovum', + 'The Lava Pits (E2M2) - Mystic Urn', + 'The Lava Pits (E2M2) - Ring of Invincibility', + 'The Lava Pits (E2M2) - Shadowsphere', + 'The Lava Pits (E2M2) - Silver Shield', + 'The Lava Pits (E2M2) - Silver Shield 2', + 'The Lava Pits (E2M2) - Tome of Power', + 'The Lava Pits (E2M2) - Tome of Power 2', + 'The Lava Pits (E2M2) - Tome of Power 3', + 'The Lava Pits (E2M2) - Yellow key', + }, + 'The Ophidian Lair (E3M5)': { + 'The Ophidian Lair (E3M5) - Bag of Holding', + 'The Ophidian Lair (E3M5) - Chaos Device', + 'The Ophidian Lair (E3M5) - Dragon Claw', + 'The Ophidian Lair (E3M5) - Enchanted Shield', + 'The Ophidian Lair (E3M5) - Ethereal Crossbow', + 'The Ophidian Lair (E3M5) - Exit', + 'The Ophidian Lair (E3M5) - Gauntlets of the Necromancer', + 'The Ophidian Lair (E3M5) - Green key', + 'The Ophidian Lair (E3M5) - Hellstaff', + 'The Ophidian Lair (E3M5) - Map Scroll', + 'The Ophidian Lair (E3M5) - Morph Ovum', + 'The Ophidian Lair (E3M5) - Mystic Urn', + 'The Ophidian Lair (E3M5) - Mystic Urn 2', + 'The Ophidian Lair (E3M5) - Phoenix Rod', + 'The Ophidian Lair (E3M5) - Ring of Invincibility', + 'The Ophidian Lair (E3M5) - Shadowsphere', + 'The Ophidian Lair (E3M5) - Silver Shield', + 'The Ophidian Lair (E3M5) - Silver Shield 2', + 'The Ophidian Lair (E3M5) - Tome of Power', + 'The Ophidian Lair (E3M5) - Tome of Power 2', + 'The Ophidian Lair (E3M5) - Torch', + 'The Ophidian Lair (E3M5) - Yellow key', + }, + 'The Portals of Chaos (E2M8)': { + 'The Portals of Chaos (E2M8) - Bag of Holding', + 'The Portals of Chaos (E2M8) - Chaos Device', + 'The Portals of Chaos (E2M8) - Dragon Claw', + 'The Portals of Chaos (E2M8) - Enchanted Shield', + 'The Portals of Chaos (E2M8) - Ethereal Crossbow', + 'The Portals of Chaos (E2M8) - Exit', + 'The Portals of Chaos (E2M8) - Gauntlets of the Necromancer', + 'The Portals of Chaos (E2M8) - Hellstaff', + 'The Portals of Chaos (E2M8) - Morph Ovum', + 'The Portals of Chaos (E2M8) - Mystic Urn', + 'The Portals of Chaos (E2M8) - Mystic Urn 2', + 'The Portals of Chaos (E2M8) - Phoenix Rod', + 'The Portals of Chaos (E2M8) - Ring of Invincibility', + 'The Portals of Chaos (E2M8) - Shadowsphere', + 'The Portals of Chaos (E2M8) - Silver Shield', + 'The Portals of Chaos (E2M8) - Tome of Power', + }, + 'The River of Fire (E2M3)': { + 'The River of Fire (E2M3) - Bag of Holding', + 'The River of Fire (E2M3) - Blue key', + 'The River of Fire (E2M3) - Chaos Device', + 'The River of Fire (E2M3) - Dragon Claw', + 'The River of Fire (E2M3) - Enchanted Shield', + 'The River of Fire (E2M3) - Ethereal Crossbow', + 'The River of Fire (E2M3) - Exit', + 'The River of Fire (E2M3) - Firemace', + 'The River of Fire (E2M3) - Firemace 2', + 'The River of Fire (E2M3) - Firemace 3', + 'The River of Fire (E2M3) - Gauntlets of the Necromancer', + 'The River of Fire (E2M3) - Green key', + 'The River of Fire (E2M3) - Hellstaff', + 'The River of Fire (E2M3) - Morph Ovum', + 'The River of Fire (E2M3) - Mystic Urn', + 'The River of Fire (E2M3) - Phoenix Rod', + 'The River of Fire (E2M3) - Ring of Invincibility', + 'The River of Fire (E2M3) - Shadowsphere', + 'The River of Fire (E2M3) - Silver Shield', + 'The River of Fire (E2M3) - Tome of Power', + 'The River of Fire (E2M3) - Tome of Power 2', + 'The River of Fire (E2M3) - Yellow key', + }, + 'The Storehouse (E3M1)': { + 'The Storehouse (E3M1) - Bag of Holding', + 'The Storehouse (E3M1) - Chaos Device', + 'The Storehouse (E3M1) - Dragon Claw', + 'The Storehouse (E3M1) - Exit', + 'The Storehouse (E3M1) - Gauntlets of the Necromancer', + 'The Storehouse (E3M1) - Green key', + 'The Storehouse (E3M1) - Hellstaff', + 'The Storehouse (E3M1) - Map Scroll', + 'The Storehouse (E3M1) - Ring of Invincibility', + 'The Storehouse (E3M1) - Shadowsphere', + 'The Storehouse (E3M1) - Silver Shield', + 'The Storehouse (E3M1) - Tome of Power', + 'The Storehouse (E3M1) - Torch', + 'The Storehouse (E3M1) - Yellow key', + }, +} + + +death_logic_locations = [ + "Ramparts of Perdition (E4M7) - Ring of Invincibility", + "Ramparts of Perdition (E4M7) - Ethereal Crossbow 2", +] diff --git a/worlds/heretic/Maps.py b/worlds/heretic/Maps.py new file mode 100644 index 000000000000..716de2904144 --- /dev/null +++ b/worlds/heretic/Maps.py @@ -0,0 +1,52 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import List + + +map_names: List[str] = [ + 'The Docks (E1M1)', + 'The Dungeons (E1M2)', + 'The Gatehouse (E1M3)', + 'The Guard Tower (E1M4)', + 'The Citadel (E1M5)', + 'The Cathedral (E1M6)', + 'The Crypts (E1M7)', + "Hell's Maw (E1M8)", + 'The Graveyard (E1M9)', + 'The Crater (E2M1)', + 'The Lava Pits (E2M2)', + 'The River of Fire (E2M3)', + 'The Ice Grotto (E2M4)', + 'The Catacombs (E2M5)', + 'The Labyrinth (E2M6)', + 'The Great Hall (E2M7)', + 'The Portals of Chaos (E2M8)', + 'The Glacier (E2M9)', + 'The Storehouse (E3M1)', + 'The Cesspool (E3M2)', + 'The Confluence (E3M3)', + 'The Azure Fortress (E3M4)', + 'The Ophidian Lair (E3M5)', + 'The Halls of Fear (E3M6)', + 'The Chasm (E3M7)', + "D'Sparil'S Keep (E3M8)", + 'The Aquifier (E3M9)', + 'Catafalque (E4M1)', + 'Blockhouse (E4M2)', + 'Ambulatory (E4M3)', + 'Sepulcher (E4M4)', + 'Great Stair (E4M5)', + 'Halls of the Apostate (E4M6)', + 'Ramparts of Perdition (E4M7)', + 'Shattered Bridge (E4M8)', + 'Mausoleum (E4M9)', + 'Ochre Cliffs (E5M1)', + 'Rapids (E5M2)', + 'Quay (E5M3)', + 'Courtyard (E5M4)', + 'Hydratyr (E5M5)', + 'Colonnade (E5M6)', + 'Foetid Manse (E5M7)', + 'Field of Judgement (E5M8)', + "Skein of D'Sparil (E5M9)", +] diff --git a/worlds/heretic/Options.py b/worlds/heretic/Options.py new file mode 100644 index 000000000000..34255f39eb5a --- /dev/null +++ b/worlds/heretic/Options.py @@ -0,0 +1,167 @@ +import typing + +from Options import AssembleOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool + + +class Goal(Choice): + """ + Choose the main goal. + complete_all_levels: All levels of the selected episodes + complete_boss_levels: Boss levels (E#M8) of selected episodes + """ + display_name = "Goal" + option_complete_all_levels = 0 + option_complete_boss_levels = 1 + default = 0 + + +class Difficulty(Choice): + """ + Choose the difficulty option. Those match DOOM's difficulty options. + baby (I'm too young to die.) double ammos, half damage, less monsters or strength. + easy (Hey, not too rough.) less monsters or strength. + medium (Hurt me plenty.) Default. + hard (Ultra-Violence.) More monsters or strength. + nightmare (Nightmare!) Monsters attack more rapidly and respawn. + + wet nurse (hou needeth a wet-nurse) - Fewer monsters and more items than medium. Damage taken is halved, and ammo pickups carry twice as much ammo. Any Quartz Flasks and Mystic Urns are automatically used when the player nears death. + easy (Yellowbellies-r-us) - Fewer monsters and more items than medium. + medium (Bringest them oneth) - Completely balanced, this is the standard difficulty level. + hard (Thou art a smite-meister) - More monsters and fewer items than medium. + black plague (Black plague possesses thee) - Same as hard, but monsters and their projectiles move much faster. Cheating is also disabled. + """ + display_name = "Difficulty" + option_wet_nurse = 0 + option_easy = 1 + option_medium = 2 + option_hard = 3 + option_black_plague = 4 + default = 2 + + +class RandomMonsters(Choice): + """ + Choose how monsters are randomized. + vanilla: No randomization + shuffle: Monsters are shuffled within the level + random_balanced: Monsters are completely randomized, but balanced based on existing ratio in the level. (Small monsters vs medium vs big) + random_chaotic: Monsters are completely randomized, but balanced based on existing ratio in the entire game. + """ + display_name = "Random Monsters" + option_vanilla = 0 + option_shuffle = 1 + option_random_balanced = 2 + option_random_chaotic = 3 + default = 1 + + +class RandomPickups(Choice): + """ + Choose how pickups are randomized. + vanilla: No randomization + shuffle: Pickups are shuffled within the level + random_balanced: Pickups are completely randomized, but balanced based on existing ratio in the level. (Small pickups vs Big) + """ + display_name = "Random Pickups" + option_vanilla = 0 + option_shuffle = 1 + option_random_balanced = 2 + default = 1 + + +class RandomMusic(Choice): + """ + Level musics will be randomized. + vanilla: No randomization + shuffle_selected: Selected episodes' levels will be shuffled + shuffle_game: All the music will be shuffled + """ + display_name = "Random Music" + option_vanilla = 0 + option_shuffle_selected = 1 + option_shuffle_game = 2 + default = 0 + + +class AllowDeathLogic(Toggle): + """Some locations require a timed puzzle that can only be tried once. + After which, if the player failed to get it, the location cannot be checked anymore. + By default, no progression items are placed here. There is a way, hovewer, to still get them: + Get killed in the current map. The map will reset, you can now attempt the puzzle again.""" + display_name = "Allow Death Logic" + + +class Pro(Toggle): + """Include difficult tricks into rules. Mostly employed by speed runners. + i.e.: Leaps across to a locked area, trigger a switch behind a window at the right angle, etc.""" + display_name = "Pro Heretic" + + +class StartWithMapScrolls(Toggle): + """Give the player all Map Scroll items from the start.""" + display_name = "Start With Map Scrolls" + + +class ResetLevelOnDeath(DefaultOnToggle): + """When dying, levels are reset and monsters respawned. But inventory and checks are kept. + Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" + display_message="Reset level on death" + + +class CheckSanity(Toggle): + """Include redundant checks. This increase total check count for the game. + i.e.: In a room, there might be 3 checks close to each other. By default, two of them will be remove. + This was done to lower the total count check for Heretic, as it is quite high compared to other games. + Check Sanity restores original checks.""" + display_name = "Check Sanity" + + +class Episode1(DefaultOnToggle): + """City of the Damned. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 1" + + +class Episode2(DefaultOnToggle): + """Hell's Maw. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 2" + + +class Episode3(DefaultOnToggle): + """The Dome of D'Sparil. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 3" + + +class Episode4(Toggle): + """The Ossuary. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 4" + + +class Episode5(Toggle): + """The Stagnant Demesne. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 5" + + +options: typing.Dict[str, AssembleOptions] = { + "start_inventory_from_pool": StartInventoryPool, + "goal": Goal, + "difficulty": Difficulty, + "random_monsters": RandomMonsters, + "random_pickups": RandomPickups, + "random_music": RandomMusic, + "allow_death_logic": AllowDeathLogic, + "pro": Pro, + "check_sanity": CheckSanity, + "start_with_map_scrolls": StartWithMapScrolls, + "reset_level_on_death": ResetLevelOnDeath, + "death_link": DeathLink, + "episode1": Episode1, + "episode2": Episode2, + "episode3": Episode3, + "episode4": Episode4, + "episode5": Episode5 +} diff --git a/worlds/heretic/Regions.py b/worlds/heretic/Regions.py new file mode 100644 index 000000000000..a30f0120a0c4 --- /dev/null +++ b/worlds/heretic/Regions.py @@ -0,0 +1,894 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import List +from BaseClasses import TypedDict + +class ConnectionDict(TypedDict, total=False): + target: str + pro: bool + +class RegionDict(TypedDict, total=False): + name: str + connects_to_hub: bool + episode: int + connections: List[ConnectionDict] + + +regions:List[RegionDict] = [ + # The Docks (E1M1) + {"name":"The Docks (E1M1) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"The Docks (E1M1) Yellow","pro":False}]}, + {"name":"The Docks (E1M1) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Docks (E1M1) Main","pro":False}, + {"target":"The Docks (E1M1) Sea","pro":False}]}, + {"name":"The Docks (E1M1) Sea", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Docks (E1M1) Main","pro":False}]}, + + # The Dungeons (E1M2) + {"name":"The Dungeons (E1M2) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Dungeons (E1M2) Yellow","pro":False}, + {"target":"The Dungeons (E1M2) Green","pro":False}]}, + {"name":"The Dungeons (E1M2) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Dungeons (E1M2) Yellow","pro":False}]}, + {"name":"The Dungeons (E1M2) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Dungeons (E1M2) Main","pro":False}, + {"target":"The Dungeons (E1M2) Blue","pro":False}]}, + {"name":"The Dungeons (E1M2) Green", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Dungeons (E1M2) Main","pro":False}, + {"target":"The Dungeons (E1M2) Yellow","pro":False}]}, + + # The Gatehouse (E1M3) + {"name":"The Gatehouse (E1M3) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Gatehouse (E1M3) Yellow","pro":False}, + {"target":"The Gatehouse (E1M3) Sea","pro":False}, + {"target":"The Gatehouse (E1M3) Green","pro":False}]}, + {"name":"The Gatehouse (E1M3) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Gatehouse (E1M3) Main","pro":False}]}, + {"name":"The Gatehouse (E1M3) Green", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Gatehouse (E1M3) Main","pro":False}]}, + {"name":"The Gatehouse (E1M3) Sea", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Gatehouse (E1M3) Main","pro":False}]}, + + # The Guard Tower (E1M4) + {"name":"The Guard Tower (E1M4) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"The Guard Tower (E1M4) Yellow","pro":False}]}, + {"name":"The Guard Tower (E1M4) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Guard Tower (E1M4) Green","pro":False}, + {"target":"The Guard Tower (E1M4) Main","pro":False}]}, + {"name":"The Guard Tower (E1M4) Green", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Guard Tower (E1M4) Yellow","pro":False}]}, + + # The Citadel (E1M5) + {"name":"The Citadel (E1M5) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"The Citadel (E1M5) Yellow","pro":False}]}, + {"name":"The Citadel (E1M5) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Citadel (E1M5) Green","pro":False}]}, + {"name":"The Citadel (E1M5) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Citadel (E1M5) Main","pro":False}, + {"target":"The Citadel (E1M5) Well","pro":False}, + {"target":"The Citadel (E1M5) Green","pro":False}]}, + {"name":"The Citadel (E1M5) Green", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Citadel (E1M5) Main","pro":False}, + {"target":"The Citadel (E1M5) Well","pro":False}, + {"target":"The Citadel (E1M5) Blue","pro":False}]}, + {"name":"The Citadel (E1M5) Well", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Citadel (E1M5) Main","pro":False}]}, + + # The Cathedral (E1M6) + {"name":"The Cathedral (E1M6) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"The Cathedral (E1M6) Yellow","pro":False}]}, + {"name":"The Cathedral (E1M6) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Cathedral (E1M6) Green","pro":False}, + {"target":"The Cathedral (E1M6) Main","pro":False}, + {"target":"The Cathedral (E1M6) Main Fly","pro":False}]}, + {"name":"The Cathedral (E1M6) Green", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Cathedral (E1M6) Yellow","pro":False}, + {"target":"The Cathedral (E1M6) Main Fly","pro":False}]}, + {"name":"The Cathedral (E1M6) Main Fly", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Cathedral (E1M6) Main","pro":False}]}, + + # The Crypts (E1M7) + {"name":"The Crypts (E1M7) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Crypts (E1M7) Yellow","pro":False}, + {"target":"The Crypts (E1M7) Green","pro":False}]}, + {"name":"The Crypts (E1M7) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Crypts (E1M7) Yellow","pro":False}, + {"target":"The Crypts (E1M7) Main","pro":False}]}, + {"name":"The Crypts (E1M7) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Crypts (E1M7) Main","pro":False}, + {"target":"The Crypts (E1M7) Green","pro":False}, + {"target":"The Crypts (E1M7) Blue","pro":False}]}, + {"name":"The Crypts (E1M7) Green", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Crypts (E1M7) Yellow","pro":False}, + {"target":"The Crypts (E1M7) Main","pro":False}]}, + + # Hell's Maw (E1M8) + {"name":"Hell's Maw (E1M8) Main", + "connects_to_hub":True, + "episode":1, + "connections":[]}, + + # The Graveyard (E1M9) + {"name":"The Graveyard (E1M9) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Graveyard (E1M9) Yellow","pro":False}, + {"target":"The Graveyard (E1M9) Green","pro":False}, + {"target":"The Graveyard (E1M9) Blue","pro":False}]}, + {"name":"The Graveyard (E1M9) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Graveyard (E1M9) Main","pro":False}]}, + {"name":"The Graveyard (E1M9) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Graveyard (E1M9) Main","pro":False}]}, + {"name":"The Graveyard (E1M9) Green", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Graveyard (E1M9) Main","pro":False}]}, + + # The Crater (E2M1) + {"name":"The Crater (E2M1) Main", + "connects_to_hub":True, + "episode":2, + "connections":[{"target":"The Crater (E2M1) Yellow","pro":False}]}, + {"name":"The Crater (E2M1) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Crater (E2M1) Main","pro":False}, + {"target":"The Crater (E2M1) Green","pro":False}]}, + {"name":"The Crater (E2M1) Green", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Crater (E2M1) Yellow","pro":False}]}, + + # The Lava Pits (E2M2) + {"name":"The Lava Pits (E2M2) Main", + "connects_to_hub":True, + "episode":2, + "connections":[{"target":"The Lava Pits (E2M2) Yellow","pro":False}]}, + {"name":"The Lava Pits (E2M2) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Lava Pits (E2M2) Green","pro":False}, + {"target":"The Lava Pits (E2M2) Main","pro":False}]}, + {"name":"The Lava Pits (E2M2) Green", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Lava Pits (E2M2) Main","pro":False}, + {"target":"The Lava Pits (E2M2) Yellow","pro":False}]}, + + # The River of Fire (E2M3) + {"name":"The River of Fire (E2M3) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The River of Fire (E2M3) Yellow","pro":False}, + {"target":"The River of Fire (E2M3) Blue","pro":False}, + {"target":"The River of Fire (E2M3) Green","pro":False}]}, + {"name":"The River of Fire (E2M3) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The River of Fire (E2M3) Main","pro":False}]}, + {"name":"The River of Fire (E2M3) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The River of Fire (E2M3) Main","pro":False}]}, + {"name":"The River of Fire (E2M3) Green", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The River of Fire (E2M3) Main","pro":False}]}, + + # The Ice Grotto (E2M4) + {"name":"The Ice Grotto (E2M4) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The Ice Grotto (E2M4) Green","pro":False}, + {"target":"The Ice Grotto (E2M4) Yellow","pro":False}]}, + {"name":"The Ice Grotto (E2M4) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Ice Grotto (E2M4) Green","pro":False}]}, + {"name":"The Ice Grotto (E2M4) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Ice Grotto (E2M4) Main","pro":False}, + {"target":"The Ice Grotto (E2M4) Magenta","pro":False}]}, + {"name":"The Ice Grotto (E2M4) Green", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Ice Grotto (E2M4) Main","pro":False}, + {"target":"The Ice Grotto (E2M4) Blue","pro":False}]}, + {"name":"The Ice Grotto (E2M4) Magenta", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Ice Grotto (E2M4) Yellow","pro":False}]}, + + # The Catacombs (E2M5) + {"name":"The Catacombs (E2M5) Main", + "connects_to_hub":True, + "episode":2, + "connections":[{"target":"The Catacombs (E2M5) Yellow","pro":False}]}, + {"name":"The Catacombs (E2M5) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Catacombs (E2M5) Green","pro":False}]}, + {"name":"The Catacombs (E2M5) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Catacombs (E2M5) Green","pro":False}, + {"target":"The Catacombs (E2M5) Main","pro":False}]}, + {"name":"The Catacombs (E2M5) Green", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Catacombs (E2M5) Blue","pro":False}, + {"target":"The Catacombs (E2M5) Yellow","pro":False}, + {"target":"The Catacombs (E2M5) Main","pro":False}]}, + + # The Labyrinth (E2M6) + {"name":"The Labyrinth (E2M6) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The Labyrinth (E2M6) Blue","pro":False}, + {"target":"The Labyrinth (E2M6) Yellow","pro":False}, + {"target":"The Labyrinth (E2M6) Green","pro":False}]}, + {"name":"The Labyrinth (E2M6) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Labyrinth (E2M6) Main","pro":False}]}, + {"name":"The Labyrinth (E2M6) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Labyrinth (E2M6) Main","pro":False}]}, + {"name":"The Labyrinth (E2M6) Green", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Labyrinth (E2M6) Main","pro":False}]}, + + # The Great Hall (E2M7) + {"name":"The Great Hall (E2M7) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The Great Hall (E2M7) Yellow","pro":False}, + {"target":"The Great Hall (E2M7) Green","pro":False}]}, + {"name":"The Great Hall (E2M7) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Great Hall (E2M7) Yellow","pro":False}]}, + {"name":"The Great Hall (E2M7) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Great Hall (E2M7) Blue","pro":False}, + {"target":"The Great Hall (E2M7) Main","pro":False}]}, + {"name":"The Great Hall (E2M7) Green", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Great Hall (E2M7) Main","pro":False}]}, + + # The Portals of Chaos (E2M8) + {"name":"The Portals of Chaos (E2M8) Main", + "connects_to_hub":True, + "episode":2, + "connections":[]}, + + # The Glacier (E2M9) + {"name":"The Glacier (E2M9) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The Glacier (E2M9) Yellow","pro":False}, + {"target":"The Glacier (E2M9) Blue","pro":False}, + {"target":"The Glacier (E2M9) Green","pro":False}]}, + {"name":"The Glacier (E2M9) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Glacier (E2M9) Main","pro":False}]}, + {"name":"The Glacier (E2M9) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Glacier (E2M9) Main","pro":False}]}, + {"name":"The Glacier (E2M9) Green", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Glacier (E2M9) Main","pro":False}]}, + + # The Storehouse (E3M1) + {"name":"The Storehouse (E3M1) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Storehouse (E3M1) Yellow","pro":False}, + {"target":"The Storehouse (E3M1) Green","pro":False}]}, + {"name":"The Storehouse (E3M1) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Storehouse (E3M1) Main","pro":False}]}, + {"name":"The Storehouse (E3M1) Green", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Storehouse (E3M1) Main","pro":False}]}, + + # The Cesspool (E3M2) + {"name":"The Cesspool (E3M2) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"The Cesspool (E3M2) Yellow","pro":False}]}, + {"name":"The Cesspool (E3M2) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Cesspool (E3M2) Green","pro":False}]}, + {"name":"The Cesspool (E3M2) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Cesspool (E3M2) Main","pro":False}, + {"target":"The Cesspool (E3M2) Green","pro":False}]}, + {"name":"The Cesspool (E3M2) Green", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Cesspool (E3M2) Blue","pro":False}, + {"target":"The Cesspool (E3M2) Main","pro":False}, + {"target":"The Cesspool (E3M2) Yellow","pro":False}]}, + + # The Confluence (E3M3) + {"name":"The Confluence (E3M3) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Confluence (E3M3) Green","pro":False}, + {"target":"The Confluence (E3M3) Yellow","pro":False}]}, + {"name":"The Confluence (E3M3) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Confluence (E3M3) Green","pro":False}]}, + {"name":"The Confluence (E3M3) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Confluence (E3M3) Main","pro":False}]}, + {"name":"The Confluence (E3M3) Green", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Confluence (E3M3) Main","pro":False}, + {"target":"The Confluence (E3M3) Blue","pro":False}, + {"target":"The Confluence (E3M3) Yellow","pro":False}]}, + + # The Azure Fortress (E3M4) + {"name":"The Azure Fortress (E3M4) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Azure Fortress (E3M4) Green","pro":False}, + {"target":"The Azure Fortress (E3M4) Yellow","pro":False}]}, + {"name":"The Azure Fortress (E3M4) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Azure Fortress (E3M4) Main","pro":False}]}, + {"name":"The Azure Fortress (E3M4) Green", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Azure Fortress (E3M4) Main","pro":False}]}, + + # The Ophidian Lair (E3M5) + {"name":"The Ophidian Lair (E3M5) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Ophidian Lair (E3M5) Yellow","pro":False}, + {"target":"The Ophidian Lair (E3M5) Green","pro":False}]}, + {"name":"The Ophidian Lair (E3M5) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Ophidian Lair (E3M5) Main","pro":False}]}, + {"name":"The Ophidian Lair (E3M5) Green", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Ophidian Lair (E3M5) Main","pro":False}]}, + + # The Halls of Fear (E3M6) + {"name":"The Halls of Fear (E3M6) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"The Halls of Fear (E3M6) Yellow","pro":False}]}, + {"name":"The Halls of Fear (E3M6) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Halls of Fear (E3M6) Yellow","pro":False}, + {"target":"The Halls of Fear (E3M6) Cyan","pro":False}]}, + {"name":"The Halls of Fear (E3M6) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Halls of Fear (E3M6) Blue","pro":False}, + {"target":"The Halls of Fear (E3M6) Main","pro":False}, + {"target":"The Halls of Fear (E3M6) Green","pro":False}]}, + {"name":"The Halls of Fear (E3M6) Green", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Halls of Fear (E3M6) Yellow","pro":False}, + {"target":"The Halls of Fear (E3M6) Main","pro":False}, + {"target":"The Halls of Fear (E3M6) Cyan","pro":False}]}, + {"name":"The Halls of Fear (E3M6) Cyan", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Halls of Fear (E3M6) Yellow","pro":False}, + {"target":"The Halls of Fear (E3M6) Main","pro":False}]}, + + # The Chasm (E3M7) + {"name":"The Chasm (E3M7) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"The Chasm (E3M7) Yellow","pro":False}]}, + {"name":"The Chasm (E3M7) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[]}, + {"name":"The Chasm (E3M7) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Chasm (E3M7) Main","pro":False}, + {"target":"The Chasm (E3M7) Green","pro":False}, + {"target":"The Chasm (E3M7) Blue","pro":False}]}, + {"name":"The Chasm (E3M7) Green", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Chasm (E3M7) Yellow","pro":False}]}, + + # D'Sparil'S Keep (E3M8) + {"name":"D'Sparil'S Keep (E3M8) Main", + "connects_to_hub":True, + "episode":3, + "connections":[]}, + + # The Aquifier (E3M9) + {"name":"The Aquifier (E3M9) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"The Aquifier (E3M9) Yellow","pro":False}]}, + {"name":"The Aquifier (E3M9) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[]}, + {"name":"The Aquifier (E3M9) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Aquifier (E3M9) Green","pro":False}, + {"target":"The Aquifier (E3M9) Main","pro":False}]}, + {"name":"The Aquifier (E3M9) Green", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Aquifier (E3M9) Yellow","pro":False}, + {"target":"The Aquifier (E3M9) Main","pro":False}, + {"target":"The Aquifier (E3M9) Blue","pro":False}]}, + + # Catafalque (E4M1) + {"name":"Catafalque (E4M1) Main", + "connects_to_hub":True, + "episode":4, + "connections":[{"target":"Catafalque (E4M1) Yellow","pro":False}]}, + {"name":"Catafalque (E4M1) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Catafalque (E4M1) Green","pro":False}, + {"target":"Catafalque (E4M1) Main","pro":False}]}, + {"name":"Catafalque (E4M1) Green", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Catafalque (E4M1) Main","pro":False}]}, + + # Blockhouse (E4M2) + {"name":"Blockhouse (E4M2) Main", + "connects_to_hub":True, + "episode":4, + "connections":[ + {"target":"Blockhouse (E4M2) Yellow","pro":False}, + {"target":"Blockhouse (E4M2) Green","pro":False}, + {"target":"Blockhouse (E4M2) Blue","pro":False}]}, + {"name":"Blockhouse (E4M2) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Blockhouse (E4M2) Main","pro":False}, + {"target":"Blockhouse (E4M2) Balcony","pro":False}, + {"target":"Blockhouse (E4M2) Lake","pro":False}]}, + {"name":"Blockhouse (E4M2) Green", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Blockhouse (E4M2) Main","pro":False}]}, + {"name":"Blockhouse (E4M2) Blue", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Blockhouse (E4M2) Main","pro":False}]}, + {"name":"Blockhouse (E4M2) Lake", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Blockhouse (E4M2) Balcony","pro":False}]}, + {"name":"Blockhouse (E4M2) Balcony", + "connects_to_hub":False, + "episode":4, + "connections":[]}, + + # Ambulatory (E4M3) + {"name":"Ambulatory (E4M3) Main", + "connects_to_hub":True, + "episode":4, + "connections":[ + {"target":"Ambulatory (E4M3) Blue","pro":False}, + {"target":"Ambulatory (E4M3) Yellow","pro":False}, + {"target":"Ambulatory (E4M3) Green","pro":False}]}, + {"name":"Ambulatory (E4M3) Blue", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Ambulatory (E4M3) Yellow","pro":False}, + {"target":"Ambulatory (E4M3) Green","pro":False}]}, + {"name":"Ambulatory (E4M3) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Ambulatory (E4M3) Main","pro":False}]}, + {"name":"Ambulatory (E4M3) Green", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Ambulatory (E4M3) Main","pro":False}]}, + + # Sepulcher (E4M4) + {"name":"Sepulcher (E4M4) Main", + "connects_to_hub":True, + "episode":4, + "connections":[]}, + + # Great Stair (E4M5) + {"name":"Great Stair (E4M5) Main", + "connects_to_hub":True, + "episode":4, + "connections":[{"target":"Great Stair (E4M5) Yellow","pro":False}]}, + {"name":"Great Stair (E4M5) Blue", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Great Stair (E4M5) Green","pro":False}]}, + {"name":"Great Stair (E4M5) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Great Stair (E4M5) Main","pro":False}, + {"target":"Great Stair (E4M5) Green","pro":False}]}, + {"name":"Great Stair (E4M5) Green", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Great Stair (E4M5) Blue","pro":False}, + {"target":"Great Stair (E4M5) Yellow","pro":False}]}, + + # Halls of the Apostate (E4M6) + {"name":"Halls of the Apostate (E4M6) Main", + "connects_to_hub":True, + "episode":4, + "connections":[{"target":"Halls of the Apostate (E4M6) Yellow","pro":False}]}, + {"name":"Halls of the Apostate (E4M6) Blue", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Halls of the Apostate (E4M6) Green","pro":False}]}, + {"name":"Halls of the Apostate (E4M6) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Halls of the Apostate (E4M6) Main","pro":False}, + {"target":"Halls of the Apostate (E4M6) Green","pro":False}]}, + {"name":"Halls of the Apostate (E4M6) Green", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Halls of the Apostate (E4M6) Yellow","pro":False}, + {"target":"Halls of the Apostate (E4M6) Blue","pro":False}]}, + + # Ramparts of Perdition (E4M7) + {"name":"Ramparts of Perdition (E4M7) Main", + "connects_to_hub":True, + "episode":4, + "connections":[{"target":"Ramparts of Perdition (E4M7) Yellow","pro":False}]}, + {"name":"Ramparts of Perdition (E4M7) Blue", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Ramparts of Perdition (E4M7) Yellow","pro":False}]}, + {"name":"Ramparts of Perdition (E4M7) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Ramparts of Perdition (E4M7) Main","pro":False}, + {"target":"Ramparts of Perdition (E4M7) Green","pro":False}, + {"target":"Ramparts of Perdition (E4M7) Blue","pro":False}]}, + {"name":"Ramparts of Perdition (E4M7) Green", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Ramparts of Perdition (E4M7) Yellow","pro":False}]}, + + # Shattered Bridge (E4M8) + {"name":"Shattered Bridge (E4M8) Main", + "connects_to_hub":True, + "episode":4, + "connections":[{"target":"Shattered Bridge (E4M8) Yellow","pro":False}]}, + {"name":"Shattered Bridge (E4M8) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Shattered Bridge (E4M8) Main","pro":False}, + {"target":"Shattered Bridge (E4M8) Boss","pro":False}]}, + {"name":"Shattered Bridge (E4M8) Boss", + "connects_to_hub":False, + "episode":4, + "connections":[]}, + + # Mausoleum (E4M9) + {"name":"Mausoleum (E4M9) Main", + "connects_to_hub":True, + "episode":4, + "connections":[{"target":"Mausoleum (E4M9) Yellow","pro":False}]}, + {"name":"Mausoleum (E4M9) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Mausoleum (E4M9) Main","pro":False}]}, + + # Ochre Cliffs (E5M1) + {"name":"Ochre Cliffs (E5M1) Main", + "connects_to_hub":True, + "episode":5, + "connections":[{"target":"Ochre Cliffs (E5M1) Yellow","pro":False}]}, + {"name":"Ochre Cliffs (E5M1) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Ochre Cliffs (E5M1) Yellow","pro":False}]}, + {"name":"Ochre Cliffs (E5M1) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Ochre Cliffs (E5M1) Main","pro":False}, + {"target":"Ochre Cliffs (E5M1) Green","pro":False}, + {"target":"Ochre Cliffs (E5M1) Blue","pro":False}]}, + {"name":"Ochre Cliffs (E5M1) Green", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Ochre Cliffs (E5M1) Yellow","pro":False}]}, + + # Rapids (E5M2) + {"name":"Rapids (E5M2) Main", + "connects_to_hub":True, + "episode":5, + "connections":[{"target":"Rapids (E5M2) Yellow","pro":False}]}, + {"name":"Rapids (E5M2) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Rapids (E5M2) Main","pro":False}, + {"target":"Rapids (E5M2) Green","pro":False}]}, + {"name":"Rapids (E5M2) Green", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Rapids (E5M2) Yellow","pro":False}, + {"target":"Rapids (E5M2) Main","pro":False}]}, + + # Quay (E5M3) + {"name":"Quay (E5M3) Main", + "connects_to_hub":True, + "episode":5, + "connections":[ + {"target":"Quay (E5M3) Yellow","pro":False}, + {"target":"Quay (E5M3) Green","pro":False}, + {"target":"Quay (E5M3) Blue","pro":False}]}, + {"name":"Quay (E5M3) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Quay (E5M3) Green","pro":False}, + {"target":"Quay (E5M3) Main","pro":False}]}, + {"name":"Quay (E5M3) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Quay (E5M3) Main","pro":False}]}, + {"name":"Quay (E5M3) Green", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Quay (E5M3) Main","pro":False}, + {"target":"Quay (E5M3) Blue","pro":False}]}, + + # Courtyard (E5M4) + {"name":"Courtyard (E5M4) Main", + "connects_to_hub":True, + "episode":5, + "connections":[ + {"target":"Courtyard (E5M4) Kakis","pro":False}, + {"target":"Courtyard (E5M4) Blue","pro":False}]}, + {"name":"Courtyard (E5M4) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Courtyard (E5M4) Main","pro":False}]}, + {"name":"Courtyard (E5M4) Kakis", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Courtyard (E5M4) Main","pro":False}]}, + + # Hydratyr (E5M5) + {"name":"Hydratyr (E5M5) Main", + "connects_to_hub":True, + "episode":5, + "connections":[{"target":"Hydratyr (E5M5) Yellow","pro":False}]}, + {"name":"Hydratyr (E5M5) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Hydratyr (E5M5) Green","pro":False}]}, + {"name":"Hydratyr (E5M5) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Hydratyr (E5M5) Main","pro":False}, + {"target":"Hydratyr (E5M5) Green","pro":False}]}, + {"name":"Hydratyr (E5M5) Green", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Hydratyr (E5M5) Main","pro":False}, + {"target":"Hydratyr (E5M5) Yellow","pro":False}, + {"target":"Hydratyr (E5M5) Blue","pro":False}]}, + + # Colonnade (E5M6) + {"name":"Colonnade (E5M6) Main", + "connects_to_hub":True, + "episode":5, + "connections":[ + {"target":"Colonnade (E5M6) Yellow","pro":False}, + {"target":"Colonnade (E5M6) Blue","pro":False}]}, + {"name":"Colonnade (E5M6) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Colonnade (E5M6) Main","pro":False}]}, + {"name":"Colonnade (E5M6) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Colonnade (E5M6) Main","pro":False}, + {"target":"Colonnade (E5M6) Green","pro":False}]}, + {"name":"Colonnade (E5M6) Green", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Colonnade (E5M6) Yellow","pro":False}]}, + + # Foetid Manse (E5M7) + {"name":"Foetid Manse (E5M7) Main", + "connects_to_hub":True, + "episode":5, + "connections":[{"target":"Foetid Manse (E5M7) Yellow","pro":False}]}, + {"name":"Foetid Manse (E5M7) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Foetid Manse (E5M7) Yellow","pro":False}]}, + {"name":"Foetid Manse (E5M7) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Foetid Manse (E5M7) Main","pro":False}, + {"target":"Foetid Manse (E5M7) Green","pro":False}, + {"target":"Foetid Manse (E5M7) Blue","pro":False}]}, + {"name":"Foetid Manse (E5M7) Green", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Foetid Manse (E5M7) Yellow","pro":False}, + {"target":"Foetid Manse (E5M7) Main","pro":False}]}, + + # Field of Judgement (E5M8) + {"name":"Field of Judgement (E5M8) Main", + "connects_to_hub":True, + "episode":5, + "connections":[]}, + + # Skein of D'Sparil (E5M9) + {"name":"Skein of D'Sparil (E5M9) Main", + "connects_to_hub":True, + "episode":5, + "connections":[ + {"target":"Skein of D'Sparil (E5M9) Blue","pro":False}, + {"target":"Skein of D'Sparil (E5M9) Yellow","pro":False}, + {"target":"Skein of D'Sparil (E5M9) Green","pro":False}]}, + {"name":"Skein of D'Sparil (E5M9) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Skein of D'Sparil (E5M9) Main","pro":False}]}, + {"name":"Skein of D'Sparil (E5M9) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Skein of D'Sparil (E5M9) Main","pro":False}]}, + {"name":"Skein of D'Sparil (E5M9) Green", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Skein of D'Sparil (E5M9) Main","pro":False}]}, +] diff --git a/worlds/heretic/Rules.py b/worlds/heretic/Rules.py new file mode 100644 index 000000000000..7ef15d7920dd --- /dev/null +++ b/worlds/heretic/Rules.py @@ -0,0 +1,736 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import TYPE_CHECKING +from worlds.generic.Rules import set_rule + +if TYPE_CHECKING: + from . import HereticWorld + + +def set_episode1_rules(player, world, pro): + # The Docks (E1M1) + set_rule(world.get_entrance("Hub -> The Docks (E1M1) Main", player), lambda state: + state.has("The Docks (E1M1)", player, 1)) + set_rule(world.get_entrance("The Docks (E1M1) Main -> The Docks (E1M1) Yellow", player), lambda state: + state.has("The Docks (E1M1) - Yellow key", player, 1)) + + # The Dungeons (E1M2) + set_rule(world.get_entrance("Hub -> The Dungeons (E1M2) Main", player), lambda state: + (state.has("The Dungeons (E1M2)", player, 1)) and + (state.has("Dragon Claw", player, 1) or + state.has("Ethereal Crossbow", player, 1))) + set_rule(world.get_entrance("The Dungeons (E1M2) Main -> The Dungeons (E1M2) Yellow", player), lambda state: + state.has("The Dungeons (E1M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Dungeons (E1M2) Main -> The Dungeons (E1M2) Green", player), lambda state: + state.has("The Dungeons (E1M2) - Green key", player, 1)) + set_rule(world.get_entrance("The Dungeons (E1M2) Blue -> The Dungeons (E1M2) Yellow", player), lambda state: + state.has("The Dungeons (E1M2) - Blue key", player, 1)) + set_rule(world.get_entrance("The Dungeons (E1M2) Yellow -> The Dungeons (E1M2) Blue", player), lambda state: + state.has("The Dungeons (E1M2) - Blue key", player, 1)) + + # The Gatehouse (E1M3) + set_rule(world.get_entrance("Hub -> The Gatehouse (E1M3) Main", player), lambda state: + (state.has("The Gatehouse (E1M3)", player, 1)) and + (state.has("Ethereal Crossbow", player, 1) or + state.has("Dragon Claw", player, 1))) + set_rule(world.get_entrance("The Gatehouse (E1M3) Main -> The Gatehouse (E1M3) Yellow", player), lambda state: + state.has("The Gatehouse (E1M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Gatehouse (E1M3) Main -> The Gatehouse (E1M3) Sea", player), lambda state: + state.has("The Gatehouse (E1M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Gatehouse (E1M3) Main -> The Gatehouse (E1M3) Green", player), lambda state: + state.has("The Gatehouse (E1M3) - Green key", player, 1)) + set_rule(world.get_entrance("The Gatehouse (E1M3) Green -> The Gatehouse (E1M3) Main", player), lambda state: + state.has("The Gatehouse (E1M3) - Green key", player, 1)) + + # The Guard Tower (E1M4) + set_rule(world.get_entrance("Hub -> The Guard Tower (E1M4) Main", player), lambda state: + (state.has("The Guard Tower (E1M4)", player, 1)) and + (state.has("Ethereal Crossbow", player, 1) or + state.has("Dragon Claw", player, 1))) + set_rule(world.get_entrance("The Guard Tower (E1M4) Main -> The Guard Tower (E1M4) Yellow", player), lambda state: + state.has("The Guard Tower (E1M4) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Guard Tower (E1M4) Yellow -> The Guard Tower (E1M4) Green", player), lambda state: + state.has("The Guard Tower (E1M4) - Green key", player, 1)) + set_rule(world.get_entrance("The Guard Tower (E1M4) Green -> The Guard Tower (E1M4) Yellow", player), lambda state: + state.has("The Guard Tower (E1M4) - Green key", player, 1)) + + # The Citadel (E1M5) + set_rule(world.get_entrance("Hub -> The Citadel (E1M5) Main", player), lambda state: + (state.has("The Citadel (E1M5)", player, 1) and + state.has("Ethereal Crossbow", player, 1)) and + (state.has("Dragon Claw", player, 1) or + state.has("Gauntlets of the Necromancer", player, 1))) + set_rule(world.get_entrance("The Citadel (E1M5) Main -> The Citadel (E1M5) Yellow", player), lambda state: + state.has("The Citadel (E1M5) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Citadel (E1M5) Blue -> The Citadel (E1M5) Green", player), lambda state: + state.has("The Citadel (E1M5) - Blue key", player, 1)) + set_rule(world.get_entrance("The Citadel (E1M5) Yellow -> The Citadel (E1M5) Green", player), lambda state: + state.has("The Citadel (E1M5) - Green key", player, 1)) + set_rule(world.get_entrance("The Citadel (E1M5) Green -> The Citadel (E1M5) Blue", player), lambda state: + state.has("The Citadel (E1M5) - Blue key", player, 1)) + + # The Cathedral (E1M6) + set_rule(world.get_entrance("Hub -> The Cathedral (E1M6) Main", player), lambda state: + (state.has("The Cathedral (E1M6)", player, 1) and + state.has("Ethereal Crossbow", player, 1)) and + (state.has("Gauntlets of the Necromancer", player, 1) or + state.has("Dragon Claw", player, 1))) + set_rule(world.get_entrance("The Cathedral (E1M6) Main -> The Cathedral (E1M6) Yellow", player), lambda state: + state.has("The Cathedral (E1M6) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Cathedral (E1M6) Yellow -> The Cathedral (E1M6) Green", player), lambda state: + state.has("The Cathedral (E1M6) - Green key", player, 1)) + + # The Crypts (E1M7) + set_rule(world.get_entrance("Hub -> The Crypts (E1M7) Main", player), lambda state: + (state.has("The Crypts (E1M7)", player, 1) and + state.has("Ethereal Crossbow", player, 1)) and + (state.has("Gauntlets of the Necromancer", player, 1) or + state.has("Dragon Claw", player, 1))) + set_rule(world.get_entrance("The Crypts (E1M7) Main -> The Crypts (E1M7) Yellow", player), lambda state: + state.has("The Crypts (E1M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Crypts (E1M7) Main -> The Crypts (E1M7) Green", player), lambda state: + state.has("The Crypts (E1M7) - Green key", player, 1)) + set_rule(world.get_entrance("The Crypts (E1M7) Yellow -> The Crypts (E1M7) Green", player), lambda state: + state.has("The Crypts (E1M7) - Green key", player, 1)) + set_rule(world.get_entrance("The Crypts (E1M7) Yellow -> The Crypts (E1M7) Blue", player), lambda state: + state.has("The Crypts (E1M7) - Blue key", player, 1)) + set_rule(world.get_entrance("The Crypts (E1M7) Green -> The Crypts (E1M7) Main", player), lambda state: + state.has("The Crypts (E1M7) - Green key", player, 1)) + + # Hell's Maw (E1M8) + set_rule(world.get_entrance("Hub -> Hell's Maw (E1M8) Main", player), lambda state: + state.has("Hell's Maw (E1M8)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) + + # The Graveyard (E1M9) + set_rule(world.get_entrance("Hub -> The Graveyard (E1M9) Main", player), lambda state: + state.has("The Graveyard (E1M9)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) + set_rule(world.get_entrance("The Graveyard (E1M9) Main -> The Graveyard (E1M9) Yellow", player), lambda state: + state.has("The Graveyard (E1M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Graveyard (E1M9) Main -> The Graveyard (E1M9) Green", player), lambda state: + state.has("The Graveyard (E1M9) - Green key", player, 1)) + set_rule(world.get_entrance("The Graveyard (E1M9) Main -> The Graveyard (E1M9) Blue", player), lambda state: + state.has("The Graveyard (E1M9) - Blue key", player, 1)) + set_rule(world.get_entrance("The Graveyard (E1M9) Yellow -> The Graveyard (E1M9) Main", player), lambda state: + state.has("The Graveyard (E1M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Graveyard (E1M9) Green -> The Graveyard (E1M9) Main", player), lambda state: + state.has("The Graveyard (E1M9) - Green key", player, 1)) + + +def set_episode2_rules(player, world, pro): + # The Crater (E2M1) + set_rule(world.get_entrance("Hub -> The Crater (E2M1) Main", player), lambda state: + state.has("The Crater (E2M1)", player, 1)) + set_rule(world.get_entrance("The Crater (E2M1) Main -> The Crater (E2M1) Yellow", player), lambda state: + state.has("The Crater (E2M1) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Crater (E2M1) Yellow -> The Crater (E2M1) Green", player), lambda state: + state.has("The Crater (E2M1) - Green key", player, 1)) + set_rule(world.get_entrance("The Crater (E2M1) Green -> The Crater (E2M1) Yellow", player), lambda state: + state.has("The Crater (E2M1) - Green key", player, 1)) + + # The Lava Pits (E2M2) + set_rule(world.get_entrance("Hub -> The Lava Pits (E2M2) Main", player), lambda state: + (state.has("The Lava Pits (E2M2)", player, 1)) and + (state.has("Ethereal Crossbow", player, 1) or + state.has("Dragon Claw", player, 1))) + set_rule(world.get_entrance("The Lava Pits (E2M2) Main -> The Lava Pits (E2M2) Yellow", player), lambda state: + state.has("The Lava Pits (E2M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Lava Pits (E2M2) Yellow -> The Lava Pits (E2M2) Green", player), lambda state: + state.has("The Lava Pits (E2M2) - Green key", player, 1)) + set_rule(world.get_entrance("The Lava Pits (E2M2) Yellow -> The Lava Pits (E2M2) Main", player), lambda state: + state.has("The Lava Pits (E2M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Lava Pits (E2M2) Green -> The Lava Pits (E2M2) Yellow", player), lambda state: + state.has("The Lava Pits (E2M2) - Green key", player, 1)) + + # The River of Fire (E2M3) + set_rule(world.get_entrance("Hub -> The River of Fire (E2M3) Main", player), lambda state: + state.has("The River of Fire (E2M3)", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Ethereal Crossbow", player, 1)) + set_rule(world.get_entrance("The River of Fire (E2M3) Main -> The River of Fire (E2M3) Yellow", player), lambda state: + state.has("The River of Fire (E2M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("The River of Fire (E2M3) Main -> The River of Fire (E2M3) Blue", player), lambda state: + state.has("The River of Fire (E2M3) - Blue key", player, 1)) + set_rule(world.get_entrance("The River of Fire (E2M3) Main -> The River of Fire (E2M3) Green", player), lambda state: + state.has("The River of Fire (E2M3) - Green key", player, 1)) + set_rule(world.get_entrance("The River of Fire (E2M3) Blue -> The River of Fire (E2M3) Main", player), lambda state: + state.has("The River of Fire (E2M3) - Blue key", player, 1)) + set_rule(world.get_entrance("The River of Fire (E2M3) Yellow -> The River of Fire (E2M3) Main", player), lambda state: + state.has("The River of Fire (E2M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("The River of Fire (E2M3) Green -> The River of Fire (E2M3) Main", player), lambda state: + state.has("The River of Fire (E2M3) - Green key", player, 1)) + + # The Ice Grotto (E2M4) + set_rule(world.get_entrance("Hub -> The Ice Grotto (E2M4) Main", player), lambda state: + (state.has("The Ice Grotto (E2M4)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) and + (state.has("Hellstaff", player, 1) or + state.has("Firemace", player, 1))) + set_rule(world.get_entrance("The Ice Grotto (E2M4) Main -> The Ice Grotto (E2M4) Green", player), lambda state: + state.has("The Ice Grotto (E2M4) - Green key", player, 1)) + set_rule(world.get_entrance("The Ice Grotto (E2M4) Main -> The Ice Grotto (E2M4) Yellow", player), lambda state: + state.has("The Ice Grotto (E2M4) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Ice Grotto (E2M4) Blue -> The Ice Grotto (E2M4) Green", player), lambda state: + state.has("The Ice Grotto (E2M4) - Blue key", player, 1)) + set_rule(world.get_entrance("The Ice Grotto (E2M4) Yellow -> The Ice Grotto (E2M4) Magenta", player), lambda state: + state.has("The Ice Grotto (E2M4) - Green key", player, 1) and + state.has("The Ice Grotto (E2M4) - Blue key", player, 1)) + set_rule(world.get_entrance("The Ice Grotto (E2M4) Green -> The Ice Grotto (E2M4) Blue", player), lambda state: + state.has("The Ice Grotto (E2M4) - Blue key", player, 1)) + + # The Catacombs (E2M5) + set_rule(world.get_entrance("Hub -> The Catacombs (E2M5) Main", player), lambda state: + (state.has("The Catacombs (E2M5)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("The Catacombs (E2M5) Main -> The Catacombs (E2M5) Yellow", player), lambda state: + state.has("The Catacombs (E2M5) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Catacombs (E2M5) Blue -> The Catacombs (E2M5) Green", player), lambda state: + state.has("The Catacombs (E2M5) - Blue key", player, 1)) + set_rule(world.get_entrance("The Catacombs (E2M5) Yellow -> The Catacombs (E2M5) Green", player), lambda state: + state.has("The Catacombs (E2M5) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Catacombs (E2M5) Green -> The Catacombs (E2M5) Blue", player), lambda state: + state.has("The Catacombs (E2M5) - Blue key", player, 1)) + + # The Labyrinth (E2M6) + set_rule(world.get_entrance("Hub -> The Labyrinth (E2M6) Main", player), lambda state: + (state.has("The Labyrinth (E2M6)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("The Labyrinth (E2M6) Main -> The Labyrinth (E2M6) Blue", player), lambda state: + state.has("The Labyrinth (E2M6) - Blue key", player, 1)) + set_rule(world.get_entrance("The Labyrinth (E2M6) Main -> The Labyrinth (E2M6) Yellow", player), lambda state: + state.has("The Labyrinth (E2M6) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Labyrinth (E2M6) Main -> The Labyrinth (E2M6) Green", player), lambda state: + state.has("The Labyrinth (E2M6) - Green key", player, 1)) + set_rule(world.get_entrance("The Labyrinth (E2M6) Blue -> The Labyrinth (E2M6) Main", player), lambda state: + state.has("The Labyrinth (E2M6) - Blue key", player, 1)) + + # The Great Hall (E2M7) + set_rule(world.get_entrance("Hub -> The Great Hall (E2M7) Main", player), lambda state: + (state.has("The Great Hall (E2M7)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("The Great Hall (E2M7) Main -> The Great Hall (E2M7) Yellow", player), lambda state: + state.has("The Great Hall (E2M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Great Hall (E2M7) Main -> The Great Hall (E2M7) Green", player), lambda state: + state.has("The Great Hall (E2M7) - Green key", player, 1)) + set_rule(world.get_entrance("The Great Hall (E2M7) Blue -> The Great Hall (E2M7) Yellow", player), lambda state: + state.has("The Great Hall (E2M7) - Blue key", player, 1)) + set_rule(world.get_entrance("The Great Hall (E2M7) Yellow -> The Great Hall (E2M7) Blue", player), lambda state: + state.has("The Great Hall (E2M7) - Blue key", player, 1)) + set_rule(world.get_entrance("The Great Hall (E2M7) Yellow -> The Great Hall (E2M7) Main", player), lambda state: + state.has("The Great Hall (E2M7) - Yellow key", player, 1)) + + # The Portals of Chaos (E2M8) + set_rule(world.get_entrance("Hub -> The Portals of Chaos (E2M8) Main", player), lambda state: + state.has("The Portals of Chaos (E2M8)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Phoenix Rod", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1)) + + # The Glacier (E2M9) + set_rule(world.get_entrance("Hub -> The Glacier (E2M9) Main", player), lambda state: + (state.has("The Glacier (E2M9)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("The Glacier (E2M9) Main -> The Glacier (E2M9) Yellow", player), lambda state: + state.has("The Glacier (E2M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Glacier (E2M9) Main -> The Glacier (E2M9) Blue", player), lambda state: + state.has("The Glacier (E2M9) - Blue key", player, 1)) + set_rule(world.get_entrance("The Glacier (E2M9) Main -> The Glacier (E2M9) Green", player), lambda state: + state.has("The Glacier (E2M9) - Green key", player, 1)) + set_rule(world.get_entrance("The Glacier (E2M9) Blue -> The Glacier (E2M9) Main", player), lambda state: + state.has("The Glacier (E2M9) - Blue key", player, 1)) + set_rule(world.get_entrance("The Glacier (E2M9) Yellow -> The Glacier (E2M9) Main", player), lambda state: + state.has("The Glacier (E2M9) - Yellow key", player, 1)) + + +def set_episode3_rules(player, world, pro): + # The Storehouse (E3M1) + set_rule(world.get_entrance("Hub -> The Storehouse (E3M1) Main", player), lambda state: + state.has("The Storehouse (E3M1)", player, 1)) + set_rule(world.get_entrance("The Storehouse (E3M1) Main -> The Storehouse (E3M1) Yellow", player), lambda state: + state.has("The Storehouse (E3M1) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Storehouse (E3M1) Main -> The Storehouse (E3M1) Green", player), lambda state: + state.has("The Storehouse (E3M1) - Green key", player, 1)) + set_rule(world.get_entrance("The Storehouse (E3M1) Yellow -> The Storehouse (E3M1) Main", player), lambda state: + state.has("The Storehouse (E3M1) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Storehouse (E3M1) Green -> The Storehouse (E3M1) Main", player), lambda state: + state.has("The Storehouse (E3M1) - Green key", player, 1)) + + # The Cesspool (E3M2) + set_rule(world.get_entrance("Hub -> The Cesspool (E3M2) Main", player), lambda state: + state.has("The Cesspool (E3M2)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1)) + set_rule(world.get_entrance("The Cesspool (E3M2) Main -> The Cesspool (E3M2) Yellow", player), lambda state: + state.has("The Cesspool (E3M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Cesspool (E3M2) Blue -> The Cesspool (E3M2) Green", player), lambda state: + state.has("The Cesspool (E3M2) - Blue key", player, 1)) + set_rule(world.get_entrance("The Cesspool (E3M2) Yellow -> The Cesspool (E3M2) Green", player), lambda state: + state.has("The Cesspool (E3M2) - Green key", player, 1)) + set_rule(world.get_entrance("The Cesspool (E3M2) Green -> The Cesspool (E3M2) Blue", player), lambda state: + state.has("The Cesspool (E3M2) - Blue key", player, 1)) + set_rule(world.get_entrance("The Cesspool (E3M2) Green -> The Cesspool (E3M2) Yellow", player), lambda state: + state.has("The Cesspool (E3M2) - Green key", player, 1)) + + # The Confluence (E3M3) + set_rule(world.get_entrance("Hub -> The Confluence (E3M3) Main", player), lambda state: + (state.has("The Confluence (E3M3)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) and + (state.has("Gauntlets of the Necromancer", player, 1) or + state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("The Confluence (E3M3) Main -> The Confluence (E3M3) Green", player), lambda state: + state.has("The Confluence (E3M3) - Green key", player, 1)) + set_rule(world.get_entrance("The Confluence (E3M3) Main -> The Confluence (E3M3) Yellow", player), lambda state: + state.has("The Confluence (E3M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Confluence (E3M3) Blue -> The Confluence (E3M3) Green", player), lambda state: + state.has("The Confluence (E3M3) - Blue key", player, 1)) + set_rule(world.get_entrance("The Confluence (E3M3) Green -> The Confluence (E3M3) Main", player), lambda state: + state.has("The Confluence (E3M3) - Green key", player, 1)) + set_rule(world.get_entrance("The Confluence (E3M3) Green -> The Confluence (E3M3) Blue", player), lambda state: + state.has("The Confluence (E3M3) - Blue key", player, 1)) + + # The Azure Fortress (E3M4) + set_rule(world.get_entrance("Hub -> The Azure Fortress (E3M4) Main", player), lambda state: + (state.has("The Azure Fortress (E3M4)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Hellstaff", player, 1)) and + (state.has("Firemace", player, 1) or + state.has("Phoenix Rod", player, 1) or + state.has("Gauntlets of the Necromancer", player, 1))) + set_rule(world.get_entrance("The Azure Fortress (E3M4) Main -> The Azure Fortress (E3M4) Green", player), lambda state: + state.has("The Azure Fortress (E3M4) - Green key", player, 1)) + set_rule(world.get_entrance("The Azure Fortress (E3M4) Main -> The Azure Fortress (E3M4) Yellow", player), lambda state: + state.has("The Azure Fortress (E3M4) - Yellow key", player, 1)) + + # The Ophidian Lair (E3M5) + set_rule(world.get_entrance("Hub -> The Ophidian Lair (E3M5) Main", player), lambda state: + (state.has("The Ophidian Lair (E3M5)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Hellstaff", player, 1)) and + (state.has("Gauntlets of the Necromancer", player, 1) or + state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1))) + set_rule(world.get_entrance("The Ophidian Lair (E3M5) Main -> The Ophidian Lair (E3M5) Yellow", player), lambda state: + state.has("The Ophidian Lair (E3M5) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Ophidian Lair (E3M5) Main -> The Ophidian Lair (E3M5) Green", player), lambda state: + state.has("The Ophidian Lair (E3M5) - Green key", player, 1)) + + # The Halls of Fear (E3M6) + set_rule(world.get_entrance("Hub -> The Halls of Fear (E3M6) Main", player), lambda state: + (state.has("The Halls of Fear (E3M6)", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Ethereal Crossbow", player, 1)) and + (state.has("Gauntlets of the Necromancer", player, 1) or + state.has("Phoenix Rod", player, 1))) + set_rule(world.get_entrance("The Halls of Fear (E3M6) Main -> The Halls of Fear (E3M6) Yellow", player), lambda state: + state.has("The Halls of Fear (E3M6) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Halls of Fear (E3M6) Blue -> The Halls of Fear (E3M6) Yellow", player), lambda state: + state.has("The Halls of Fear (E3M6) - Blue key", player, 1)) + set_rule(world.get_entrance("The Halls of Fear (E3M6) Yellow -> The Halls of Fear (E3M6) Blue", player), lambda state: + state.has("The Halls of Fear (E3M6) - Blue key", player, 1)) + set_rule(world.get_entrance("The Halls of Fear (E3M6) Yellow -> The Halls of Fear (E3M6) Green", player), lambda state: + state.has("The Halls of Fear (E3M6) - Green key", player, 1)) + + # The Chasm (E3M7) + set_rule(world.get_entrance("Hub -> The Chasm (E3M7) Main", player), lambda state: + (state.has("The Chasm (E3M7)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1)) and + (state.has("Gauntlets of the Necromancer", player, 1) or + state.has("Phoenix Rod", player, 1))) + set_rule(world.get_entrance("The Chasm (E3M7) Main -> The Chasm (E3M7) Yellow", player), lambda state: + state.has("The Chasm (E3M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Chasm (E3M7) Yellow -> The Chasm (E3M7) Main", player), lambda state: + state.has("The Chasm (E3M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Chasm (E3M7) Yellow -> The Chasm (E3M7) Green", player), lambda state: + state.has("The Chasm (E3M7) - Green key", player, 1)) + set_rule(world.get_entrance("The Chasm (E3M7) Yellow -> The Chasm (E3M7) Blue", player), lambda state: + state.has("The Chasm (E3M7) - Blue key", player, 1)) + set_rule(world.get_entrance("The Chasm (E3M7) Green -> The Chasm (E3M7) Yellow", player), lambda state: + state.has("The Chasm (E3M7) - Green key", player, 1)) + + # D'Sparil'S Keep (E3M8) + set_rule(world.get_entrance("Hub -> D'Sparil'S Keep (E3M8) Main", player), lambda state: + state.has("D'Sparil'S Keep (E3M8)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Phoenix Rod", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1)) + + # The Aquifier (E3M9) + set_rule(world.get_entrance("Hub -> The Aquifier (E3M9) Main", player), lambda state: + state.has("The Aquifier (E3M9)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Phoenix Rod", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1)) + set_rule(world.get_entrance("The Aquifier (E3M9) Main -> The Aquifier (E3M9) Yellow", player), lambda state: + state.has("The Aquifier (E3M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Aquifier (E3M9) Yellow -> The Aquifier (E3M9) Green", player), lambda state: + state.has("The Aquifier (E3M9) - Green key", player, 1)) + set_rule(world.get_entrance("The Aquifier (E3M9) Yellow -> The Aquifier (E3M9) Main", player), lambda state: + state.has("The Aquifier (E3M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Aquifier (E3M9) Green -> The Aquifier (E3M9) Yellow", player), lambda state: + state.has("The Aquifier (E3M9) - Green key", player, 1)) + + +def set_episode4_rules(player, world, pro): + # Catafalque (E4M1) + set_rule(world.get_entrance("Hub -> Catafalque (E4M1) Main", player), lambda state: + state.has("Catafalque (E4M1)", player, 1)) + set_rule(world.get_entrance("Catafalque (E4M1) Main -> Catafalque (E4M1) Yellow", player), lambda state: + state.has("Catafalque (E4M1) - Yellow key", player, 1)) + set_rule(world.get_entrance("Catafalque (E4M1) Yellow -> Catafalque (E4M1) Green", player), lambda state: + (state.has("Catafalque (E4M1) - Green key", player, 1)) and (state.has("Ethereal Crossbow", player, 1) or + state.has("Dragon Claw", player, 1) or + state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1) or + state.has("Hellstaff", player, 1))) + + # Blockhouse (E4M2) + set_rule(world.get_entrance("Hub -> Blockhouse (E4M2) Main", player), lambda state: + state.has("Blockhouse (E4M2)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) + set_rule(world.get_entrance("Blockhouse (E4M2) Main -> Blockhouse (E4M2) Yellow", player), lambda state: + state.has("Blockhouse (E4M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("Blockhouse (E4M2) Main -> Blockhouse (E4M2) Green", player), lambda state: + state.has("Blockhouse (E4M2) - Green key", player, 1)) + set_rule(world.get_entrance("Blockhouse (E4M2) Main -> Blockhouse (E4M2) Blue", player), lambda state: + state.has("Blockhouse (E4M2) - Blue key", player, 1)) + set_rule(world.get_entrance("Blockhouse (E4M2) Green -> Blockhouse (E4M2) Main", player), lambda state: + state.has("Blockhouse (E4M2) - Green key", player, 1)) + set_rule(world.get_entrance("Blockhouse (E4M2) Blue -> Blockhouse (E4M2) Main", player), lambda state: + state.has("Blockhouse (E4M2) - Blue key", player, 1)) + + # Ambulatory (E4M3) + set_rule(world.get_entrance("Hub -> Ambulatory (E4M3) Main", player), lambda state: + (state.has("Ambulatory (E4M3)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Ambulatory (E4M3) Main -> Ambulatory (E4M3) Blue", player), lambda state: + state.has("Ambulatory (E4M3) - Blue key", player, 1)) + set_rule(world.get_entrance("Ambulatory (E4M3) Main -> Ambulatory (E4M3) Yellow", player), lambda state: + state.has("Ambulatory (E4M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("Ambulatory (E4M3) Main -> Ambulatory (E4M3) Green", player), lambda state: + state.has("Ambulatory (E4M3) - Green key", player, 1)) + + # Sepulcher (E4M4) + set_rule(world.get_entrance("Hub -> Sepulcher (E4M4) Main", player), lambda state: + (state.has("Sepulcher (E4M4)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + + # Great Stair (E4M5) + set_rule(world.get_entrance("Hub -> Great Stair (E4M5) Main", player), lambda state: + (state.has("Great Stair (E4M5)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Hellstaff", player, 1) or + state.has("Phoenix Rod", player, 1))) + set_rule(world.get_entrance("Great Stair (E4M5) Main -> Great Stair (E4M5) Yellow", player), lambda state: + state.has("Great Stair (E4M5) - Yellow key", player, 1)) + set_rule(world.get_entrance("Great Stair (E4M5) Blue -> Great Stair (E4M5) Green", player), lambda state: + state.has("Great Stair (E4M5) - Blue key", player, 1)) + set_rule(world.get_entrance("Great Stair (E4M5) Yellow -> Great Stair (E4M5) Green", player), lambda state: + state.has("Great Stair (E4M5) - Green key", player, 1)) + set_rule(world.get_entrance("Great Stair (E4M5) Green -> Great Stair (E4M5) Blue", player), lambda state: + state.has("Great Stair (E4M5) - Blue key", player, 1)) + set_rule(world.get_entrance("Great Stair (E4M5) Green -> Great Stair (E4M5) Yellow", player), lambda state: + state.has("Great Stair (E4M5) - Green key", player, 1)) + + # Halls of the Apostate (E4M6) + set_rule(world.get_entrance("Hub -> Halls of the Apostate (E4M6) Main", player), lambda state: + (state.has("Halls of the Apostate (E4M6)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Halls of the Apostate (E4M6) Main -> Halls of the Apostate (E4M6) Yellow", player), lambda state: + state.has("Halls of the Apostate (E4M6) - Yellow key", player, 1)) + set_rule(world.get_entrance("Halls of the Apostate (E4M6) Blue -> Halls of the Apostate (E4M6) Green", player), lambda state: + state.has("Halls of the Apostate (E4M6) - Blue key", player, 1)) + set_rule(world.get_entrance("Halls of the Apostate (E4M6) Yellow -> Halls of the Apostate (E4M6) Green", player), lambda state: + state.has("Halls of the Apostate (E4M6) - Green key", player, 1)) + set_rule(world.get_entrance("Halls of the Apostate (E4M6) Green -> Halls of the Apostate (E4M6) Yellow", player), lambda state: + state.has("Halls of the Apostate (E4M6) - Green key", player, 1)) + set_rule(world.get_entrance("Halls of the Apostate (E4M6) Green -> Halls of the Apostate (E4M6) Blue", player), lambda state: + state.has("Halls of the Apostate (E4M6) - Blue key", player, 1)) + + # Ramparts of Perdition (E4M7) + set_rule(world.get_entrance("Hub -> Ramparts of Perdition (E4M7) Main", player), lambda state: + (state.has("Ramparts of Perdition (E4M7)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Ramparts of Perdition (E4M7) Main -> Ramparts of Perdition (E4M7) Yellow", player), lambda state: + state.has("Ramparts of Perdition (E4M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("Ramparts of Perdition (E4M7) Blue -> Ramparts of Perdition (E4M7) Yellow", player), lambda state: + state.has("Ramparts of Perdition (E4M7) - Blue key", player, 1)) + set_rule(world.get_entrance("Ramparts of Perdition (E4M7) Yellow -> Ramparts of Perdition (E4M7) Main", player), lambda state: + state.has("Ramparts of Perdition (E4M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("Ramparts of Perdition (E4M7) Yellow -> Ramparts of Perdition (E4M7) Green", player), lambda state: + state.has("Ramparts of Perdition (E4M7) - Green key", player, 1)) + set_rule(world.get_entrance("Ramparts of Perdition (E4M7) Yellow -> Ramparts of Perdition (E4M7) Blue", player), lambda state: + state.has("Ramparts of Perdition (E4M7) - Blue key", player, 1)) + set_rule(world.get_entrance("Ramparts of Perdition (E4M7) Green -> Ramparts of Perdition (E4M7) Yellow", player), lambda state: + state.has("Ramparts of Perdition (E4M7) - Green key", player, 1)) + + # Shattered Bridge (E4M8) + set_rule(world.get_entrance("Hub -> Shattered Bridge (E4M8) Main", player), lambda state: + state.has("Shattered Bridge (E4M8)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Phoenix Rod", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1)) + set_rule(world.get_entrance("Shattered Bridge (E4M8) Main -> Shattered Bridge (E4M8) Yellow", player), lambda state: + state.has("Shattered Bridge (E4M8) - Yellow key", player, 1)) + set_rule(world.get_entrance("Shattered Bridge (E4M8) Yellow -> Shattered Bridge (E4M8) Main", player), lambda state: + state.has("Shattered Bridge (E4M8) - Yellow key", player, 1)) + + # Mausoleum (E4M9) + set_rule(world.get_entrance("Hub -> Mausoleum (E4M9) Main", player), lambda state: + (state.has("Mausoleum (E4M9)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Mausoleum (E4M9) Main -> Mausoleum (E4M9) Yellow", player), lambda state: + state.has("Mausoleum (E4M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("Mausoleum (E4M9) Yellow -> Mausoleum (E4M9) Main", player), lambda state: + state.has("Mausoleum (E4M9) - Yellow key", player, 1)) + + +def set_episode5_rules(player, world, pro): + # Ochre Cliffs (E5M1) + set_rule(world.get_entrance("Hub -> Ochre Cliffs (E5M1) Main", player), lambda state: + state.has("Ochre Cliffs (E5M1)", player, 1)) + set_rule(world.get_entrance("Ochre Cliffs (E5M1) Main -> Ochre Cliffs (E5M1) Yellow", player), lambda state: + state.has("Ochre Cliffs (E5M1) - Yellow key", player, 1)) + set_rule(world.get_entrance("Ochre Cliffs (E5M1) Blue -> Ochre Cliffs (E5M1) Yellow", player), lambda state: + state.has("Ochre Cliffs (E5M1) - Blue key", player, 1)) + set_rule(world.get_entrance("Ochre Cliffs (E5M1) Yellow -> Ochre Cliffs (E5M1) Main", player), lambda state: + state.has("Ochre Cliffs (E5M1) - Yellow key", player, 1)) + set_rule(world.get_entrance("Ochre Cliffs (E5M1) Yellow -> Ochre Cliffs (E5M1) Green", player), lambda state: + state.has("Ochre Cliffs (E5M1) - Green key", player, 1)) + set_rule(world.get_entrance("Ochre Cliffs (E5M1) Yellow -> Ochre Cliffs (E5M1) Blue", player), lambda state: + state.has("Ochre Cliffs (E5M1) - Blue key", player, 1)) + set_rule(world.get_entrance("Ochre Cliffs (E5M1) Green -> Ochre Cliffs (E5M1) Yellow", player), lambda state: + state.has("Ochre Cliffs (E5M1) - Green key", player, 1)) + + # Rapids (E5M2) + set_rule(world.get_entrance("Hub -> Rapids (E5M2) Main", player), lambda state: + state.has("Rapids (E5M2)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) + set_rule(world.get_entrance("Rapids (E5M2) Main -> Rapids (E5M2) Yellow", player), lambda state: + state.has("Rapids (E5M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("Rapids (E5M2) Yellow -> Rapids (E5M2) Main", player), lambda state: + state.has("Rapids (E5M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("Rapids (E5M2) Yellow -> Rapids (E5M2) Green", player), lambda state: + state.has("Rapids (E5M2) - Green key", player, 1)) + + # Quay (E5M3) + set_rule(world.get_entrance("Hub -> Quay (E5M3) Main", player), lambda state: + (state.has("Quay (E5M3)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1) or + state.has("Firemace", player, 1))) + set_rule(world.get_entrance("Quay (E5M3) Main -> Quay (E5M3) Yellow", player), lambda state: + state.has("Quay (E5M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("Quay (E5M3) Main -> Quay (E5M3) Green", player), lambda state: + state.has("Quay (E5M3) - Green key", player, 1)) + set_rule(world.get_entrance("Quay (E5M3) Main -> Quay (E5M3) Blue", player), lambda state: + state.has("Quay (E5M3) - Blue key", player, 1)) + set_rule(world.get_entrance("Quay (E5M3) Blue -> Quay (E5M3) Green", player), lambda state: + state.has("Quay (E5M3) - Blue key", player, 1)) + set_rule(world.get_entrance("Quay (E5M3) Yellow -> Quay (E5M3) Main", player), lambda state: + state.has("Quay (E5M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("Quay (E5M3) Green -> Quay (E5M3) Main", player), lambda state: + state.has("Quay (E5M3) - Green key", player, 1)) + set_rule(world.get_entrance("Quay (E5M3) Green -> Quay (E5M3) Blue", player), lambda state: + state.has("Quay (E5M3) - Blue key", player, 1)) + + # Courtyard (E5M4) + set_rule(world.get_entrance("Hub -> Courtyard (E5M4) Main", player), lambda state: + (state.has("Courtyard (E5M4)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Kakis", player), lambda state: + state.has("Courtyard (E5M4) - Yellow key", player, 1) or + state.has("Courtyard (E5M4) - Green key", player, 1)) + set_rule(world.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Blue", player), lambda state: + state.has("Courtyard (E5M4) - Blue key", player, 1)) + set_rule(world.get_entrance("Courtyard (E5M4) Blue -> Courtyard (E5M4) Main", player), lambda state: + state.has("Courtyard (E5M4) - Blue key", player, 1)) + set_rule(world.get_entrance("Courtyard (E5M4) Kakis -> Courtyard (E5M4) Main", player), lambda state: + state.has("Courtyard (E5M4) - Yellow key", player, 1) or + state.has("Courtyard (E5M4) - Green key", player, 1)) + + # Hydratyr (E5M5) + set_rule(world.get_entrance("Hub -> Hydratyr (E5M5) Main", player), lambda state: + (state.has("Hydratyr (E5M5)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Hydratyr (E5M5) Main -> Hydratyr (E5M5) Yellow", player), lambda state: + state.has("Hydratyr (E5M5) - Yellow key", player, 1)) + set_rule(world.get_entrance("Hydratyr (E5M5) Blue -> Hydratyr (E5M5) Green", player), lambda state: + state.has("Hydratyr (E5M5) - Blue key", player, 1)) + set_rule(world.get_entrance("Hydratyr (E5M5) Yellow -> Hydratyr (E5M5) Green", player), lambda state: + state.has("Hydratyr (E5M5) - Green key", player, 1)) + set_rule(world.get_entrance("Hydratyr (E5M5) Green -> Hydratyr (E5M5) Blue", player), lambda state: + state.has("Hydratyr (E5M5) - Blue key", player, 1)) + + # Colonnade (E5M6) + set_rule(world.get_entrance("Hub -> Colonnade (E5M6) Main", player), lambda state: + (state.has("Colonnade (E5M6)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Colonnade (E5M6) Main -> Colonnade (E5M6) Yellow", player), lambda state: + state.has("Colonnade (E5M6) - Yellow key", player, 1)) + set_rule(world.get_entrance("Colonnade (E5M6) Main -> Colonnade (E5M6) Blue", player), lambda state: + state.has("Colonnade (E5M6) - Blue key", player, 1)) + set_rule(world.get_entrance("Colonnade (E5M6) Blue -> Colonnade (E5M6) Main", player), lambda state: + state.has("Colonnade (E5M6) - Blue key", player, 1)) + set_rule(world.get_entrance("Colonnade (E5M6) Yellow -> Colonnade (E5M6) Green", player), lambda state: + state.has("Colonnade (E5M6) - Green key", player, 1)) + set_rule(world.get_entrance("Colonnade (E5M6) Green -> Colonnade (E5M6) Yellow", player), lambda state: + state.has("Colonnade (E5M6) - Green key", player, 1)) + + # Foetid Manse (E5M7) + set_rule(world.get_entrance("Hub -> Foetid Manse (E5M7) Main", player), lambda state: + (state.has("Foetid Manse (E5M7)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Foetid Manse (E5M7) Main -> Foetid Manse (E5M7) Yellow", player), lambda state: + state.has("Foetid Manse (E5M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("Foetid Manse (E5M7) Yellow -> Foetid Manse (E5M7) Green", player), lambda state: + state.has("Foetid Manse (E5M7) - Green key", player, 1)) + set_rule(world.get_entrance("Foetid Manse (E5M7) Yellow -> Foetid Manse (E5M7) Blue", player), lambda state: + state.has("Foetid Manse (E5M7) - Blue key", player, 1)) + + # Field of Judgement (E5M8) + set_rule(world.get_entrance("Hub -> Field of Judgement (E5M8) Main", player), lambda state: + state.has("Field of Judgement (E5M8)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Phoenix Rod", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Bag of Holding", player, 1)) + + # Skein of D'Sparil (E5M9) + set_rule(world.get_entrance("Hub -> Skein of D'Sparil (E5M9) Main", player), lambda state: + state.has("Skein of D'Sparil (E5M9)", player, 1) and + state.has("Bag of Holding", player, 1) and + state.has("Hellstaff", player, 1) and + state.has("Phoenix Rod", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Firemace", player, 1)) + set_rule(world.get_entrance("Skein of D'Sparil (E5M9) Main -> Skein of D'Sparil (E5M9) Blue", player), lambda state: + state.has("Skein of D'Sparil (E5M9) - Blue key", player, 1)) + set_rule(world.get_entrance("Skein of D'Sparil (E5M9) Main -> Skein of D'Sparil (E5M9) Yellow", player), lambda state: + state.has("Skein of D'Sparil (E5M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("Skein of D'Sparil (E5M9) Main -> Skein of D'Sparil (E5M9) Green", player), lambda state: + state.has("Skein of D'Sparil (E5M9) - Green key", player, 1)) + set_rule(world.get_entrance("Skein of D'Sparil (E5M9) Yellow -> Skein of D'Sparil (E5M9) Main", player), lambda state: + state.has("Skein of D'Sparil (E5M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("Skein of D'Sparil (E5M9) Green -> Skein of D'Sparil (E5M9) Main", player), lambda state: + state.has("Skein of D'Sparil (E5M9) - Green key", player, 1)) + + +def set_rules(heretic_world: "HereticWorld", included_episodes, pro): + player = heretic_world.player + world = heretic_world.multiworld + + if included_episodes[0]: + set_episode1_rules(player, world, pro) + if included_episodes[1]: + set_episode2_rules(player, world, pro) + if included_episodes[2]: + set_episode3_rules(player, world, pro) + if included_episodes[3]: + set_episode4_rules(player, world, pro) + if included_episodes[4]: + set_episode5_rules(player, world, pro) diff --git a/worlds/heretic/__init__.py b/worlds/heretic/__init__.py new file mode 100644 index 000000000000..b0b2bfce8f26 --- /dev/null +++ b/worlds/heretic/__init__.py @@ -0,0 +1,287 @@ +import functools +import logging +from typing import Any, Dict, List, Set + +from BaseClasses import Entrance, CollectionState, Item, ItemClassification, Location, MultiWorld, Region, Tutorial +from worlds.AutoWorld import WebWorld, World +from . import Items, Locations, Maps, Options, Regions, Rules + +logger = logging.getLogger("Heretic") + +HERETIC_TYPE_LEVEL_COMPLETE = -2 +HERETIC_TYPE_MAP_SCROLL = 35 + + +class HereticLocation(Location): + game: str = "Heretic" + + +class HereticItem(Item): + game: str = "Heretic" + + +class HereticWeb(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Heretic randomizer connected to an Archipelago Multiworld", + "English", + "setup_en.md", + "setup/en", + ["Daivuk"] + )] + theme = "dirt" + + +class HereticWorld(World): + """ + Heretic is a dark fantasy first-person shooter video game released in December 1994. It was developed by Raven Software. + """ + option_definitions = Options.options + game = "Heretic" + web = HereticWeb() + data_version = 3 + required_client_version = (0, 3, 9) + + item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} + item_name_groups = Items.item_name_groups + + location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()} + location_name_groups = Locations.location_name_groups + + starting_level_for_episode: List[str] = [ + "The Docks (E1M1)", + "The Crater (E2M1)", + "The Storehouse (E3M1)", + "Catafalque (E4M1)", + "Ochre Cliffs (E5M1)" + ] + + boss_level_for_espidoes: List[str] = [ + "Hell's Maw (E1M8)", + "The Portals of Chaos (E2M8)", + "D'Sparil'S Keep (E3M8)", + "Shattered Bridge (E4M8)", + "Field of Judgement (E5M8)" + ] + + # Item ratio that scales depending on episode count. These are the ratio for 1 episode. + items_ratio: Dict[str, float] = { + "Timebomb of the Ancients": 16, + "Tome of Power": 16, + "Silver Shield": 10, + "Enchanted Shield": 5, + "Morph Ovum": 3, + "Mystic Urn": 2, + "Chaos Device": 1, + "Ring of Invincibility": 1, + "Shadowsphere": 1 + } + + def __init__(self, world: MultiWorld, player: int): + self.included_episodes = [1, 1, 1, 0, 0] + self.location_count = 0 + + super().__init__(world, player) + + def get_episode_count(self): + return functools.reduce(lambda count, episode: count + episode, self.included_episodes) + + def generate_early(self): + # Cache which episodes are included + for i in range(5): + self.included_episodes[i] = getattr(self.multiworld, f"episode{i + 1}")[self.player].value + + # If no episodes selected, select Episode 1 + if self.get_episode_count() == 0: + self.included_episodes[0] = 1 + + def create_regions(self): + pro = getattr(self.multiworld, "pro")[self.player].value + check_sanity = getattr(self.multiworld, "check_sanity")[self.player].value + + # Main regions + menu_region = Region("Menu", self.player, self.multiworld) + hub_region = Region("Hub", self.player, self.multiworld) + self.multiworld.regions += [menu_region, hub_region] + menu_region.add_exits(["Hub"]) + + # Create regions and locations + main_regions = [] + connections = [] + for region_dict in Regions.regions: + if not self.included_episodes[region_dict["episode"] - 1]: + continue + + region_name = region_dict["name"] + if region_dict["connects_to_hub"]: + main_regions.append(region_name) + + region = Region(region_name, self.player, self.multiworld) + region.add_locations({ + loc["name"]: loc_id + for loc_id, loc in Locations.location_table.items() + if loc["region"] == region_name and (not loc["check_sanity"] or check_sanity) + }, HereticLocation) + + self.multiworld.regions.append(region) + + for connection_dict in region_dict["connections"]: + # Check if it's a pro-only connection + if connection_dict["pro"] and not pro: + continue + connections.append((region, connection_dict["target"])) + + # Connect main regions to Hub + hub_region.add_exits(main_regions) + + # Do the other connections between regions (They are not all both ways) + for connection in connections: + source = connection[0] + target = self.multiworld.get_region(connection[1], self.player) + + entrance = Entrance(self.player, f"{source.name} -> {target.name}", source) + source.exits.append(entrance) + entrance.connect(target) + + # Sum locations for items creation + self.location_count = len(self.multiworld.get_locations(self.player)) + + def completion_rule(self, state: CollectionState): + goal_levels = Maps.map_names + if getattr(self.multiworld, "goal")[self.player].value: + goal_levels = self.boss_level_for_espidoes + + for map_name in goal_levels: + if map_name + " - Exit" not in self.location_name_to_id: + continue + + # Exit location names are in form: The Docks (E1M1) - Exit + loc = Locations.location_table[self.location_name_to_id[map_name + " - Exit"]] + if not self.included_episodes[loc["episode"] - 1]: + continue + + # Map complete item names are in form: The Docks (E1M1) - Complete + if not state.has(map_name + " - Complete", self.player, 1): + return False + + return True + + def set_rules(self): + pro = getattr(self.multiworld, "pro")[self.player].value + allow_death_logic = getattr(self.multiworld, "allow_death_logic")[self.player].value + + Rules.set_rules(self, self.included_episodes, pro) + self.multiworld.completion_condition[self.player] = lambda state: self.completion_rule(state) + + # Forbid progression items to locations that can be missed and can't be picked up. (e.g. One-time timed + # platform) Unless the user allows for it. + if not allow_death_logic: + for death_logic_location in Locations.death_logic_locations: + self.multiworld.exclude_locations[self.player].value.add(death_logic_location) + + def create_item(self, name: str) -> HereticItem: + item_id: int = self.item_name_to_id[name] + return HereticItem(name, Items.item_table[item_id]["classification"], item_id, self.player) + + def create_items(self): + itempool: List[HereticItem] = [] + start_with_map_scrolls: bool = getattr(self.multiworld, "start_with_map_scrolls")[self.player].value + + # Items + for item_id, item in Items.item_table.items(): + if item["doom_type"] == HERETIC_TYPE_LEVEL_COMPLETE: + continue # We'll fill it manually later + + if item["doom_type"] == HERETIC_TYPE_MAP_SCROLL and start_with_map_scrolls: + continue # We'll fill it manually, and we will put fillers in place + + if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]: + continue + + count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1 + itempool += [self.create_item(item["name"]) for _ in range(count)] + + # Place end level items in locked locations + for map_name in Maps.map_names: + loc_name = map_name + " - Exit" + item_name = map_name + " - Complete" + + if loc_name not in self.location_name_to_id: + continue + + if item_name not in self.item_name_to_id: + continue + + loc = Locations.location_table[self.location_name_to_id[loc_name]] + if not self.included_episodes[loc["episode"] - 1]: + continue + + self.multiworld.get_location(loc_name, self.player).place_locked_item(self.create_item(item_name)) + self.location_count -= 1 + + # Give starting levels right away + for i in range(len(self.included_episodes)): + if self.included_episodes[i]: + self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i])) + + # Give Computer area maps if option selected + if getattr(self.multiworld, "start_with_map_scrolls")[self.player].value: + for item_id, item_dict in Items.item_table.items(): + item_episode = item_dict["episode"] + if item_episode > 0: + if item_dict["doom_type"] == HERETIC_TYPE_MAP_SCROLL and self.included_episodes[item_episode - 1]: + self.multiworld.push_precollected(self.create_item(item_dict["name"])) + + # Fill the rest starting with powerups, then fillers + self.create_ratioed_items("Chaos Device", itempool) + self.create_ratioed_items("Morph Ovum", itempool) + self.create_ratioed_items("Mystic Urn", itempool) + self.create_ratioed_items("Ring of Invincibility", itempool) + self.create_ratioed_items("Shadowsphere", itempool) + self.create_ratioed_items("Timebomb of the Ancients", itempool) + self.create_ratioed_items("Tome of Power", itempool) + self.create_ratioed_items("Silver Shield", itempool) + self.create_ratioed_items("Enchanted Shield", itempool) + + while len(itempool) < self.location_count: + itempool.append(self.create_item(self.get_filler_item_name())) + + # add itempool to multiworld + self.multiworld.itempool += itempool + + def get_filler_item_name(self): + return self.multiworld.random.choice([ + "Quartz Flask", + "Crystal Geode", + "Energy Orb", + "Greater Runes", + "Inferno Orb", + "Pile of Mace Spheres", + "Quiver of Ethereal Arrows" + ]) + + def create_ratioed_items(self, item_name: str, itempool: List[HereticItem]): + remaining_loc = self.location_count - len(itempool) + if remaining_loc <= 0: + return + + episode_count = self.get_episode_count() + count = min(remaining_loc, max(1, self.items_ratio[item_name] * episode_count)) + if count == 0: + logger.warning("Warning, no " + item_name + " will be placed.") + return + + for i in range(count): + itempool.append(self.create_item(item_name)) + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data = self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "check_sanity") + + # Make sure we send proper episode settings + slot_data["episode1"] = self.included_episodes[0] + slot_data["episode2"] = self.included_episodes[1] + slot_data["episode3"] = self.included_episodes[2] + slot_data["episode4"] = self.included_episodes[3] + slot_data["episode5"] = self.included_episodes[4] + + return slot_data diff --git a/worlds/heretic/docs/en_Heretic.md b/worlds/heretic/docs/en_Heretic.md new file mode 100644 index 000000000000..97d371de2c2f --- /dev/null +++ b/worlds/heretic/docs/en_Heretic.md @@ -0,0 +1,23 @@ +# Heretic + +## Where is the settings page? + +The [player settings page](../player-settings) contains the options needed to configure your game session. + +## What does randomization do to this game? + +Weapons, keys, and level unlocks have been randomized. Monsters and Pickups are also randomized. Typically, you will end up playing different levels out of order to find your keys and level unlocks and eventually complete your game. + +Maps can be selected on a level select screen. You can exit a level at any time by visiting the hub station at the beginning of each level. The state of each level is saved and restored upon re-entering the level. + +## What is the goal? + +The goal is to complete every level in the episodes you have chosen to play. + +## What is a "check" in The Heretic? + +Weapons, keys, and powerups have been replaced with Archipelago checks. Some have been selectively removed because Heretic contains a lot of collectibles. Usually when many bunch together, one was kept. The switch at the end of each level is also a check. + +## What "items" can you unlock in Heretic? + +Keys and level unlocks are your main progression items. Weapon unlocks and some upgrades are your useful items. Powerups, ammo, healing, and armor are filler items. diff --git a/worlds/heretic/docs/setup_en.md b/worlds/heretic/docs/setup_en.md new file mode 100644 index 000000000000..e01d616e8ff1 --- /dev/null +++ b/worlds/heretic/docs/setup_en.md @@ -0,0 +1,51 @@ +# Heretic Randomizer Setup + +## Required Software + +- [Heretic (e.g. Steam version)](https://store.steampowered.com/app/2390/Heretic_Shadow_of_the_Serpent_Riders/) +- [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases) (Same download for DOOM 1993, DOOM II and Heretic) + +## Optional Software + +- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases) + +## Installing APDoom +1. Download [APDOOM.zip](https://github.com/Daivuk/apdoom/releases) and extract it. +2. Copy HERETIC.WAD from your steam install into the extracted folder. + You can find the folder in steam by finding the game in your library, + right clicking it and choosing *Manage→Browse Local Files*. + +## Joining a MultiWorld Game + +1. Launch apdoom-launcher.exe +2. Choose Heretic in the dropdown +3. Enter the Archipelago server address, slot name, and password (if you have one) +4. Press "Launch Game" +5. Enjoy! + +To continue a game, follow the same connection steps. +Connecting with a different seed won't erase your progress in other seeds. + +## Archipelago Text Client + +We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. +APDOOM has in-game messages, +but they disappear quickly and there's no reasonable way to check your message history in-game. + +### Hinting + +To hint from in-game, use the chat (Default key: 'T'). Hinting from Heretic can be difficult because names are rather long and contain special characters. For example: +``` +!hint The River of Fire (E2M3) - Green key +``` +The game has a hint helper implemented, where you can simply type this: +``` +!hint e2m3 green +``` +For this to work, include the map short name (`E1M1`), followed by one of the keywords: `map`, `blue`, `yellow`, `green`. + +## Auto-Tracking + +APDOOM has a functional map tracker integrated into the level select screen. +It tells you which levels you have unlocked, which keys you have for each level, which levels have been completed, +and how many of the checks you have completed in each level. From dd47790c318edb9befdcff0e58de9a310f4298dd Mon Sep 17 00:00:00 2001 From: Brooty Johnson <83629348+Br00ty@users.noreply.github.com> Date: Sat, 25 Nov 2023 09:38:18 -0500 Subject: [PATCH 088/104] DS3: Added 'Early Banner' Setting (#2199) Co-authored-by: Zach Parks --- worlds/dark_souls_3/Options.py | 11 +++++++++++ worlds/dark_souls_3/__init__.py | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/worlds/dark_souls_3/Options.py b/worlds/dark_souls_3/Options.py index d613e4733406..df0bb953b8d9 100644 --- a/worlds/dark_souls_3/Options.py +++ b/worlds/dark_souls_3/Options.py @@ -171,6 +171,16 @@ class MaxLevelsIn10WeaponPoolOption(Range): default = 10 +class EarlySmallLothricBanner(Choice): + """This option makes it so the user can choose to force the Small Lothric Banner into an early sphere in their world or + into an early sphere across all worlds.""" + display_name = "Early Small Lothric Banner" + option_off = 0 + option_early_global = 1 + option_early_local = 2 + default = option_off + + class LateBasinOfVowsOption(Toggle): """This option makes it so the Basin of Vows is still randomized, but guarantees you that you wont have to venture into Lothric Castle to find your Small Lothric Banner to get out of High Wall of Lothric. So you may find Basin of Vows early, @@ -215,6 +225,7 @@ class EnableDLCOption(Toggle): "max_levels_in_5": MaxLevelsIn5WeaponPoolOption, "min_levels_in_10": MinLevelsIn10WeaponPoolOption, "max_levels_in_10": MaxLevelsIn10WeaponPoolOption, + "early_banner": EarlySmallLothricBanner, "late_basin_of_vows": LateBasinOfVowsOption, "late_dlc": LateDLCOption, "no_spell_requirements": NoSpellRequirementsOption, diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index b9879f70f302..7ee6c2a6411b 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -9,7 +9,7 @@ from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names, item_descriptions from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary -from .Options import RandomizeWeaponLevelOption, PoolTypeOption, dark_souls_options +from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, dark_souls_options class DarkSouls3Web(WebWorld): @@ -86,6 +86,10 @@ def generate_early(self): self.enabled_location_categories.add(DS3LocationCategory.NPC) if self.multiworld.enable_key_locations[self.player] == Toggle.option_true: self.enabled_location_categories.add(DS3LocationCategory.KEY) + if self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_global: + self.multiworld.early_items[self.player]['Small Lothric Banner'] = 1 + elif self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_local: + self.multiworld.local_early_items[self.player]['Small Lothric Banner'] = 1 if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true: self.enabled_location_categories.add(DS3LocationCategory.BOSS) if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true: From 59ed2602bd48c7d861b0230562a85c239110f4f6 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 25 Nov 2023 15:42:03 +0100 Subject: [PATCH 089/104] Pokemon: delete old files (#2501) --- inno_setup.iss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/inno_setup.iss b/inno_setup.iss index 4744fa2b724d..10d699ad7065 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -81,6 +81,8 @@ Type: dirifempty; Name: "{app}" [InstallDelete] Type: files; Name: "{app}\ArchipelagoLttPClient.exe" +Type: files; Name: "{app}\ArchipelagoPokemonClient.exe" +Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua" Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*" Type: filesandordirs; Name: "{app}\SNI\lua*" Type: filesandordirs; Name: "{app}\EnemizerCLI*" From c138918400447ff9b8425e4344adb319f56e766f Mon Sep 17 00:00:00 2001 From: David St-Louis Date: Sat, 25 Nov 2023 09:43:14 -0500 Subject: [PATCH 090/104] DOOM 1993: Added various new options (#2067) --- worlds/doom_1993/Items.py | 1 + worlds/doom_1993/Locations.py | 2 +- worlds/doom_1993/Options.py | 62 +++++- worlds/doom_1993/Regions.py | 351 ++++++++++++++++-------------- worlds/doom_1993/Rules.py | 21 +- worlds/doom_1993/__init__.py | 39 +++- worlds/doom_1993/docs/setup_en.md | 26 ++- 7 files changed, 316 insertions(+), 186 deletions(-) diff --git a/worlds/doom_1993/Items.py b/worlds/doom_1993/Items.py index fe5576c4dfc4..3c5124d4d57b 100644 --- a/worlds/doom_1993/Items.py +++ b/worlds/doom_1993/Items.py @@ -1165,6 +1165,7 @@ class ItemDict(TypedDict, total=False): item_name_groups: Dict[str, Set[str]] = { 'Ammos': {'Box of bullets', 'Box of rockets', 'Box of shotgun shells', 'Energy cell pack', }, + 'Computer area maps': {'Against Thee Wickedly (E4M6) - Computer area map', 'And Hell Followed (E4M7) - Computer area map', 'Central Processing (E1M6) - Computer area map', 'Command Center (E2M5) - Computer area map', 'Command Control (E1M4) - Computer area map', 'Computer Station (E1M7) - Computer area map', 'Containment Area (E2M2) - Computer area map', 'Deimos Anomaly (E2M1) - Computer area map', 'Deimos Lab (E2M4) - Computer area map', 'Dis (E3M8) - Computer area map', 'Fear (E4M9) - Computer area map', 'Fortress of Mystery (E2M9) - Computer area map', 'Halls of the Damned (E2M6) - Computer area map', 'Hangar (E1M1) - Computer area map', 'Hell Beneath (E4M1) - Computer area map', 'Hell Keep (E3M1) - Computer area map', 'House of Pain (E3M4) - Computer area map', 'Limbo (E3M7) - Computer area map', 'Military Base (E1M9) - Computer area map', 'Mt. Erebus (E3M6) - Computer area map', 'Nuclear Plant (E1M2) - Computer area map', 'Pandemonium (E3M3) - Computer area map', 'Perfect Hatred (E4M2) - Computer area map', 'Phobos Anomaly (E1M8) - Computer area map', 'Phobos Lab (E1M5) - Computer area map', 'Refinery (E2M3) - Computer area map', 'Sever the Wicked (E4M3) - Computer area map', 'Slough of Despair (E3M2) - Computer area map', 'Spawning Vats (E2M7) - Computer area map', 'They Will Repent (E4M5) - Computer area map', 'Tower of Babel (E2M8) - Computer area map', 'Toxin Refinery (E1M3) - Computer area map', 'Unholy Cathedral (E3M5) - Computer area map', 'Unruly Evil (E4M4) - Computer area map', 'Unto the Cruel (E4M8) - Computer area map', 'Warrens (E3M9) - Computer area map', }, 'Keys': {'Against Thee Wickedly (E4M6) - Blue skull key', 'Against Thee Wickedly (E4M6) - Red skull key', 'Against Thee Wickedly (E4M6) - Yellow skull key', 'And Hell Followed (E4M7) - Blue skull key', 'And Hell Followed (E4M7) - Red skull key', 'And Hell Followed (E4M7) - Yellow skull key', 'Central Processing (E1M6) - Blue keycard', 'Central Processing (E1M6) - Red keycard', 'Central Processing (E1M6) - Yellow keycard', 'Command Control (E1M4) - Blue keycard', 'Command Control (E1M4) - Yellow keycard', 'Computer Station (E1M7) - Blue keycard', 'Computer Station (E1M7) - Red keycard', 'Computer Station (E1M7) - Yellow keycard', 'Containment Area (E2M2) - Blue keycard', 'Containment Area (E2M2) - Red keycard', 'Containment Area (E2M2) - Yellow keycard', 'Deimos Anomaly (E2M1) - Blue keycard', 'Deimos Anomaly (E2M1) - Red keycard', 'Deimos Lab (E2M4) - Blue keycard', 'Deimos Lab (E2M4) - Yellow keycard', 'Fear (E4M9) - Yellow skull key', 'Fortress of Mystery (E2M9) - Blue skull key', 'Fortress of Mystery (E2M9) - Red skull key', 'Fortress of Mystery (E2M9) - Yellow skull key', 'Halls of the Damned (E2M6) - Blue skull key', 'Halls of the Damned (E2M6) - Red skull key', 'Halls of the Damned (E2M6) - Yellow skull key', 'Hell Beneath (E4M1) - Blue skull key', 'Hell Beneath (E4M1) - Red skull key', 'House of Pain (E3M4) - Blue skull key', 'House of Pain (E3M4) - Red skull key', 'House of Pain (E3M4) - Yellow skull key', 'Limbo (E3M7) - Blue skull key', 'Limbo (E3M7) - Red skull key', 'Limbo (E3M7) - Yellow skull key', 'Military Base (E1M9) - Blue keycard', 'Military Base (E1M9) - Red keycard', 'Military Base (E1M9) - Yellow keycard', 'Mt. Erebus (E3M6) - Blue skull key', 'Nuclear Plant (E1M2) - Red keycard', 'Pandemonium (E3M3) - Blue skull key', 'Perfect Hatred (E4M2) - Blue skull key', 'Perfect Hatred (E4M2) - Yellow skull key', 'Phobos Lab (E1M5) - Blue keycard', 'Phobos Lab (E1M5) - Yellow keycard', 'Refinery (E2M3) - Blue keycard', 'Sever the Wicked (E4M3) - Blue skull key', 'Sever the Wicked (E4M3) - Red skull key', 'Slough of Despair (E3M2) - Blue skull key', 'Spawning Vats (E2M7) - Blue keycard', 'Spawning Vats (E2M7) - Red keycard', 'Spawning Vats (E2M7) - Yellow keycard', 'They Will Repent (E4M5) - Blue skull key', 'They Will Repent (E4M5) - Red skull key', 'They Will Repent (E4M5) - Yellow skull key', 'Toxin Refinery (E1M3) - Blue keycard', 'Toxin Refinery (E1M3) - Yellow keycard', 'Unholy Cathedral (E3M5) - Blue skull key', 'Unholy Cathedral (E3M5) - Yellow skull key', 'Unruly Evil (E4M4) - Red skull key', 'Unto the Cruel (E4M8) - Red skull key', 'Unto the Cruel (E4M8) - Yellow skull key', 'Warrens (E3M9) - Blue skull key', 'Warrens (E3M9) - Red skull key', }, 'Levels': {'Against Thee Wickedly (E4M6)', 'And Hell Followed (E4M7)', 'Central Processing (E1M6)', 'Command Center (E2M5)', 'Command Control (E1M4)', 'Computer Station (E1M7)', 'Containment Area (E2M2)', 'Deimos Anomaly (E2M1)', 'Deimos Lab (E2M4)', 'Dis (E3M8)', 'Fear (E4M9)', 'Fortress of Mystery (E2M9)', 'Halls of the Damned (E2M6)', 'Hangar (E1M1)', 'Hell Beneath (E4M1)', 'Hell Keep (E3M1)', 'House of Pain (E3M4)', 'Limbo (E3M7)', 'Military Base (E1M9)', 'Mt. Erebus (E3M6)', 'Nuclear Plant (E1M2)', 'Pandemonium (E3M3)', 'Perfect Hatred (E4M2)', 'Phobos Anomaly (E1M8)', 'Phobos Lab (E1M5)', 'Refinery (E2M3)', 'Sever the Wicked (E4M3)', 'Slough of Despair (E3M2)', 'Spawning Vats (E2M7)', 'They Will Repent (E4M5)', 'Tower of Babel (E2M8)', 'Toxin Refinery (E1M3)', 'Unholy Cathedral (E3M5)', 'Unruly Evil (E4M4)', 'Unto the Cruel (E4M8)', 'Warrens (E3M9)', }, 'Powerups': {'Armor', 'Berserk', 'Invulnerability', 'Mega Armor', 'Partial invisibility', 'Supercharge', }, diff --git a/worlds/doom_1993/Locations.py b/worlds/doom_1993/Locations.py index 778efb4661a8..2cbb9b9d150e 100644 --- a/worlds/doom_1993/Locations.py +++ b/worlds/doom_1993/Locations.py @@ -1968,7 +1968,7 @@ class LocationDict(TypedDict, total=False): 'map': 2, 'index': -1, 'doom_type': -1, - 'region': "Containment Area (E2M2) Red"}, + 'region': "Containment Area (E2M2) Red Exit"}, 351326: {'name': 'Deimos Anomaly (E2M1) - Exit', 'episode': 2, 'map': 1, diff --git a/worlds/doom_1993/Options.py b/worlds/doom_1993/Options.py index 72bb7c3aea4e..59f7bcef49a2 100644 --- a/worlds/doom_1993/Options.py +++ b/worlds/doom_1993/Options.py @@ -1,6 +1,18 @@ import typing -from Options import AssembleOptions, Choice, Toggle, DeathLink, DefaultOnToggle +from Options import AssembleOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool + + +class Goal(Choice): + """ + Choose the main goal. + complete_all_levels: All levels of the selected episodes + complete_boss_levels: Boss levels (E#M8) of selected episodes + """ + display_name = "Goal" + option_complete_all_levels = 0 + option_complete_boss_levels = 1 + default = 0 class Difficulty(Choice): @@ -27,11 +39,13 @@ class RandomMonsters(Choice): vanilla: No randomization shuffle: Monsters are shuffled within the level random_balanced: Monsters are completely randomized, but balanced based on existing ratio in the level. (Small monsters vs medium vs big) + random_chaotic: Monsters are completely randomized, but balanced based on existing ratio in the entire game. """ display_name = "Random Monsters" option_vanilla = 0 option_shuffle = 1 option_random_balanced = 2 + option_random_chaotic = 3 default = 1 @@ -49,6 +63,34 @@ class RandomPickups(Choice): default = 1 +class RandomMusic(Choice): + """ + Level musics will be randomized. + vanilla: No randomization + shuffle_selected: Selected episodes' levels will be shuffled + shuffle_game: All the music will be shuffled + """ + display_name = "Random Music" + option_vanilla = 0 + option_shuffle_selected = 1 + option_shuffle_game = 2 + default = 0 + + +class FlipLevels(Choice): + """ + Flip levels on one axis. + vanilla: No flipping + flipped: All levels are flipped + randomly_flipped: Random levels are flipped + """ + display_name = "Flip Levels" + option_vanilla = 0 + option_flipped = 1 + option_randomly_flipped = 2 + default = 0 + + class AllowDeathLogic(Toggle): """Some locations require a timed puzzle that can only be tried once. After which, if the player failed to get it, the location cannot be checked anymore. @@ -56,12 +98,24 @@ class AllowDeathLogic(Toggle): Get killed in the current map. The map will reset, you can now attempt the puzzle again.""" display_name = "Allow Death Logic" + +class Pro(Toggle): + """Include difficult tricks into rules. Mostly employed by speed runners. + i.e.: Leaps across to a locked area, trigger a switch behind a window at the right angle, etc.""" + display_name = "Pro Doom" + class StartWithComputerAreaMaps(Toggle): """Give the player all Computer Area Map items from the start.""" display_name = "Start With Computer Area Maps" +class ResetLevelOnDeath(DefaultOnToggle): + """When dying, levels are reset and monsters respawned. But inventory and checks are kept. + Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" + display_name="Reset Level on Death" + + class Episode1(DefaultOnToggle): """Knee-Deep in the Dead. If none of the episodes are chosen, Episode 1 will be chosen by default.""" @@ -87,12 +141,18 @@ class Episode4(Toggle): options: typing.Dict[str, AssembleOptions] = { + "start_inventory_from_pool": StartInventoryPool, + "goal": Goal, "difficulty": Difficulty, "random_monsters": RandomMonsters, "random_pickups": RandomPickups, + "random_music": RandomMusic, + "flip_levels": FlipLevels, "allow_death_logic": AllowDeathLogic, + "pro": Pro, "start_with_computer_area_maps": StartWithComputerAreaMaps, "death_link": DeathLink, + "reset_level_on_death": ResetLevelOnDeath, "episode1": Episode1, "episode2": Episode2, "episode3": Episode3, diff --git a/worlds/doom_1993/Regions.py b/worlds/doom_1993/Regions.py index 602c29f5bd83..f013bdceaf07 100644 --- a/worlds/doom_1993/Regions.py +++ b/worlds/doom_1993/Regions.py @@ -3,11 +3,15 @@ from typing import List from BaseClasses import TypedDict -class RegionDict(TypedDict, total=False): +class ConnectionDict(TypedDict, total=False): + target: str + pro: bool + +class RegionDict(TypedDict, total=False): name: str connects_to_hub: bool episode: int - connections: List[str] + connections: List[ConnectionDict] regions:List[RegionDict] = [ @@ -21,121 +25,131 @@ class RegionDict(TypedDict, total=False): {"name":"Nuclear Plant (E1M2) Main", "connects_to_hub":True, "episode":1, - "connections":["Nuclear Plant (E1M2) Red"]}, + "connections":[{"target":"Nuclear Plant (E1M2) Red","pro":False}]}, {"name":"Nuclear Plant (E1M2) Red", "connects_to_hub":False, "episode":1, - "connections":["Nuclear Plant (E1M2) Main"]}, + "connections":[{"target":"Nuclear Plant (E1M2) Main","pro":False}]}, # Toxin Refinery (E1M3) {"name":"Toxin Refinery (E1M3) Main", "connects_to_hub":True, "episode":1, - "connections":["Toxin Refinery (E1M3) Blue"]}, + "connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]}, {"name":"Toxin Refinery (E1M3) Blue", "connects_to_hub":False, "episode":1, "connections":[ - "Toxin Refinery (E1M3) Yellow", - "Toxin Refinery (E1M3) Main"]}, + {"target":"Toxin Refinery (E1M3) Yellow","pro":False}, + {"target":"Toxin Refinery (E1M3) Main","pro":False}]}, {"name":"Toxin Refinery (E1M3) Yellow", "connects_to_hub":False, "episode":1, - "connections":["Toxin Refinery (E1M3) Blue"]}, + "connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]}, # Command Control (E1M4) {"name":"Command Control (E1M4) Main", "connects_to_hub":True, "episode":1, "connections":[ - "Command Control (E1M4) Blue", - "Command Control (E1M4) Yellow"]}, + {"target":"Command Control (E1M4) Blue","pro":False}, + {"target":"Command Control (E1M4) Yellow","pro":False}, + {"target":"Command Control (E1M4) Ledge","pro":True}]}, {"name":"Command Control (E1M4) Blue", "connects_to_hub":False, "episode":1, - "connections":["Command Control (E1M4) Main"]}, + "connections":[ + {"target":"Command Control (E1M4) Ledge","pro":False}, + {"target":"Command Control (E1M4) Main","pro":False}]}, {"name":"Command Control (E1M4) Yellow", "connects_to_hub":False, "episode":1, - "connections":["Command Control (E1M4) Main"]}, + "connections":[{"target":"Command Control (E1M4) Main","pro":False}]}, + {"name":"Command Control (E1M4) Ledge", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"Command Control (E1M4) Main","pro":False}, + {"target":"Command Control (E1M4) Blue","pro":False}, + {"target":"Command Control (E1M4) Yellow","pro":False}]}, # Phobos Lab (E1M5) {"name":"Phobos Lab (E1M5) Main", "connects_to_hub":True, "episode":1, - "connections":["Phobos Lab (E1M5) Yellow"]}, + "connections":[{"target":"Phobos Lab (E1M5) Yellow","pro":False}]}, {"name":"Phobos Lab (E1M5) Yellow", "connects_to_hub":False, "episode":1, "connections":[ - "Phobos Lab (E1M5) Main", - "Phobos Lab (E1M5) Blue", - "Phobos Lab (E1M5) Green"]}, + {"target":"Phobos Lab (E1M5) Main","pro":False}, + {"target":"Phobos Lab (E1M5) Blue","pro":False}, + {"target":"Phobos Lab (E1M5) Green","pro":False}]}, {"name":"Phobos Lab (E1M5) Blue", "connects_to_hub":False, "episode":1, "connections":[ - "Phobos Lab (E1M5) Green", - "Phobos Lab (E1M5) Yellow"]}, + {"target":"Phobos Lab (E1M5) Green","pro":False}, + {"target":"Phobos Lab (E1M5) Yellow","pro":False}]}, {"name":"Phobos Lab (E1M5) Green", "connects_to_hub":False, "episode":1, "connections":[ - "Phobos Lab (E1M5) Main", - "Phobos Lab (E1M5) Blue"]}, + {"target":"Phobos Lab (E1M5) Main","pro":False}, + {"target":"Phobos Lab (E1M5) Blue","pro":False}]}, # Central Processing (E1M6) {"name":"Central Processing (E1M6) Main", "connects_to_hub":True, "episode":1, "connections":[ - "Central Processing (E1M6) Yellow", - "Central Processing (E1M6) Red", - "Central Processing (E1M6) Blue", - "Central Processing (E1M6) Nukage"]}, + {"target":"Central Processing (E1M6) Yellow","pro":False}, + {"target":"Central Processing (E1M6) Red","pro":False}, + {"target":"Central Processing (E1M6) Blue","pro":False}, + {"target":"Central Processing (E1M6) Nukage","pro":False}]}, {"name":"Central Processing (E1M6) Red", "connects_to_hub":False, "episode":1, - "connections":["Central Processing (E1M6) Main"]}, + "connections":[{"target":"Central Processing (E1M6) Main","pro":False}]}, {"name":"Central Processing (E1M6) Blue", "connects_to_hub":False, "episode":1, - "connections":["Central Processing (E1M6) Main"]}, + "connections":[{"target":"Central Processing (E1M6) Main","pro":False}]}, {"name":"Central Processing (E1M6) Yellow", "connects_to_hub":False, "episode":1, - "connections":["Central Processing (E1M6) Main"]}, + "connections":[{"target":"Central Processing (E1M6) Main","pro":False}]}, {"name":"Central Processing (E1M6) Nukage", "connects_to_hub":False, "episode":1, - "connections":["Central Processing (E1M6) Yellow"]}, + "connections":[{"target":"Central Processing (E1M6) Yellow","pro":False}]}, # Computer Station (E1M7) {"name":"Computer Station (E1M7) Main", "connects_to_hub":True, "episode":1, "connections":[ - "Computer Station (E1M7) Red", - "Computer Station (E1M7) Yellow"]}, + {"target":"Computer Station (E1M7) Red","pro":False}, + {"target":"Computer Station (E1M7) Yellow","pro":False}]}, {"name":"Computer Station (E1M7) Blue", "connects_to_hub":False, "episode":1, - "connections":["Computer Station (E1M7) Yellow"]}, + "connections":[{"target":"Computer Station (E1M7) Yellow","pro":False}]}, {"name":"Computer Station (E1M7) Red", "connects_to_hub":False, "episode":1, - "connections":["Computer Station (E1M7) Main"]}, + "connections":[{"target":"Computer Station (E1M7) Main","pro":False}]}, {"name":"Computer Station (E1M7) Yellow", "connects_to_hub":False, "episode":1, "connections":[ - "Computer Station (E1M7) Blue", - "Computer Station (E1M7) Courtyard", - "Computer Station (E1M7) Main"]}, + {"target":"Computer Station (E1M7) Blue","pro":False}, + {"target":"Computer Station (E1M7) Courtyard","pro":False}, + {"target":"Computer Station (E1M7) Main","pro":False}]}, {"name":"Computer Station (E1M7) Courtyard", "connects_to_hub":False, "episode":1, - "connections":["Computer Station (E1M7) Yellow"]}, + "connections":[{"target":"Computer Station (E1M7) Yellow","pro":False}]}, # Phobos Anomaly (E1M8) {"name":"Phobos Anomaly (E1M8) Main", @@ -145,91 +159,98 @@ class RegionDict(TypedDict, total=False): {"name":"Phobos Anomaly (E1M8) Start", "connects_to_hub":True, "episode":1, - "connections":["Phobos Anomaly (E1M8) Main"]}, + "connections":[{"target":"Phobos Anomaly (E1M8) Main","pro":False}]}, # Military Base (E1M9) {"name":"Military Base (E1M9) Main", "connects_to_hub":True, "episode":1, "connections":[ - "Military Base (E1M9) Blue", - "Military Base (E1M9) Yellow", - "Military Base (E1M9) Red"]}, + {"target":"Military Base (E1M9) Blue","pro":False}, + {"target":"Military Base (E1M9) Yellow","pro":False}, + {"target":"Military Base (E1M9) Red","pro":False}]}, {"name":"Military Base (E1M9) Blue", "connects_to_hub":False, "episode":1, - "connections":["Military Base (E1M9) Main"]}, + "connections":[{"target":"Military Base (E1M9) Main","pro":False}]}, {"name":"Military Base (E1M9) Red", "connects_to_hub":False, "episode":1, - "connections":["Military Base (E1M9) Main"]}, + "connections":[{"target":"Military Base (E1M9) Main","pro":False}]}, {"name":"Military Base (E1M9) Yellow", "connects_to_hub":False, "episode":1, - "connections":["Military Base (E1M9) Main"]}, + "connections":[{"target":"Military Base (E1M9) Main","pro":False}]}, # Deimos Anomaly (E2M1) {"name":"Deimos Anomaly (E2M1) Main", "connects_to_hub":True, "episode":2, "connections":[ - "Deimos Anomaly (E2M1) Red", - "Deimos Anomaly (E2M1) Blue"]}, + {"target":"Deimos Anomaly (E2M1) Red","pro":False}, + {"target":"Deimos Anomaly (E2M1) Blue","pro":False}]}, {"name":"Deimos Anomaly (E2M1) Blue", "connects_to_hub":False, "episode":2, - "connections":["Deimos Anomaly (E2M1) Main"]}, + "connections":[{"target":"Deimos Anomaly (E2M1) Main","pro":False}]}, {"name":"Deimos Anomaly (E2M1) Red", "connects_to_hub":False, "episode":2, - "connections":["Deimos Anomaly (E2M1) Main"]}, + "connections":[{"target":"Deimos Anomaly (E2M1) Main","pro":False}]}, # Containment Area (E2M2) {"name":"Containment Area (E2M2) Main", "connects_to_hub":True, "episode":2, "connections":[ - "Containment Area (E2M2) Yellow", - "Containment Area (E2M2) Blue", - "Containment Area (E2M2) Red"]}, + {"target":"Containment Area (E2M2) Yellow","pro":False}, + {"target":"Containment Area (E2M2) Blue","pro":False}, + {"target":"Containment Area (E2M2) Red","pro":False}, + {"target":"Containment Area (E2M2) Red Exit","pro":True}]}, {"name":"Containment Area (E2M2) Blue", "connects_to_hub":False, "episode":2, - "connections":["Containment Area (E2M2) Main"]}, + "connections":[{"target":"Containment Area (E2M2) Main","pro":False}]}, {"name":"Containment Area (E2M2) Red", "connects_to_hub":False, "episode":2, - "connections":["Containment Area (E2M2) Main"]}, + "connections":[ + {"target":"Containment Area (E2M2) Main","pro":False}, + {"target":"Containment Area (E2M2) Red Exit","pro":False}]}, {"name":"Containment Area (E2M2) Yellow", "connects_to_hub":False, "episode":2, - "connections":["Containment Area (E2M2) Main"]}, + "connections":[{"target":"Containment Area (E2M2) Main","pro":False}]}, + {"name":"Containment Area (E2M2) Red Exit", + "connects_to_hub":False, + "episode":2, + "connections":[]}, # Refinery (E2M3) {"name":"Refinery (E2M3) Main", "connects_to_hub":True, "episode":2, - "connections":["Refinery (E2M3) Blue"]}, + "connections":[{"target":"Refinery (E2M3) Blue","pro":False}]}, {"name":"Refinery (E2M3) Blue", "connects_to_hub":False, "episode":2, - "connections":["Refinery (E2M3) Main"]}, + "connections":[{"target":"Refinery (E2M3) Main","pro":False}]}, # Deimos Lab (E2M4) {"name":"Deimos Lab (E2M4) Main", "connects_to_hub":True, "episode":2, - "connections":["Deimos Lab (E2M4) Blue"]}, + "connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]}, {"name":"Deimos Lab (E2M4) Blue", "connects_to_hub":False, "episode":2, "connections":[ - "Deimos Lab (E2M4) Main", - "Deimos Lab (E2M4) Yellow"]}, + {"target":"Deimos Lab (E2M4) Main","pro":False}, + {"target":"Deimos Lab (E2M4) Yellow","pro":False}]}, {"name":"Deimos Lab (E2M4) Yellow", "connects_to_hub":False, "episode":2, - "connections":["Deimos Lab (E2M4) Blue"]}, + "connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]}, # Command Center (E2M5) {"name":"Command Center (E2M5) Main", @@ -242,47 +263,54 @@ class RegionDict(TypedDict, total=False): "connects_to_hub":True, "episode":2, "connections":[ - "Halls of the Damned (E2M6) Blue Yellow Red", - "Halls of the Damned (E2M6) Yellow", - "Halls of the Damned (E2M6) One way Yellow"]}, + {"target":"Halls of the Damned (E2M6) Blue Yellow Red","pro":False}, + {"target":"Halls of the Damned (E2M6) Yellow","pro":False}, + {"target":"Halls of the Damned (E2M6) One way Yellow","pro":False}]}, {"name":"Halls of the Damned (E2M6) Yellow", "connects_to_hub":False, "episode":2, - "connections":["Halls of the Damned (E2M6) Main"]}, + "connections":[{"target":"Halls of the Damned (E2M6) Main","pro":False}]}, {"name":"Halls of the Damned (E2M6) Blue Yellow Red", "connects_to_hub":False, "episode":2, - "connections":["Halls of the Damned (E2M6) Main"]}, + "connections":[{"target":"Halls of the Damned (E2M6) Main","pro":False}]}, {"name":"Halls of the Damned (E2M6) One way Yellow", "connects_to_hub":False, "episode":2, - "connections":["Halls of the Damned (E2M6) Main"]}, + "connections":[{"target":"Halls of the Damned (E2M6) Main","pro":False}]}, # Spawning Vats (E2M7) {"name":"Spawning Vats (E2M7) Main", "connects_to_hub":True, "episode":2, "connections":[ - "Spawning Vats (E2M7) Blue", - "Spawning Vats (E2M7) Entrance Secret", - "Spawning Vats (E2M7) Red", - "Spawning Vats (E2M7) Yellow"]}, + {"target":"Spawning Vats (E2M7) Blue","pro":False}, + {"target":"Spawning Vats (E2M7) Entrance Secret","pro":False}, + {"target":"Spawning Vats (E2M7) Red","pro":False}, + {"target":"Spawning Vats (E2M7) Yellow","pro":False}, + {"target":"Spawning Vats (E2M7) Red Exit","pro":True}]}, {"name":"Spawning Vats (E2M7) Blue", "connects_to_hub":False, "episode":2, - "connections":["Spawning Vats (E2M7) Main"]}, + "connections":[{"target":"Spawning Vats (E2M7) Main","pro":False}]}, {"name":"Spawning Vats (E2M7) Yellow", "connects_to_hub":False, "episode":2, - "connections":["Spawning Vats (E2M7) Main"]}, + "connections":[{"target":"Spawning Vats (E2M7) Main","pro":False}]}, {"name":"Spawning Vats (E2M7) Red", "connects_to_hub":False, "episode":2, - "connections":["Spawning Vats (E2M7) Main"]}, + "connections":[ + {"target":"Spawning Vats (E2M7) Main","pro":False}, + {"target":"Spawning Vats (E2M7) Red Exit","pro":False}]}, {"name":"Spawning Vats (E2M7) Entrance Secret", "connects_to_hub":False, "episode":2, - "connections":["Spawning Vats (E2M7) Main"]}, + "connections":[{"target":"Spawning Vats (E2M7) Main","pro":False}]}, + {"name":"Spawning Vats (E2M7) Red Exit", + "connects_to_hub":False, + "episode":2, + "connections":[]}, # Tower of Babel (E2M8) {"name":"Tower of Babel (E2M8) Main", @@ -295,134 +323,134 @@ class RegionDict(TypedDict, total=False): "connects_to_hub":True, "episode":2, "connections":[ - "Fortress of Mystery (E2M9) Blue", - "Fortress of Mystery (E2M9) Red", - "Fortress of Mystery (E2M9) Yellow"]}, + {"target":"Fortress of Mystery (E2M9) Blue","pro":False}, + {"target":"Fortress of Mystery (E2M9) Red","pro":False}, + {"target":"Fortress of Mystery (E2M9) Yellow","pro":False}]}, {"name":"Fortress of Mystery (E2M9) Blue", "connects_to_hub":False, "episode":2, - "connections":["Fortress of Mystery (E2M9) Main"]}, + "connections":[{"target":"Fortress of Mystery (E2M9) Main","pro":False}]}, {"name":"Fortress of Mystery (E2M9) Red", "connects_to_hub":False, "episode":2, - "connections":["Fortress of Mystery (E2M9) Main"]}, + "connections":[{"target":"Fortress of Mystery (E2M9) Main","pro":False}]}, {"name":"Fortress of Mystery (E2M9) Yellow", "connects_to_hub":False, "episode":2, - "connections":["Fortress of Mystery (E2M9) Main"]}, + "connections":[{"target":"Fortress of Mystery (E2M9) Main","pro":False}]}, # Hell Keep (E3M1) {"name":"Hell Keep (E3M1) Main", "connects_to_hub":True, "episode":3, - "connections":["Hell Keep (E3M1) Narrow"]}, + "connections":[{"target":"Hell Keep (E3M1) Narrow","pro":False}]}, {"name":"Hell Keep (E3M1) Narrow", "connects_to_hub":False, "episode":3, - "connections":["Hell Keep (E3M1) Main"]}, + "connections":[{"target":"Hell Keep (E3M1) Main","pro":False}]}, # Slough of Despair (E3M2) {"name":"Slough of Despair (E3M2) Main", "connects_to_hub":True, "episode":3, - "connections":["Slough of Despair (E3M2) Blue"]}, + "connections":[{"target":"Slough of Despair (E3M2) Blue","pro":False}]}, {"name":"Slough of Despair (E3M2) Blue", "connects_to_hub":False, "episode":3, - "connections":["Slough of Despair (E3M2) Main"]}, + "connections":[{"target":"Slough of Despair (E3M2) Main","pro":False}]}, # Pandemonium (E3M3) {"name":"Pandemonium (E3M3) Main", "connects_to_hub":True, "episode":3, - "connections":["Pandemonium (E3M3) Blue"]}, + "connections":[{"target":"Pandemonium (E3M3) Blue","pro":False}]}, {"name":"Pandemonium (E3M3) Blue", "connects_to_hub":False, "episode":3, - "connections":["Pandemonium (E3M3) Main"]}, + "connections":[{"target":"Pandemonium (E3M3) Main","pro":False}]}, # House of Pain (E3M4) {"name":"House of Pain (E3M4) Main", "connects_to_hub":True, "episode":3, - "connections":["House of Pain (E3M4) Blue"]}, + "connections":[{"target":"House of Pain (E3M4) Blue","pro":False}]}, {"name":"House of Pain (E3M4) Blue", "connects_to_hub":False, "episode":3, "connections":[ - "House of Pain (E3M4) Main", - "House of Pain (E3M4) Yellow", - "House of Pain (E3M4) Red"]}, + {"target":"House of Pain (E3M4) Main","pro":False}, + {"target":"House of Pain (E3M4) Yellow","pro":False}, + {"target":"House of Pain (E3M4) Red","pro":False}]}, {"name":"House of Pain (E3M4) Red", "connects_to_hub":False, "episode":3, - "connections":["House of Pain (E3M4) Blue"]}, + "connections":[{"target":"House of Pain (E3M4) Blue","pro":False}]}, {"name":"House of Pain (E3M4) Yellow", "connects_to_hub":False, "episode":3, - "connections":["House of Pain (E3M4) Blue"]}, + "connections":[{"target":"House of Pain (E3M4) Blue","pro":False}]}, # Unholy Cathedral (E3M5) {"name":"Unholy Cathedral (E3M5) Main", "connects_to_hub":True, "episode":3, "connections":[ - "Unholy Cathedral (E3M5) Yellow", - "Unholy Cathedral (E3M5) Blue"]}, + {"target":"Unholy Cathedral (E3M5) Yellow","pro":False}, + {"target":"Unholy Cathedral (E3M5) Blue","pro":False}]}, {"name":"Unholy Cathedral (E3M5) Blue", "connects_to_hub":False, "episode":3, - "connections":["Unholy Cathedral (E3M5) Main"]}, + "connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]}, {"name":"Unholy Cathedral (E3M5) Yellow", "connects_to_hub":False, "episode":3, - "connections":["Unholy Cathedral (E3M5) Main"]}, + "connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]}, # Mt. Erebus (E3M6) {"name":"Mt. Erebus (E3M6) Main", "connects_to_hub":True, "episode":3, - "connections":["Mt. Erebus (E3M6) Blue"]}, + "connections":[{"target":"Mt. Erebus (E3M6) Blue","pro":False}]}, {"name":"Mt. Erebus (E3M6) Blue", "connects_to_hub":False, "episode":3, - "connections":["Mt. Erebus (E3M6) Main"]}, + "connections":[{"target":"Mt. Erebus (E3M6) Main","pro":False}]}, # Limbo (E3M7) {"name":"Limbo (E3M7) Main", "connects_to_hub":True, "episode":3, "connections":[ - "Limbo (E3M7) Red", - "Limbo (E3M7) Blue", - "Limbo (E3M7) Pink"]}, + {"target":"Limbo (E3M7) Red","pro":False}, + {"target":"Limbo (E3M7) Blue","pro":False}, + {"target":"Limbo (E3M7) Pink","pro":False}]}, {"name":"Limbo (E3M7) Blue", "connects_to_hub":False, "episode":3, - "connections":["Limbo (E3M7) Main"]}, + "connections":[{"target":"Limbo (E3M7) Main","pro":False}]}, {"name":"Limbo (E3M7) Red", "connects_to_hub":False, "episode":3, "connections":[ - "Limbo (E3M7) Main", - "Limbo (E3M7) Yellow", - "Limbo (E3M7) Green"]}, + {"target":"Limbo (E3M7) Main","pro":False}, + {"target":"Limbo (E3M7) Yellow","pro":False}, + {"target":"Limbo (E3M7) Green","pro":False}]}, {"name":"Limbo (E3M7) Yellow", "connects_to_hub":False, "episode":3, - "connections":["Limbo (E3M7) Red"]}, + "connections":[{"target":"Limbo (E3M7) Red","pro":False}]}, {"name":"Limbo (E3M7) Pink", "connects_to_hub":False, "episode":3, "connections":[ - "Limbo (E3M7) Green", - "Limbo (E3M7) Main"]}, + {"target":"Limbo (E3M7) Green","pro":False}, + {"target":"Limbo (E3M7) Main","pro":False}]}, {"name":"Limbo (E3M7) Green", "connects_to_hub":False, "episode":3, "connections":[ - "Limbo (E3M7) Pink", - "Limbo (E3M7) Red"]}, + {"target":"Limbo (E3M7) Pink","pro":False}, + {"target":"Limbo (E3M7) Red","pro":False}]}, # Dis (E3M8) {"name":"Dis (E3M8) Main", @@ -435,8 +463,8 @@ class RegionDict(TypedDict, total=False): "connects_to_hub":True, "episode":3, "connections":[ - "Warrens (E3M9) Blue", - "Warrens (E3M9) Blue trigger"]}, + {"target":"Warrens (E3M9) Blue","pro":False}, + {"target":"Warrens (E3M9) Blue trigger","pro":False}]}, {"name":"Warrens (E3M9) Red", "connects_to_hub":False, "episode":3, @@ -445,8 +473,8 @@ class RegionDict(TypedDict, total=False): "connects_to_hub":False, "episode":3, "connections":[ - "Warrens (E3M9) Main", - "Warrens (E3M9) Red"]}, + {"target":"Warrens (E3M9) Main","pro":False}, + {"target":"Warrens (E3M9) Red","pro":False}]}, {"name":"Warrens (E3M9) Blue trigger", "connects_to_hub":False, "episode":3, @@ -457,36 +485,36 @@ class RegionDict(TypedDict, total=False): "connects_to_hub":True, "episode":4, "connections":[ - "Hell Beneath (E4M1) Red", - "Hell Beneath (E4M1) Blue"]}, + {"target":"Hell Beneath (E4M1) Red","pro":False}, + {"target":"Hell Beneath (E4M1) Blue","pro":False}]}, {"name":"Hell Beneath (E4M1) Red", "connects_to_hub":False, "episode":4, - "connections":["Hell Beneath (E4M1) Main"]}, + "connections":[{"target":"Hell Beneath (E4M1) Main","pro":False}]}, {"name":"Hell Beneath (E4M1) Blue", "connects_to_hub":False, "episode":4, - "connections":["Hell Beneath (E4M1) Main"]}, + "connections":[{"target":"Hell Beneath (E4M1) Main","pro":False}]}, # Perfect Hatred (E4M2) {"name":"Perfect Hatred (E4M2) Main", "connects_to_hub":True, "episode":4, "connections":[ - "Perfect Hatred (E4M2) Blue", - "Perfect Hatred (E4M2) Yellow"]}, + {"target":"Perfect Hatred (E4M2) Blue","pro":False}, + {"target":"Perfect Hatred (E4M2) Yellow","pro":False}]}, {"name":"Perfect Hatred (E4M2) Blue", "connects_to_hub":False, "episode":4, "connections":[ - "Perfect Hatred (E4M2) Main", - "Perfect Hatred (E4M2) Cave"]}, + {"target":"Perfect Hatred (E4M2) Main","pro":False}, + {"target":"Perfect Hatred (E4M2) Cave","pro":False}]}, {"name":"Perfect Hatred (E4M2) Yellow", "connects_to_hub":False, "episode":4, "connections":[ - "Perfect Hatred (E4M2) Main", - "Perfect Hatred (E4M2) Cave"]}, + {"target":"Perfect Hatred (E4M2) Main","pro":False}, + {"target":"Perfect Hatred (E4M2) Cave","pro":False}]}, {"name":"Perfect Hatred (E4M2) Cave", "connects_to_hub":False, "episode":4, @@ -496,132 +524,135 @@ class RegionDict(TypedDict, total=False): {"name":"Sever the Wicked (E4M3) Main", "connects_to_hub":True, "episode":4, - "connections":["Sever the Wicked (E4M3) Red"]}, + "connections":[{"target":"Sever the Wicked (E4M3) Red","pro":False}]}, {"name":"Sever the Wicked (E4M3) Red", "connects_to_hub":False, "episode":4, "connections":[ - "Sever the Wicked (E4M3) Blue", - "Sever the Wicked (E4M3) Main"]}, + {"target":"Sever the Wicked (E4M3) Blue","pro":False}, + {"target":"Sever the Wicked (E4M3) Main","pro":False}]}, {"name":"Sever the Wicked (E4M3) Blue", "connects_to_hub":False, "episode":4, - "connections":["Sever the Wicked (E4M3) Red"]}, + "connections":[{"target":"Sever the Wicked (E4M3) Red","pro":False}]}, # Unruly Evil (E4M4) {"name":"Unruly Evil (E4M4) Main", "connects_to_hub":True, "episode":4, - "connections":["Unruly Evil (E4M4) Red"]}, + "connections":[{"target":"Unruly Evil (E4M4) Red","pro":False}]}, {"name":"Unruly Evil (E4M4) Red", "connects_to_hub":False, "episode":4, - "connections":["Unruly Evil (E4M4) Main"]}, + "connections":[{"target":"Unruly Evil (E4M4) Main","pro":False}]}, # They Will Repent (E4M5) {"name":"They Will Repent (E4M5) Main", "connects_to_hub":True, "episode":4, - "connections":["They Will Repent (E4M5) Red"]}, + "connections":[{"target":"They Will Repent (E4M5) Red","pro":False}]}, {"name":"They Will Repent (E4M5) Yellow", "connects_to_hub":False, "episode":4, - "connections":["They Will Repent (E4M5) Red"]}, + "connections":[{"target":"They Will Repent (E4M5) Red","pro":False}]}, {"name":"They Will Repent (E4M5) Blue", "connects_to_hub":False, "episode":4, - "connections":["They Will Repent (E4M5) Red"]}, + "connections":[{"target":"They Will Repent (E4M5) Red","pro":False}]}, {"name":"They Will Repent (E4M5) Red", "connects_to_hub":False, "episode":4, "connections":[ - "They Will Repent (E4M5) Main", - "They Will Repent (E4M5) Yellow", - "They Will Repent (E4M5) Blue"]}, + {"target":"They Will Repent (E4M5) Main","pro":False}, + {"target":"They Will Repent (E4M5) Yellow","pro":False}, + {"target":"They Will Repent (E4M5) Blue","pro":False}]}, # Against Thee Wickedly (E4M6) {"name":"Against Thee Wickedly (E4M6) Main", "connects_to_hub":True, "episode":4, - "connections":["Against Thee Wickedly (E4M6) Blue"]}, + "connections":[ + {"target":"Against Thee Wickedly (E4M6) Blue","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Pink","pro":True}]}, {"name":"Against Thee Wickedly (E4M6) Red", "connects_to_hub":False, "episode":4, "connections":[ - "Against Thee Wickedly (E4M6) Blue", - "Against Thee Wickedly (E4M6) Pink", - "Against Thee Wickedly (E4M6) Main"]}, + {"target":"Against Thee Wickedly (E4M6) Blue","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Pink","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Main","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Magenta","pro":True}]}, {"name":"Against Thee Wickedly (E4M6) Blue", "connects_to_hub":False, "episode":4, "connections":[ - "Against Thee Wickedly (E4M6) Main", - "Against Thee Wickedly (E4M6) Yellow", - "Against Thee Wickedly (E4M6) Red"]}, + {"target":"Against Thee Wickedly (E4M6) Main","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Yellow","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Red","pro":False}]}, {"name":"Against Thee Wickedly (E4M6) Magenta", "connects_to_hub":False, "episode":4, - "connections":["Against Thee Wickedly (E4M6) Main"]}, + "connections":[{"target":"Against Thee Wickedly (E4M6) Main","pro":False}]}, {"name":"Against Thee Wickedly (E4M6) Yellow", "connects_to_hub":False, "episode":4, "connections":[ - "Against Thee Wickedly (E4M6) Blue", - "Against Thee Wickedly (E4M6) Magenta"]}, + {"target":"Against Thee Wickedly (E4M6) Blue","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Magenta","pro":False}]}, {"name":"Against Thee Wickedly (E4M6) Pink", "connects_to_hub":False, "episode":4, - "connections":["Against Thee Wickedly (E4M6) Main"]}, + "connections":[{"target":"Against Thee Wickedly (E4M6) Main","pro":False}]}, # And Hell Followed (E4M7) {"name":"And Hell Followed (E4M7) Main", "connects_to_hub":True, "episode":4, "connections":[ - "And Hell Followed (E4M7) Blue", - "And Hell Followed (E4M7) Red", - "And Hell Followed (E4M7) Yellow"]}, + {"target":"And Hell Followed (E4M7) Blue","pro":False}, + {"target":"And Hell Followed (E4M7) Red","pro":False}, + {"target":"And Hell Followed (E4M7) Yellow","pro":False}]}, {"name":"And Hell Followed (E4M7) Red", "connects_to_hub":False, "episode":4, - "connections":["And Hell Followed (E4M7) Main"]}, + "connections":[{"target":"And Hell Followed (E4M7) Main","pro":False}]}, {"name":"And Hell Followed (E4M7) Blue", "connects_to_hub":False, "episode":4, - "connections":["And Hell Followed (E4M7) Main"]}, + "connections":[{"target":"And Hell Followed (E4M7) Main","pro":False}]}, {"name":"And Hell Followed (E4M7) Yellow", "connects_to_hub":False, "episode":4, - "connections":["And Hell Followed (E4M7) Main"]}, + "connections":[{"target":"And Hell Followed (E4M7) Main","pro":False}]}, # Unto the Cruel (E4M8) {"name":"Unto the Cruel (E4M8) Main", "connects_to_hub":True, "episode":4, "connections":[ - "Unto the Cruel (E4M8) Red", - "Unto the Cruel (E4M8) Yellow", - "Unto the Cruel (E4M8) Orange"]}, + {"target":"Unto the Cruel (E4M8) Red","pro":False}, + {"target":"Unto the Cruel (E4M8) Yellow","pro":False}, + {"target":"Unto the Cruel (E4M8) Orange","pro":False}]}, {"name":"Unto the Cruel (E4M8) Yellow", "connects_to_hub":False, "episode":4, - "connections":["Unto the Cruel (E4M8) Main"]}, + "connections":[{"target":"Unto the Cruel (E4M8) Main","pro":False}]}, {"name":"Unto the Cruel (E4M8) Red", "connects_to_hub":False, "episode":4, - "connections":["Unto the Cruel (E4M8) Main"]}, + "connections":[{"target":"Unto the Cruel (E4M8) Main","pro":False}]}, {"name":"Unto the Cruel (E4M8) Orange", "connects_to_hub":False, "episode":4, - "connections":["Unto the Cruel (E4M8) Main"]}, + "connections":[{"target":"Unto the Cruel (E4M8) Main","pro":False}]}, # Fear (E4M9) {"name":"Fear (E4M9) Main", "connects_to_hub":True, "episode":4, - "connections":["Fear (E4M9) Yellow"]}, + "connections":[{"target":"Fear (E4M9) Yellow","pro":False}]}, {"name":"Fear (E4M9) Yellow", "connects_to_hub":False, "episode":4, - "connections":["Fear (E4M9) Main"]}, + "connections":[{"target":"Fear (E4M9) Main","pro":False}]}, ] diff --git a/worlds/doom_1993/Rules.py b/worlds/doom_1993/Rules.py index 6e13a8af34ce..d5abc367a149 100644 --- a/worlds/doom_1993/Rules.py +++ b/worlds/doom_1993/Rules.py @@ -7,7 +7,7 @@ from . import DOOM1993World -def set_episode1_rules(player, world): +def set_episode1_rules(player, world, pro): # Hangar (E1M1) set_rule(world.get_entrance("Hub -> Hangar (E1M1) Main", player), lambda state: state.has("Hangar (E1M1)", player, 1)) @@ -130,7 +130,7 @@ def set_episode1_rules(player, world): state.has("Military Base (E1M9) - Yellow keycard", player, 1)) -def set_episode2_rules(player, world): +def set_episode2_rules(player, world, pro): # Deimos Anomaly (E2M1) set_rule(world.get_entrance("Hub -> Deimos Anomaly (E2M1) Main", player), lambda state: state.has("Deimos Anomaly (E2M1)", player, 1)) @@ -226,6 +226,9 @@ def set_episode2_rules(player, world): state.has("Spawning Vats (E2M7) - Red keycard", player, 1)) set_rule(world.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Yellow", player), lambda state: state.has("Spawning Vats (E2M7) - Yellow keycard", player, 1)) + if pro: + set_rule(world.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Red Exit", player), lambda state: + state.has("Rocket launcher", player, 1)) set_rule(world.get_entrance("Spawning Vats (E2M7) Yellow -> Spawning Vats (E2M7) Main", player), lambda state: state.has("Spawning Vats (E2M7) - Yellow keycard", player, 1)) set_rule(world.get_entrance("Spawning Vats (E2M7) Red -> Spawning Vats (E2M7) Main", player), lambda state: @@ -260,7 +263,7 @@ def set_episode2_rules(player, world): state.has("Fortress of Mystery (E2M9) - Yellow skull key", player, 1)) -def set_episode3_rules(player, world): +def set_episode3_rules(player, world, pro): # Hell Keep (E3M1) set_rule(world.get_entrance("Hub -> Hell Keep (E3M1) Main", player), lambda state: state.has("Hell Keep (E3M1)", player, 1)) @@ -385,7 +388,7 @@ def set_episode3_rules(player, world): state.has("Warrens (E3M9) - Red skull key", player, 1)) -def set_episode4_rules(player, world): +def set_episode4_rules(player, world, pro): # Hell Beneath (E4M1) set_rule(world.get_entrance("Hub -> Hell Beneath (E4M1) Main", player), lambda state: state.has("Hell Beneath (E4M1)", player, 1)) @@ -520,15 +523,15 @@ def set_episode4_rules(player, world): state.has("Fear (E4M9) - Yellow skull key", player, 1)) -def set_rules(doom_1993_world: "DOOM1993World", included_episodes): +def set_rules(doom_1993_world: "DOOM1993World", included_episodes, pro): player = doom_1993_world.player world = doom_1993_world.multiworld if included_episodes[0]: - set_episode1_rules(player, world) + set_episode1_rules(player, world, pro) if included_episodes[1]: - set_episode2_rules(player, world) + set_episode2_rules(player, world, pro) if included_episodes[2]: - set_episode3_rules(player, world) + set_episode3_rules(player, world, pro) if included_episodes[3]: - set_episode4_rules(player, world) + set_episode4_rules(player, world, pro) diff --git a/worlds/doom_1993/__init__.py b/worlds/doom_1993/__init__.py index 83a8652af1d1..e420b34b4f00 100644 --- a/worlds/doom_1993/__init__.py +++ b/worlds/doom_1993/__init__.py @@ -56,6 +56,13 @@ class DOOM1993World(World): "Hell Beneath (E4M1)" ] + boss_level_for_espidoes: List[str] = [ + "Phobos Anomaly (E1M8)", + "Tower of Babel (E2M8)", + "Dis (E3M8)", + "Unto the Cruel (E4M8)" + ] + # Item ratio that scales depending on episode count. These are the ratio for 3 episode. items_ratio: Dict[str, float] = { "Armor": 41, @@ -90,6 +97,8 @@ def generate_early(self): self.included_episodes[0] = 1 def create_regions(self): + pro = getattr(self.multiworld, "pro")[self.player].value + # Main regions menu_region = Region("Menu", self.player, self.multiworld) hub_region = Region("Hub", self.player, self.multiworld) @@ -116,8 +125,11 @@ def create_regions(self): self.multiworld.regions.append(region) - for connection in region_dict["connections"]: - connections.append((region, connection)) + for connection_dict in region_dict["connections"]: + # Check if it's a pro-only connection + if connection_dict["pro"] and not pro: + continue + connections.append((region, connection_dict["target"])) # Connect main regions to Hub hub_region.add_exits(main_regions) @@ -135,7 +147,11 @@ def create_regions(self): self.location_count = len(self.multiworld.get_locations(self.player)) def completion_rule(self, state: CollectionState): - for map_name in Maps.map_names: + goal_levels = Maps.map_names + if getattr(self.multiworld, "goal")[self.player].value: + goal_levels = self.boss_level_for_espidoes + + for map_name in goal_levels: if map_name + " - Exit" not in self.location_name_to_id: continue @@ -151,12 +167,15 @@ def completion_rule(self, state: CollectionState): return True def set_rules(self): - Rules.set_rules(self, self.included_episodes) + pro = getattr(self.multiworld, "pro")[self.player].value + allow_death_logic = getattr(self.multiworld, "allow_death_logic")[self.player].value + + Rules.set_rules(self, self.included_episodes, pro) self.multiworld.completion_condition[self.player] = lambda state: self.completion_rule(state) # Forbid progression items to locations that can be missed and can't be picked up. (e.g. One-time timed # platform) Unless the user allows for it. - if not getattr(self.multiworld, "allow_death_logic")[self.player].value: + if not allow_death_logic: for death_logic_location in Locations.death_logic_locations: self.multiworld.exclude_locations[self.player].value.add(death_logic_location) @@ -165,7 +184,6 @@ def create_item(self, name: str) -> DOOM1993Item: return DOOM1993Item(name, Items.item_table[item_id]["classification"], item_id, self.player) def create_items(self): - is_only_first_episode: bool = self.get_episode_count() == 1 and self.included_episodes[0] itempool: List[DOOM1993Item] = [] start_with_computer_area_maps: bool = getattr(self.multiworld, "start_with_computer_area_maps")[self.player].value @@ -180,9 +198,6 @@ def create_items(self): if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]: continue - if item["name"] in {"BFG9000", "Plasma Gun"} and is_only_first_episode: - continue # Don't include those guns if only first episode - count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1 itempool += [self.create_item(item["name"]) for _ in range(count)] @@ -212,8 +227,10 @@ def create_items(self): # Give Computer area maps if option selected if getattr(self.multiworld, "start_with_computer_area_maps")[self.player].value: for item_id, item_dict in Items.item_table.items(): - if item_dict["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP: - self.multiworld.push_precollected(self.create_item(item_dict["name"])) + item_episode = item_dict["episode"] + if item_episode > 0: + if item_dict["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP and self.included_episodes[item_episode - 1]: + self.multiworld.push_precollected(self.create_item(item_dict["name"])) # Fill the rest starting with powerups, then fillers self.create_ratioed_items("Armor", itempool) diff --git a/worlds/doom_1993/docs/setup_en.md b/worlds/doom_1993/docs/setup_en.md index cfd97f623a0c..1e546d359c91 100644 --- a/worlds/doom_1993/docs/setup_en.md +++ b/worlds/doom_1993/docs/setup_en.md @@ -8,6 +8,8 @@ ## Optional Software - [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases) +- [PopTracker](https://github.com/black-sliver/PopTracker/) + - [OZone's APDoom tracker pack](https://github.com/Ozone31/doom-ap-tracker/releases) ## Installing AP Doom 1. Download [APDOOM.zip](https://github.com/Daivuk/apdoom/releases) and extract it. @@ -17,10 +19,11 @@ ## Joining a MultiWorld Game -1. Launch APDoomLauncher.exe -2. Enter the Archipelago server address, slot name, and password (if you have one) -3. Press "Launch DOOM" -4. Enjoy! +1. Launch apdoom-launcher.exe +2. Select `Ultimate DOOM` from the drop-down +3. Enter the Archipelago server address, slot name, and password (if you have one) +4. Press "Launch DOOM" +5. Enjoy! To continue a game, follow the same connection steps. Connecting with a different seed won't erase your progress in other seeds. @@ -31,8 +34,23 @@ We recommend having Archipelago's Text Client open on the side to keep track of APDOOM has in-game messages, but they disappear quickly and there's no reasonable way to check your message history in-game. +### Hinting + +To hint from in-game, use the chat (Default key: 'T'). Hinting from DOOM can be difficult because names are rather long and contain special characters. For example: +``` +!hint Toxin Refinery (E1M3) - Computer area map +``` +The game has a hint helper implemented, where you can simply type this: +``` +!hint e1m3 map +``` +For this to work, include the map short name (`E1M1`), followed by one of the keywords: `map`, `blue`, `yellow`, `red`. + ## Auto-Tracking APDOOM has a functional map tracker integrated into the level select screen. It tells you which levels you have unlocked, which keys you have for each level, which levels have been completed, and how many of the checks you have completed in each level. + +For better tracking, try OZone's poptracker package: https://github.com/Ozone31/doom-ap-tracker/releases . +Requires [PopTracker](https://github.com/black-sliver/PopTracker/). From 2ccf11f3d78c49011b3e69878910cc46ef7e5309 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sat, 25 Nov 2023 09:46:00 -0500 Subject: [PATCH 091/104] KH2: Version 2 (#2009) Co-authored-by: Aaron Wagener Co-authored-by: Joe Prochaska --- KH2Client.py | 892 +--------- setup.py | 1 - worlds/LauncherComponents.py | 2 - worlds/kh2/Client.py | 881 ++++++++++ worlds/kh2/Items.py | 1352 ++++++--------- worlds/kh2/Locations.py | 2458 +++++++++++---------------- worlds/kh2/Logic.py | 642 +++++++ worlds/kh2/Names/ItemName.py | 137 +- worlds/kh2/Names/LocationName.py | 235 ++- worlds/kh2/Names/RegionName.py | 246 ++- worlds/kh2/OpenKH.py | 250 ++- worlds/kh2/Options.py | 238 ++- worlds/kh2/Regions.py | 1213 ++++++------- worlds/kh2/Rules.py | 1247 +++++++++++++- worlds/kh2/WorldLocations.py | 171 +- worlds/kh2/__init__.py | 621 ++++--- worlds/kh2/logic.py | 312 ---- worlds/kh2/mod_template/mod.yml | 38 - worlds/kh2/test/TestGoal.py | 30 - worlds/kh2/test/TestSlotData.py | 21 - worlds/kh2/test/__init__.py | 2 +- worlds/kh2/test/test_fight_logic.py | 19 + worlds/kh2/test/test_form_logic.py | 214 +++ worlds/kh2/test/test_goal.py | 59 + 24 files changed, 6370 insertions(+), 4911 deletions(-) create mode 100644 worlds/kh2/Client.py create mode 100644 worlds/kh2/Logic.py delete mode 100644 worlds/kh2/logic.py delete mode 100644 worlds/kh2/mod_template/mod.yml delete mode 100644 worlds/kh2/test/TestGoal.py delete mode 100644 worlds/kh2/test/TestSlotData.py create mode 100644 worlds/kh2/test/test_fight_logic.py create mode 100644 worlds/kh2/test/test_form_logic.py create mode 100644 worlds/kh2/test/test_goal.py diff --git a/KH2Client.py b/KH2Client.py index 1134932dc26c..69e4adf8bf7c 100644 --- a/KH2Client.py +++ b/KH2Client.py @@ -1,894 +1,8 @@ -import os -import asyncio import ModuleUpdate -import json import Utils -from pymem import pymem -from worlds.kh2.Items import exclusionItem_table, CheckDupingItems -from worlds.kh2 import all_locations, item_dictionary_table, exclusion_table - -from worlds.kh2.WorldLocations import * - -from worlds import network_data_package - -if __name__ == "__main__": - Utils.init_logging("KH2Client", exception_logger="Client") - -from NetUtils import ClientStatus -from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \ - CommonContext, server_loop - +from worlds.kh2.Client import launch ModuleUpdate.update() -kh2_loc_name_to_id = network_data_package["games"]["Kingdom Hearts 2"]["location_name_to_id"] - - -# class KH2CommandProcessor(ClientCommandProcessor): - - -class KH2Context(CommonContext): - # command_processor: int = KH2CommandProcessor - game = "Kingdom Hearts 2" - items_handling = 0b101 # Indicates you get items sent from other worlds. - - def __init__(self, server_address, password): - super(KH2Context, self).__init__(server_address, password) - self.kh2LocalItems = None - self.ability = None - self.growthlevel = None - self.KH2_sync_task = None - self.syncing = False - self.kh2connected = False - self.serverconneced = False - self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()} - self.location_name_to_data = {name: data for name, data, in all_locations.items()} - self.lookup_id_to_item: typing.Dict[int, str] = {data.code: item_name for item_name, data in - item_dictionary_table.items() if data.code} - self.lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in - all_locations.items() if data.code} - self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()} - - self.location_table = {} - self.collectible_table = {} - self.collectible_override_flags_address = 0 - self.collectible_offsets = {} - self.sending = [] - # list used to keep track of locations+items player has. Used for disoneccting - self.kh2seedsave = None - self.slotDataProgressionNames = {} - self.kh2seedname = None - self.kh2slotdata = None - self.itemamount = {} - # sora equipped, valor equipped, master equipped, final equipped - self.keybladeAnchorList = (0x24F0, 0x32F4, 0x339C, 0x33D4) - if "localappdata" in os.environ: - self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP") - self.amountOfPieces = 0 - # hooked object - self.kh2 = None - self.ItemIsSafe = False - self.game_connected = False - self.finalxemnas = False - self.worldid = { - # 1: {}, # world of darkness (story cutscenes) - 2: TT_Checks, - # 3: {}, # destiny island doesn't have checks to ima put tt checks here - 4: HB_Checks, - 5: BC_Checks, - 6: Oc_Checks, - 7: AG_Checks, - 8: LoD_Checks, - 9: HundredAcreChecks, - 10: PL_Checks, - 11: DC_Checks, # atlantica isn't a supported world. if you go in atlantica it will check dc - 12: DC_Checks, - 13: TR_Checks, - 14: HT_Checks, - 15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb - 16: PR_Checks, - 17: SP_Checks, - 18: TWTNW_Checks, - # 255: {}, # starting screen - } - # 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room - self.sveroom = 0x2A09C00 + 0x41 - # 0 not in battle 1 in yellow battle 2 red battle #short - self.inBattle = 0x2A0EAC4 + 0x40 - self.onDeath = 0xAB9078 - # PC Address anchors - self.Now = 0x0714DB8 - self.Save = 0x09A70B0 - self.Sys3 = 0x2A59DF0 - self.Bt10 = 0x2A74880 - self.BtlEnd = 0x2A0D3E0 - self.Slot1 = 0x2A20C98 - - self.chest_set = set(exclusion_table["Chests"]) - - self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"]) - self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"]) - self.shield_set = set(CheckDupingItems["Weapons"]["Shields"]) - - self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set) - - self.equipment_categories = CheckDupingItems["Equipment"] - self.armor_set = set(self.equipment_categories["Armor"]) - self.accessories_set = set(self.equipment_categories["Accessories"]) - self.all_equipment = self.armor_set.union(self.accessories_set) - - self.Equipment_Anchor_Dict = { - "Armor": [0x2504, 0x2506, 0x2508, 0x250A], - "Accessories": [0x2514, 0x2516, 0x2518, 0x251A]} - - self.AbilityQuantityDict = {} - self.ability_categories = CheckDupingItems["Abilities"] - - self.sora_ability_set = set(self.ability_categories["Sora"]) - self.donald_ability_set = set(self.ability_categories["Donald"]) - self.goofy_ability_set = set(self.ability_categories["Goofy"]) - - self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set) - - self.boost_set = set(CheckDupingItems["Boosts"]) - self.stat_increase_set = set(CheckDupingItems["Stat Increases"]) - self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities} - # Growth:[level 1,level 4,slot] - self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25DA], - "Quick Run": [0x62, 0x65, 0x25DC], - "Dodge Roll": [0x234, 0x237, 0x25DE], - "Aerial Dodge": [0x066, 0x069, 0x25E0], - "Glide": [0x6A, 0x6D, 0x25E2]} - self.boost_to_anchor_dict = { - "Power Boost": 0x24F9, - "Magic Boost": 0x24FA, - "Defense Boost": 0x24FB, - "AP Boost": 0x24F8} - - self.AbilityCodeList = [self.item_name_to_data[item].code for item in exclusionItem_table["Ability"]] - self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"} - - self.bitmask_item_code = [ - 0x130000, 0x130001, 0x130002, 0x130003, 0x130004, 0x130005, 0x130006, 0x130007 - , 0x130008, 0x130009, 0x13000A, 0x13000B, 0x13000C - , 0x13001F, 0x130020, 0x130021, 0x130022, 0x130023 - , 0x13002A, 0x13002B, 0x13002C, 0x13002D] - - async def server_auth(self, password_requested: bool = False): - if password_requested and not self.password: - await super(KH2Context, self).server_auth(password_requested) - await self.get_username() - await self.send_connect() - - async def connection_closed(self): - self.kh2connected = False - self.serverconneced = False - if self.kh2seedname is not None and self.auth is not None: - with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), - 'w') as f: - f.write(json.dumps(self.kh2seedsave, indent=4)) - await super(KH2Context, self).connection_closed() - - async def disconnect(self, allow_autoreconnect: bool = False): - self.kh2connected = False - self.serverconneced = False - if self.kh2seedname not in {None} and self.auth not in {None}: - with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), - 'w') as f: - f.write(json.dumps(self.kh2seedsave, indent=4)) - await super(KH2Context, self).disconnect() - - @property - def endpoints(self): - if self.server: - return [self.server] - else: - return [] - - async def shutdown(self): - if self.kh2seedname not in {None} and self.auth not in {None}: - with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), - 'w') as f: - f.write(json.dumps(self.kh2seedsave, indent=4)) - await super(KH2Context, self).shutdown() - - def on_package(self, cmd: str, args: dict): - if cmd in {"RoomInfo"}: - self.kh2seedname = args['seed_name'] - if not os.path.exists(self.game_communication_path): - os.makedirs(self.game_communication_path) - if not os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"): - self.kh2seedsave = {"itemIndex": -1, - # back of soras invo is 0x25E2. Growth should be moved there - # Character: [back of invo, front of invo] - "SoraInvo": [0x25D8, 0x2546], - "DonaldInvo": [0x26F4, 0x2658], - "GoofyInvo": [0x280A, 0x276C], - "AmountInvo": { - "ServerItems": { - "Ability": {}, - "Amount": {}, - "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, - "Aerial Dodge": 0, - "Glide": 0}, - "Bitmask": [], - "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, - "Equipment": [], - "Magic": {}, - "StatIncrease": {}, - "Boost": {}, - }, - "LocalItems": { - "Ability": {}, - "Amount": {}, - "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, - "Aerial Dodge": 0, "Glide": 0}, - "Bitmask": [], - "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, - "Equipment": [], - "Magic": {}, - "StatIncrease": {}, - "Boost": {}, - }}, - # 1,3,255 are in this list in case the player gets locations in those "worlds" and I need to still have them checked - "LocationsChecked": [], - "Levels": { - "SoraLevel": 0, - "ValorLevel": 0, - "WisdomLevel": 0, - "LimitLevel": 0, - "MasterLevel": 0, - "FinalLevel": 0, - }, - "SoldEquipment": [], - "SoldBoosts": {"Power Boost": 0, - "Magic Boost": 0, - "Defense Boost": 0, - "AP Boost": 0} - } - with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), - 'wt') as f: - pass - self.locations_checked = set() - elif os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"): - with open(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json", 'r') as f: - self.kh2seedsave = json.load(f) - self.locations_checked = set(self.kh2seedsave["LocationsChecked"]) - self.serverconneced = True - - if cmd in {"Connected"}: - self.kh2slotdata = args['slot_data'] - self.kh2LocalItems = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()} - try: - self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") - logger.info("You are now auto-tracking") - self.kh2connected = True - except Exception as e: - logger.info("Line 247") - if self.kh2connected: - logger.info("Connection Lost") - self.kh2connected = False - logger.info(e) - - if cmd in {"ReceivedItems"}: - start_index = args["index"] - if start_index == 0: - # resetting everything that were sent from the server - self.kh2seedsave["SoraInvo"][0] = 0x25D8 - self.kh2seedsave["DonaldInvo"][0] = 0x26F4 - self.kh2seedsave["GoofyInvo"][0] = 0x280A - self.kh2seedsave["itemIndex"] = - 1 - self.kh2seedsave["AmountInvo"]["ServerItems"] = { - "Ability": {}, - "Amount": {}, - "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, - "Aerial Dodge": 0, - "Glide": 0}, - "Bitmask": [], - "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, - "Equipment": [], - "Magic": {}, - "StatIncrease": {}, - "Boost": {}, - } - if start_index > self.kh2seedsave["itemIndex"]: - self.kh2seedsave["itemIndex"] = start_index - for item in args['items']: - asyncio.create_task(self.give_item(item.item)) - - if cmd in {"RoomUpdate"}: - if "checked_locations" in args: - new_locations = set(args["checked_locations"]) - # TODO: make this take locations from other players on the same slot so proper coop happens - # items_to_give = [self.kh2slotdata["LocalItems"][str(location_id)] for location_id in new_locations if - # location_id in self.kh2LocalItems.keys()] - self.checked_locations |= new_locations - - async def checkWorldLocations(self): - try: - currentworldint = int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big") - if currentworldint in self.worldid: - curworldid = self.worldid[currentworldint] - for location, data in curworldid.items(): - locationId = kh2_loc_name_to_id[location] - if locationId not in self.locations_checked \ - and (int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), - "big") & 0x1 << data.bitIndex) > 0: - self.sending = self.sending + [(int(locationId))] - except Exception as e: - logger.info("Line 285") - if self.kh2connected: - logger.info("Connection Lost.") - self.kh2connected = False - logger.info(e) - - async def checkLevels(self): - try: - for location, data in SoraLevels.items(): - currentLevel = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), "big") - locationId = kh2_loc_name_to_id[location] - if locationId not in self.locations_checked \ - and currentLevel >= data.bitIndex: - if self.kh2seedsave["Levels"]["SoraLevel"] < currentLevel: - self.kh2seedsave["Levels"]["SoraLevel"] = currentLevel - self.sending = self.sending + [(int(locationId))] - formDict = { - 0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels], - 3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels]} - for i in range(5): - for location, data in formDict[i][1].items(): - formlevel = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big") - locationId = kh2_loc_name_to_id[location] - if locationId not in self.locations_checked \ - and formlevel >= data.bitIndex: - if formlevel > self.kh2seedsave["Levels"][formDict[i][0]]: - self.kh2seedsave["Levels"][formDict[i][0]] = formlevel - self.sending = self.sending + [(int(locationId))] - except Exception as e: - logger.info("Line 312") - if self.kh2connected: - logger.info("Connection Lost.") - self.kh2connected = False - logger.info(e) - - async def checkSlots(self): - try: - for location, data in weaponSlots.items(): - locationId = kh2_loc_name_to_id[location] - if locationId not in self.locations_checked: - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), - "big") > 0: - self.sending = self.sending + [(int(locationId))] - - for location, data in formSlots.items(): - locationId = kh2_loc_name_to_id[location] - if locationId not in self.locations_checked: - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), - "big") & 0x1 << data.bitIndex > 0: - # self.locations_checked - self.sending = self.sending + [(int(locationId))] - - except Exception as e: - if self.kh2connected: - logger.info("Line 333") - logger.info("Connection Lost.") - self.kh2connected = False - logger.info(e) - - async def verifyChests(self): - try: - for location in self.locations_checked: - locationName = self.lookup_id_to_Location[location] - if locationName in self.chest_set: - if locationName in self.location_name_to_worlddata.keys(): - locationData = self.location_name_to_worlddata[locationName] - if int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1), - "big") & 0x1 << locationData.bitIndex == 0: - roomData = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, - 1), "big") - self.kh2.write_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, - (roomData | 0x01 << locationData.bitIndex).to_bytes(1, 'big'), 1) - - except Exception as e: - if self.kh2connected: - logger.info("Line 350") - logger.info("Connection Lost.") - self.kh2connected = False - logger.info(e) - - async def verifyLevel(self): - for leveltype, anchor in {"SoraLevel": 0x24FF, - "ValorLevel": 0x32F6, - "WisdomLevel": 0x332E, - "LimitLevel": 0x3366, - "MasterLevel": 0x339E, - "FinalLevel": 0x33D6}.items(): - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + anchor, 1), "big") < \ - self.kh2seedsave["Levels"][leveltype]: - self.kh2.write_bytes(self.kh2.base_address + self.Save + anchor, - (self.kh2seedsave["Levels"][leveltype]).to_bytes(1, 'big'), 1) - - async def give_item(self, item, ItemType="ServerItems"): - try: - itemname = self.lookup_id_to_item[item] - itemcode = self.item_name_to_data[itemname] - if itemcode.ability: - abilityInvoType = 0 - TwilightZone = 2 - if ItemType == "LocalItems": - abilityInvoType = 1 - TwilightZone = -2 - if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}: - self.kh2seedsave["AmountInvo"][ItemType]["Growth"][itemname] += 1 - return - - if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Ability"]: - self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname] = [] - # appending the slot that the ability should be in - - if len(self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname]) < \ - self.AbilityQuantityDict[itemname]: - if itemname in self.sora_ability_set: - self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append( - self.kh2seedsave["SoraInvo"][abilityInvoType]) - self.kh2seedsave["SoraInvo"][abilityInvoType] -= TwilightZone - elif itemname in self.donald_ability_set: - self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append( - self.kh2seedsave["DonaldInvo"][abilityInvoType]) - self.kh2seedsave["DonaldInvo"][abilityInvoType] -= TwilightZone - else: - self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append( - self.kh2seedsave["GoofyInvo"][abilityInvoType]) - self.kh2seedsave["GoofyInvo"][abilityInvoType] -= TwilightZone - - elif itemcode.code in self.bitmask_item_code: - - if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"]: - self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"].append(itemname) - - elif itemcode.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}: - - if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Magic"]: - self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] += 1 - else: - self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] = 1 - elif itemname in self.all_equipment: - - self.kh2seedsave["AmountInvo"][ItemType]["Equipment"].append(itemname) - - elif itemname in self.all_weapons: - if itemname in self.keyblade_set: - self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Sora"].append(itemname) - elif itemname in self.staff_set: - self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Donald"].append(itemname) - else: - self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Goofy"].append(itemname) - - elif itemname in self.boost_set: - if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Boost"]: - self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] += 1 - else: - self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] = 1 - - elif itemname in self.stat_increase_set: - - if itemname in self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"]: - self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] += 1 - else: - self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] = 1 - - else: - if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Amount"]: - self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] += 1 - else: - self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] = 1 - - except Exception as e: - if self.kh2connected: - logger.info("Line 398") - logger.info("Connection Lost.") - self.kh2connected = False - logger.info(e) - - def run_gui(self): - """Import kivy UI system and start running it as self.ui_task.""" - from kvui import GameManager - - class KH2Manager(GameManager): - logging_pairs = [ - ("Client", "Archipelago") - ] - base_title = "Archipelago KH2 Client" - - self.ui = KH2Manager(self) - self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") - - async def IsInShop(self, sellable, master_boost): - # journal = 0x741230 shop = 0x741320 - # if journal=-1 and shop = 5 then in shop - # if journam !=-1 and shop = 10 then journal - journal = self.kh2.read_short(self.kh2.base_address + 0x741230) - shop = self.kh2.read_short(self.kh2.base_address + 0x741320) - if (journal == -1 and shop == 5) or (journal != -1 and shop == 10): - # print("your in the shop") - sellable_dict = {} - for itemName in sellable: - itemdata = self.item_name_to_data[itemName] - amount = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big") - sellable_dict[itemName] = amount - while (journal == -1 and shop == 5) or (journal != -1 and shop == 10): - journal = self.kh2.read_short(self.kh2.base_address + 0x741230) - shop = self.kh2.read_short(self.kh2.base_address + 0x741320) - await asyncio.sleep(0.5) - for item, amount in sellable_dict.items(): - itemdata = self.item_name_to_data[item] - afterShop = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big") - if afterShop < amount: - if item in master_boost: - self.kh2seedsave["SoldBoosts"][item] += (amount - afterShop) - else: - self.kh2seedsave["SoldEquipment"].append(item) - - async def verifyItems(self): - try: - local_amount = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"].keys()) - server_amount = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"].keys()) - master_amount = local_amount | server_amount - - local_ability = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"].keys()) - server_ability = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"].keys()) - master_ability = local_ability | server_ability - - local_bitmask = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Bitmask"]) - server_bitmask = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Bitmask"]) - master_bitmask = local_bitmask | server_bitmask - - local_keyblade = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Sora"]) - local_staff = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Donald"]) - local_shield = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Goofy"]) - - server_keyblade = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Sora"]) - server_staff = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Donald"]) - server_shield = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Goofy"]) - - master_keyblade = local_keyblade | server_keyblade - master_staff = local_staff | server_staff - master_shield = local_shield | server_shield - - local_equipment = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Equipment"]) - server_equipment = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Equipment"]) - master_equipment = local_equipment | server_equipment - - local_magic = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"].keys()) - server_magic = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"].keys()) - master_magic = local_magic | server_magic - - local_stat = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"].keys()) - server_stat = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"].keys()) - master_stat = local_stat | server_stat - - local_boost = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"].keys()) - server_boost = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"].keys()) - master_boost = local_boost | server_boost - - master_sell = master_equipment | master_staff | master_shield | master_boost - await asyncio.create_task(self.IsInShop(master_sell, master_boost)) - for itemName in master_amount: - itemData = self.item_name_to_data[itemName] - amountOfItems = 0 - if itemName in local_amount: - amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"][itemName] - if itemName in server_amount: - amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"][itemName] - - if itemName == "Torn Page": - # Torn Pages are handled differently because they can be consumed. - # Will check the progression in 100 acre and - the amount of visits - # amountofitems-amount of visits done - for location, data in tornPageLocks.items(): - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), - "big") & 0x1 << data.bitIndex > 0: - amountOfItems -= 1 - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != amountOfItems and amountOfItems >= 0: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - amountOfItems.to_bytes(1, 'big'), 1) - - for itemName in master_keyblade: - itemData = self.item_name_to_data[itemName] - # if the inventory slot for that keyblade is less than the amount they should have - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != 1 and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x1CFF, 1), - "big") != 13: - # Checking form anchors for the keyblade - if self.kh2.read_short(self.kh2.base_address + self.Save + 0x24F0) == itemData.kh2id \ - or self.kh2.read_short(self.kh2.base_address + self.Save + 0x32F4) == itemData.kh2id \ - or self.kh2.read_short(self.kh2.base_address + self.Save + 0x339C) == itemData.kh2id \ - or self.kh2.read_short(self.kh2.base_address + self.Save + 0x33D4) == itemData.kh2id: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (0).to_bytes(1, 'big'), 1) - else: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (1).to_bytes(1, 'big'), 1) - for itemName in master_staff: - itemData = self.item_name_to_data[itemName] - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != 1 \ - and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2604) != itemData.kh2id \ - and itemName not in self.kh2seedsave["SoldEquipment"]: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (1).to_bytes(1, 'big'), 1) - - for itemName in master_shield: - itemData = self.item_name_to_data[itemName] - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != 1 \ - and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2718) != itemData.kh2id \ - and itemName not in self.kh2seedsave["SoldEquipment"]: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (1).to_bytes(1, 'big'), 1) - - for itemName in master_ability: - itemData = self.item_name_to_data[itemName] - ability_slot = [] - if itemName in local_ability: - ability_slot += self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"][itemName] - if itemName in server_ability: - ability_slot += self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"][itemName] - for slot in ability_slot: - current = self.kh2.read_short(self.kh2.base_address + self.Save + slot) - ability = current & 0x0FFF - if ability | 0x8000 != (0x8000 + itemData.memaddr): - if current - 0x8000 > 0: - self.kh2.write_short(self.kh2.base_address + self.Save + slot, (0x8000 + itemData.memaddr)) - else: - self.kh2.write_short(self.kh2.base_address + self.Save + slot, itemData.memaddr) - # removes the duped ability if client gave faster than the game. - for charInvo in {"SoraInvo", "DonaldInvo", "GoofyInvo"}: - if self.kh2.read_short(self.kh2.base_address + self.Save + self.kh2seedsave[charInvo][1]) != 0 and \ - self.kh2seedsave[charInvo][1] + 2 < self.kh2seedsave[charInvo][0]: - self.kh2.write_short(self.kh2.base_address + self.Save + self.kh2seedsave[charInvo][1], 0) - # remove the dummy level 1 growths if they are in these invo slots. - for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}: - current = self.kh2.read_short(self.kh2.base_address + self.Save + inventorySlot) - ability = current & 0x0FFF - if 0x05E <= ability <= 0x06D: - self.kh2.write_short(self.kh2.base_address + self.Save + inventorySlot, 0) - - for itemName in self.master_growth: - growthLevel = self.kh2seedsave["AmountInvo"]["ServerItems"]["Growth"][itemName] \ - + self.kh2seedsave["AmountInvo"]["LocalItems"]["Growth"][itemName] - if growthLevel > 0: - slot = self.growth_values_dict[itemName][2] - min_growth = self.growth_values_dict[itemName][0] - max_growth = self.growth_values_dict[itemName][1] - if growthLevel > 4: - growthLevel = 4 - current_growth_level = self.kh2.read_short(self.kh2.base_address + self.Save + slot) - ability = current_growth_level & 0x0FFF - # if the player should be getting a growth ability - if ability | 0x8000 != 0x8000 + min_growth - 1 + growthLevel: - # if it should be level one of that growth - if 0x8000 + min_growth - 1 + growthLevel <= 0x8000 + min_growth or ability < min_growth: - self.kh2.write_short(self.kh2.base_address + self.Save + slot, min_growth) - # if it is already in the inventory - elif ability | 0x8000 < (0x8000 + max_growth): - self.kh2.write_short(self.kh2.base_address + self.Save + slot, current_growth_level + 1) - - for itemName in master_bitmask: - itemData = self.item_name_to_data[itemName] - itemMemory = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big") - if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") & 0x1 << itemData.bitmask) == 0: - # when getting a form anti points should be reset to 0 but bit-shift doesn't trigger the game. - if itemName in {"Valor Form", "Wisdom Form", "Limit Form", "Master Form", "Final Form"}: - self.kh2.write_bytes(self.kh2.base_address + self.Save + 0x3410, - (0).to_bytes(1, 'big'), 1) - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (itemMemory | 0x01 << itemData.bitmask).to_bytes(1, 'big'), 1) - - for itemName in master_equipment: - itemData = self.item_name_to_data[itemName] - isThere = False - if itemName in self.accessories_set: - Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"] - else: - Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"] - # Checking form anchors for the equipment - for slot in Equipment_Anchor_List: - if self.kh2.read_short(self.kh2.base_address + self.Save + slot) == itemData.kh2id: - isThere = True - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != 0: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (0).to_bytes(1, 'big'), 1) - break - if not isThere and itemName not in self.kh2seedsave["SoldEquipment"]: - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != 1: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (1).to_bytes(1, 'big'), 1) - - for itemName in master_magic: - itemData = self.item_name_to_data[itemName] - amountOfItems = 0 - if itemName in local_magic: - amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"][itemName] - if itemName in server_magic: - amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"][itemName] - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != amountOfItems \ - and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x741320, 1), "big") in {10, 8}: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - amountOfItems.to_bytes(1, 'big'), 1) - - for itemName in master_stat: - itemData = self.item_name_to_data[itemName] - amountOfItems = 0 - if itemName in local_stat: - amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"][itemName] - if itemName in server_stat: - amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"][itemName] - - # 0x130293 is Crit_1's location id for touching the computer - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != amountOfItems \ - and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Slot1 + 0x1B2, 1), - "big") >= 5 and int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x23DF, 1), - "big") > 0: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - amountOfItems.to_bytes(1, 'big'), 1) - - for itemName in master_boost: - itemData = self.item_name_to_data[itemName] - amountOfItems = 0 - if itemName in local_boost: - amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"][itemName] - if itemName in server_boost: - amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"][itemName] - amountOfBoostsInInvo = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") - amountOfUsedBoosts = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + self.boost_to_anchor_dict[itemName], 1), - "big") - # Ap Boots start at +50 for some reason - if itemName == "AP Boost": - amountOfUsedBoosts -= 50 - totalBoosts = (amountOfBoostsInInvo + amountOfUsedBoosts) - if totalBoosts <= amountOfItems - self.kh2seedsave["SoldBoosts"][ - itemName] and amountOfBoostsInInvo < 255: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (amountOfBoostsInInvo + 1).to_bytes(1, 'big'), 1) - - except Exception as e: - logger.info("Line 573") - if self.kh2connected: - logger.info("Connection Lost.") - self.kh2connected = False - logger.info(e) - - -def finishedGame(ctx: KH2Context, message): - if ctx.kh2slotdata['FinalXemnas'] == 1: - if 0x1301ED in message[0]["locations"]: - ctx.finalxemnas = True - # three proofs - if ctx.kh2slotdata['Goal'] == 0: - if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, 1), "big") > 0 \ - and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, 1), "big") > 0 \ - and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, 1), "big") > 0: - if ctx.kh2slotdata['FinalXemnas'] == 1: - if ctx.finalxemnas: - return True - else: - return False - else: - return True - else: - return False - elif ctx.kh2slotdata['Goal'] == 1: - if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x3641, 1), "big") >= \ - ctx.kh2slotdata['LuckyEmblemsRequired']: - ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1) - ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1) - ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1) - if ctx.kh2slotdata['FinalXemnas'] == 1: - if ctx.finalxemnas: - return True - else: - return False - else: - return True - else: - return False - elif ctx.kh2slotdata['Goal'] == 2: - for boss in ctx.kh2slotdata["hitlist"]: - if boss in message[0]["locations"]: - ctx.amountOfPieces += 1 - if ctx.amountOfPieces >= ctx.kh2slotdata["BountyRequired"]: - ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1) - ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1) - ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1) - if ctx.kh2slotdata['FinalXemnas'] == 1: - if ctx.finalxemnas: - return True - else: - return False - else: - return True - else: - return False - - -async def kh2_watcher(ctx: KH2Context): - while not ctx.exit_event.is_set(): - try: - if ctx.kh2connected and ctx.serverconneced: - ctx.sending = [] - await asyncio.create_task(ctx.checkWorldLocations()) - await asyncio.create_task(ctx.checkLevels()) - await asyncio.create_task(ctx.checkSlots()) - await asyncio.create_task(ctx.verifyChests()) - await asyncio.create_task(ctx.verifyItems()) - await asyncio.create_task(ctx.verifyLevel()) - message = [{"cmd": 'LocationChecks', "locations": ctx.sending}] - if finishedGame(ctx, message): - await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True - location_ids = [] - location_ids = [location for location in message[0]["locations"] if location not in location_ids] - for location in location_ids: - if location not in ctx.locations_checked: - ctx.locations_checked.add(location) - ctx.kh2seedsave["LocationsChecked"].append(location) - if location in ctx.kh2LocalItems: - item = ctx.kh2slotdata["LocalItems"][str(location)] - await asyncio.create_task(ctx.give_item(item, "LocalItems")) - await ctx.send_msgs(message) - elif not ctx.kh2connected and ctx.serverconneced: - logger.info("Game is not open. Disconnecting from Server.") - await ctx.disconnect() - except Exception as e: - logger.info("Line 661") - if ctx.kh2connected: - logger.info("Connection Lost.") - ctx.kh2connected = False - logger.info(e) - await asyncio.sleep(0.5) - - if __name__ == '__main__': - async def main(args): - ctx = KH2Context(args.connect, args.password) - ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") - if gui_enabled: - ctx.run_gui() - ctx.run_cli() - progression_watcher = asyncio.create_task( - kh2_watcher(ctx), name="KH2ProgressionWatcher") - - await ctx.exit_event.wait() - ctx.server_address = None - - await progression_watcher - - await ctx.shutdown() - - - import colorama - - parser = get_base_parser(description="KH2 Client, for text interfacing.") - - args, rest = parser.parse_known_args() - colorama.init() - asyncio.run(main(args)) - colorama.deinit() + Utils.init_logging("KH2Client", exception_logger="Client") + launch() diff --git a/setup.py b/setup.py index 0d2da0bb1818..c864a8cc9d39 100644 --- a/setup.py +++ b/setup.py @@ -71,7 +71,6 @@ "Clique", "DLCQuest", "Final Fantasy", - "Kingdom Hearts 2", "Lufia II Ancient Cave", "Meritous", "Ocarina of Time", diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 31739bb24606..03c89b75ff11 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -112,8 +112,6 @@ def launch_textclient(): # Zillion Component('Zillion Client', 'ZillionClient', file_identifier=SuffixIdentifier('.apzl')), - # Kingdom Hearts 2 - Component('KH2 Client', "KH2Client"), #MegaMan Battle Network 3 Component('MMBN3 Client', 'MMBN3Client', file_identifier=SuffixIdentifier('.apbn3')) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py new file mode 100644 index 000000000000..be85dc6907be --- /dev/null +++ b/worlds/kh2/Client.py @@ -0,0 +1,881 @@ +import ModuleUpdate + +ModuleUpdate.update() + +import os +import asyncio +import json +from pymem import pymem +from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, SupportAbility_Table, ActionAbility_Table, all_weapon_slot +from .Names import ItemName +from .WorldLocations import * + +from NetUtils import ClientStatus +from CommonClient import gui_enabled, logger, get_base_parser, CommonContext, server_loop + + +class KH2Context(CommonContext): + # command_processor: int = KH2CommandProcessor + game = "Kingdom Hearts 2" + items_handling = 0b111 # Indicates you get items sent from other worlds. + + def __init__(self, server_address, password): + super(KH2Context, self).__init__(server_address, password) + self.goofy_ability_to_slot = dict() + self.donald_ability_to_slot = dict() + self.all_weapon_location_id = None + self.sora_ability_to_slot = dict() + self.kh2_seed_save = None + self.kh2_local_items = None + self.growthlevel = None + self.kh2connected = False + self.serverconneced = False + self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()} + self.location_name_to_data = {name: data for name, data, in all_locations.items()} + self.kh2_loc_name_to_id = None + self.kh2_item_name_to_id = None + self.lookup_id_to_item = None + self.lookup_id_to_location = None + self.sora_ability_dict = {k: v.quantity for dic in [SupportAbility_Table, ActionAbility_Table] for k, v in + dic.items()} + self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()} + + self.sending = [] + # list used to keep track of locations+items player has. Used for disoneccting + self.kh2_seed_save_cache = { + "itemIndex": -1, + # back of soras invo is 0x25E2. Growth should be moved there + # Character: [back of invo, front of invo] + "SoraInvo": [0x25D8, 0x2546], + "DonaldInvo": [0x26F4, 0x2658], + "GoofyInvo": [0x2808, 0x276C], + "AmountInvo": { + "Ability": {}, + "Amount": { + "Bounty": 0, + }, + "Growth": { + "High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, + "Aerial Dodge": 0, "Glide": 0 + }, + "Bitmask": [], + "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, + "Equipment": [], + "Magic": { + "Fire Element": 0, + "Blizzard Element": 0, + "Thunder Element": 0, + "Cure Element": 0, + "Magnet Element": 0, + "Reflect Element": 0 + }, + "StatIncrease": { + ItemName.MaxHPUp: 0, + ItemName.MaxMPUp: 0, + ItemName.DriveGaugeUp: 0, + ItemName.ArmorSlotUp: 0, + ItemName.AccessorySlotUp: 0, + ItemName.ItemSlotUp: 0, + }, + }, + } + self.front_of_inventory = { + "Sora": 0x2546, + "Donald": 0x2658, + "Goofy": 0x276C, + } + self.kh2seedname = None + self.kh2slotdata = None + self.itemamount = {} + if "localappdata" in os.environ: + self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP") + self.hitlist_bounties = 0 + # hooked object + self.kh2 = None + self.final_xemnas = False + self.worldid_to_locations = { + # 1: {}, # world of darkness (story cutscenes) + 2: TT_Checks, + # 3: {}, # destiny island doesn't have checks + 4: HB_Checks, + 5: BC_Checks, + 6: Oc_Checks, + 7: AG_Checks, + 8: LoD_Checks, + 9: HundredAcreChecks, + 10: PL_Checks, + 11: Atlantica_Checks, + 12: DC_Checks, + 13: TR_Checks, + 14: HT_Checks, + 15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb + 16: PR_Checks, + 17: SP_Checks, + 18: TWTNW_Checks, + # 255: {}, # starting screen + } + # 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room + # self.sveroom = 0x2A09C00 + 0x41 + # 0 not in battle 1 in yellow battle 2 red battle #short + # self.inBattle = 0x2A0EAC4 + 0x40 + # self.onDeath = 0xAB9078 + # PC Address anchors + self.Now = 0x0714DB8 + self.Save = 0x09A70B0 + # self.Sys3 = 0x2A59DF0 + # self.Bt10 = 0x2A74880 + # self.BtlEnd = 0x2A0D3E0 + self.Slot1 = 0x2A20C98 + + self.chest_set = set(exclusion_table["Chests"]) + self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"]) + self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"]) + self.shield_set = set(CheckDupingItems["Weapons"]["Shields"]) + + self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set) + + self.equipment_categories = CheckDupingItems["Equipment"] + self.armor_set = set(self.equipment_categories["Armor"]) + self.accessories_set = set(self.equipment_categories["Accessories"]) + self.all_equipment = self.armor_set.union(self.accessories_set) + + self.Equipment_Anchor_Dict = { + "Armor": [0x2504, 0x2506, 0x2508, 0x250A], + "Accessories": [0x2514, 0x2516, 0x2518, 0x251A] + } + + self.AbilityQuantityDict = {} + self.ability_categories = CheckDupingItems["Abilities"] + + self.sora_ability_set = set(self.ability_categories["Sora"]) + self.donald_ability_set = set(self.ability_categories["Donald"]) + self.goofy_ability_set = set(self.ability_categories["Goofy"]) + + self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set) + + self.stat_increase_set = set(CheckDupingItems["Stat Increases"]) + self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities} + + # Growth:[level 1,level 4,slot] + self.growth_values_dict = { + "High Jump": [0x05E, 0x061, 0x25DA], + "Quick Run": [0x62, 0x65, 0x25DC], + "Dodge Roll": [0x234, 0x237, 0x25DE], + "Aerial Dodge": [0x66, 0x069, 0x25E0], + "Glide": [0x6A, 0x6D, 0x25E2] + } + + self.ability_code_list = None + self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"} + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(KH2Context, self).server_auth(password_requested) + await self.get_username() + await self.send_connect() + + async def connection_closed(self): + self.kh2connected = False + self.serverconneced = False + if self.kh2seedname is not None and self.auth is not None: + with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"), + 'w') as f: + f.write(json.dumps(self.kh2_seed_save, indent=4)) + await super(KH2Context, self).connection_closed() + + async def disconnect(self, allow_autoreconnect: bool = False): + self.kh2connected = False + self.serverconneced = False + if self.kh2seedname not in {None} and self.auth not in {None}: + with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"), + 'w') as f: + f.write(json.dumps(self.kh2_seed_save, indent=4)) + await super(KH2Context, self).disconnect() + + @property + def endpoints(self): + if self.server: + return [self.server] + else: + return [] + + async def shutdown(self): + if self.kh2seedname not in {None} and self.auth not in {None}: + with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"), + 'w') as f: + f.write(json.dumps(self.kh2_seed_save, indent=4)) + await super(KH2Context, self).shutdown() + + def kh2_read_short(self, address): + return self.kh2.read_short(self.kh2.base_address + address) + + def kh2_write_short(self, address, value): + return self.kh2.write_short(self.kh2.base_address + address, value) + + def kh2_write_byte(self, address, value): + return self.kh2.write_bytes(self.kh2.base_address + address, value.to_bytes(1, 'big'), 1) + + def kh2_read_byte(self, address): + return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1), "big") + + def on_package(self, cmd: str, args: dict): + if cmd in {"RoomInfo"}: + self.kh2seedname = args['seed_name'] + if not os.path.exists(self.game_communication_path): + os.makedirs(self.game_communication_path) + if not os.path.exists(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json"): + self.kh2_seed_save = { + "Levels": { + "SoraLevel": 0, + "ValorLevel": 0, + "WisdomLevel": 0, + "LimitLevel": 0, + "MasterLevel": 0, + "FinalLevel": 0, + "SummonLevel": 0, + }, + "SoldEquipment": [], + } + with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"), + 'wt') as f: + pass + # self.locations_checked = set() + elif os.path.exists(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json"): + with open(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json", 'r') as f: + self.kh2_seed_save = json.load(f) + if self.kh2_seed_save is None: + self.kh2_seed_save = { + "Levels": { + "SoraLevel": 0, + "ValorLevel": 0, + "WisdomLevel": 0, + "LimitLevel": 0, + "MasterLevel": 0, + "FinalLevel": 0, + "SummonLevel": 0, + }, + "SoldEquipment": [], + } + # self.locations_checked = set(self.kh2_seed_save_cache["LocationsChecked"]) + # self.serverconneced = True + + if cmd in {"Connected"}: + asyncio.create_task(self.send_msgs([{"cmd": "GetDataPackage", "games": ["Kingdom Hearts 2"]}])) + self.kh2slotdata = args['slot_data'] + # self.kh2_local_items = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()} + self.locations_checked = set(args["checked_locations"]) + + if cmd in {"ReceivedItems"}: + # 0x2546 + # 0x2658 + # 0x276A + start_index = args["index"] + if start_index == 0: + self.kh2_seed_save_cache = { + "itemIndex": -1, + # back of soras invo is 0x25E2. Growth should be moved there + # Character: [back of invo, front of invo] + "SoraInvo": [0x25D8, 0x2546], + "DonaldInvo": [0x26F4, 0x2658], + "GoofyInvo": [0x2808, 0x276C], + "AmountInvo": { + "Ability": {}, + "Amount": { + "Bounty": 0, + }, + "Growth": { + "High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, + "Aerial Dodge": 0, "Glide": 0 + }, + "Bitmask": [], + "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, + "Equipment": [], + "Magic": { + "Fire Element": 0, + "Blizzard Element": 0, + "Thunder Element": 0, + "Cure Element": 0, + "Magnet Element": 0, + "Reflect Element": 0 + }, + "StatIncrease": { + ItemName.MaxHPUp: 0, + ItemName.MaxMPUp: 0, + ItemName.DriveGaugeUp: 0, + ItemName.ArmorSlotUp: 0, + ItemName.AccessorySlotUp: 0, + ItemName.ItemSlotUp: 0, + }, + }, + } + if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconneced: + self.kh2_seed_save_cache["itemIndex"] = start_index + for item in args['items']: + asyncio.create_task(self.give_item(item.item, item.location)) + + if cmd in {"RoomUpdate"}: + if "checked_locations" in args: + new_locations = set(args["checked_locations"]) + self.locations_checked |= new_locations + + if cmd in {"DataPackage"}: + self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"] + self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()} + self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"] + self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()} + self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]] + + if "keyblade_abilities" in self.kh2slotdata.keys(): + sora_ability_dict = self.kh2slotdata["KeybladeAbilities"] + # sora ability to slot + # itemid:[slots that are available for that item] + for k, v in sora_ability_dict.items(): + if v >= 1: + if k not in self.sora_ability_to_slot.keys(): + self.sora_ability_to_slot[k] = [] + for _ in range(sora_ability_dict[k]): + self.sora_ability_to_slot[k].append(self.kh2_seed_save_cache["SoraInvo"][0]) + self.kh2_seed_save_cache["SoraInvo"][0] -= 2 + donald_ability_dict = self.kh2slotdata["StaffAbilities"] + for k, v in donald_ability_dict.items(): + if v >= 1: + if k not in self.donald_ability_to_slot.keys(): + self.donald_ability_to_slot[k] = [] + for _ in range(donald_ability_dict[k]): + self.donald_ability_to_slot[k].append(self.kh2_seed_save_cache["DonaldInvo"][0]) + self.kh2_seed_save_cache["DonaldInvo"][0] -= 2 + goofy_ability_dict = self.kh2slotdata["ShieldAbilities"] + for k, v in goofy_ability_dict.items(): + if v >= 1: + if k not in self.goofy_ability_to_slot.keys(): + self.goofy_ability_to_slot[k] = [] + for _ in range(goofy_ability_dict[k]): + self.goofy_ability_to_slot[k].append(self.kh2_seed_save_cache["GoofyInvo"][0]) + self.kh2_seed_save_cache["GoofyInvo"][0] -= 2 + + all_weapon_location_id = [] + for weapon_location in all_weapon_slot: + all_weapon_location_id.append(self.kh2_loc_name_to_id[weapon_location]) + self.all_weapon_location_id = set(all_weapon_location_id) + try: + self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") + logger.info("You are now auto-tracking") + self.kh2connected = True + + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info("Game is not open.") + self.serverconneced = True + asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}])) + + async def checkWorldLocations(self): + try: + currentworldint = self.kh2_read_byte(self.Now) + await self.send_msgs([{ + "cmd": "Set", "key": "Slot: " + str(self.slot) + " :CurrentWorld", + "default": 0, "want_reply": True, "operations": [{ + "operation": "replace", + "value": currentworldint + }] + }]) + if currentworldint in self.worldid_to_locations: + curworldid = self.worldid_to_locations[currentworldint] + for location, data in curworldid.items(): + if location in self.kh2_loc_name_to_id.keys(): + locationId = self.kh2_loc_name_to_id[location] + if locationId not in self.locations_checked \ + and self.kh2_read_byte(self.Save + data.addrObtained) & 0x1 << data.bitIndex > 0: + self.sending = self.sending + [(int(locationId))] + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info(e) + logger.info("line 425") + + async def checkLevels(self): + try: + for location, data in SoraLevels.items(): + currentLevel = self.kh2_read_byte(self.Save + 0x24FF) + locationId = self.kh2_loc_name_to_id[location] + if locationId not in self.locations_checked \ + and currentLevel >= data.bitIndex: + if self.kh2_seed_save["Levels"]["SoraLevel"] < currentLevel: + self.kh2_seed_save["Levels"]["SoraLevel"] = currentLevel + self.sending = self.sending + [(int(locationId))] + formDict = { + 0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels], + 3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels], 5: ["SummonLevel", SummonLevels] + } + # TODO: remove formDict[i][0] in self.kh2_seed_save_cache["Levels"].keys() after 4.3 + for i in range(6): + for location, data in formDict[i][1].items(): + formlevel = self.kh2_read_byte(self.Save + data.addrObtained) + if location in self.kh2_loc_name_to_id.keys(): + # if current form level is above other form level + locationId = self.kh2_loc_name_to_id[location] + if locationId not in self.locations_checked \ + and formlevel >= data.bitIndex: + if formlevel > self.kh2_seed_save["Levels"][formDict[i][0]]: + self.kh2_seed_save["Levels"][formDict[i][0]] = formlevel + self.sending = self.sending + [(int(locationId))] + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info(e) + logger.info("line 456") + + async def checkSlots(self): + try: + for location, data in weaponSlots.items(): + locationId = self.kh2_loc_name_to_id[location] + if locationId not in self.locations_checked: + if self.kh2_read_byte(self.Save + data.addrObtained) > 0: + self.sending = self.sending + [(int(locationId))] + + for location, data in formSlots.items(): + locationId = self.kh2_loc_name_to_id[location] + if locationId not in self.locations_checked and self.kh2_read_byte(self.Save + 0x06B2) == 0: + if self.kh2_read_byte(self.Save + data.addrObtained) & 0x1 << data.bitIndex > 0: + self.sending = self.sending + [(int(locationId))] + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info(e) + logger.info("line 475") + + async def verifyChests(self): + try: + for location in self.locations_checked: + locationName = self.lookup_id_to_location[location] + if locationName in self.chest_set: + if locationName in self.location_name_to_worlddata.keys(): + locationData = self.location_name_to_worlddata[locationName] + if self.kh2_read_byte(self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0: + roomData = self.kh2_read_byte(self.Save + locationData.addrObtained) + self.kh2_write_byte(self.Save + locationData.addrObtained, roomData | 0x01 << locationData.bitIndex) + + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info(e) + logger.info("line 491") + + async def verifyLevel(self): + for leveltype, anchor in { + "SoraLevel": 0x24FF, + "ValorLevel": 0x32F6, + "WisdomLevel": 0x332E, + "LimitLevel": 0x3366, + "MasterLevel": 0x339E, + "FinalLevel": 0x33D6 + }.items(): + if self.kh2_read_byte(self.Save + anchor) < self.kh2_seed_save["Levels"][leveltype]: + self.kh2_write_byte(self.Save + anchor, self.kh2_seed_save["Levels"][leveltype]) + + async def give_item(self, item, location): + try: + # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites + itemname = self.lookup_id_to_item[item] + itemdata = self.item_name_to_data[itemname] + # itemcode = self.kh2_item_name_to_id[itemname] + if itemdata.ability: + if location in self.all_weapon_location_id: + return + if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}: + self.kh2_seed_save_cache["AmountInvo"]["Growth"][itemname] += 1 + return + + if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]: + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = [] + # appending the slot that the ability should be in + # for non beta. remove after 4.3 + if "PoptrackerVersion" in self.kh2slotdata: + if self.kh2slotdata["PoptrackerVersionCheck"] < 4.3: + if (itemname in self.sora_ability_set + and len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < self.item_name_to_data[itemname].quantity) \ + and self.kh2_seed_save_cache["SoraInvo"][1] > 0x254C: + ability_slot = self.kh2_seed_save_cache["SoraInvo"][1] + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) + self.kh2_seed_save_cache["SoraInvo"][1] -= 2 + elif itemname in self.donald_ability_set: + ability_slot = self.kh2_seed_save_cache["DonaldInvo"][1] + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) + self.kh2_seed_save_cache["DonaldInvo"][1] -= 2 + else: + ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1] + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) + self.kh2_seed_save_cache["GoofyInvo"][1] -= 2 + + elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \ + self.AbilityQuantityDict[itemname]: + if itemname in self.sora_ability_set: + ability_slot = self.kh2_seed_save_cache["SoraInvo"][0] + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) + self.kh2_seed_save_cache["SoraInvo"][0] -= 2 + elif itemname in self.donald_ability_set: + ability_slot = self.kh2_seed_save_cache["DonaldInvo"][0] + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) + self.kh2_seed_save_cache["DonaldInvo"][0] -= 2 + elif itemname in self.goofy_ability_set: + ability_slot = self.kh2_seed_save_cache["GoofyInvo"][0] + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) + self.kh2_seed_save_cache["GoofyInvo"][0] -= 2 + + elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}: + # if memaddr is in a bitmask location in memory + if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]: + self.kh2_seed_save_cache["AmountInvo"]["Bitmask"].append(itemname) + + elif itemdata.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}: + # if memaddr is in magic addresses + self.kh2_seed_save_cache["AmountInvo"]["Magic"][itemname] += 1 + + elif itemname in self.all_equipment: + self.kh2_seed_save_cache["AmountInvo"]["Equipment"].append(itemname) + + elif itemname in self.all_weapons: + if itemname in self.keyblade_set: + self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"].append(itemname) + elif itemname in self.staff_set: + self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Donald"].append(itemname) + else: + self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Goofy"].append(itemname) + + elif itemname in self.stat_increase_set: + self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][itemname] += 1 + else: + if itemname in self.kh2_seed_save_cache["AmountInvo"]["Amount"]: + self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] += 1 + else: + self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] = 1 + + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info(e) + logger.info("line 582") + + def run_gui(self): + """Import kivy UI system and start running it as self.ui_task.""" + from kvui import GameManager + + class KH2Manager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago KH2 Client" + + self.ui = KH2Manager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + async def IsInShop(self, sellable): + # journal = 0x741230 shop = 0x741320 + # if journal=-1 and shop = 5 then in shop + # if journal !=-1 and shop = 10 then journal + + journal = self.kh2_read_short(0x741230) + shop = self.kh2_read_short(0x741320) + if (journal == -1 and shop == 5) or (journal != -1 and shop == 10): + # print("your in the shop") + sellable_dict = {} + for itemName in sellable: + itemdata = self.item_name_to_data[itemName] + amount = self.kh2_read_byte(self.Save + itemdata.memaddr) + sellable_dict[itemName] = amount + while (journal == -1 and shop == 5) or (journal != -1 and shop == 10): + journal = self.kh2_read_short(0x741230) + shop = self.kh2_read_short(0x741320) + await asyncio.sleep(0.5) + for item, amount in sellable_dict.items(): + itemdata = self.item_name_to_data[item] + afterShop = self.kh2_read_byte(self.Save + itemdata.memaddr) + if afterShop < amount: + self.kh2_seed_save["SoldEquipment"].append(item) + + async def verifyItems(self): + try: + master_amount = set(self.kh2_seed_save_cache["AmountInvo"]["Amount"].keys()) + + master_ability = set(self.kh2_seed_save_cache["AmountInvo"]["Ability"].keys()) + + master_bitmask = set(self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]) + + master_keyblade = set(self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"]) + master_staff = set(self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Donald"]) + master_shield = set(self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Goofy"]) + + master_equipment = set(self.kh2_seed_save_cache["AmountInvo"]["Equipment"]) + + master_magic = set(self.kh2_seed_save_cache["AmountInvo"]["Magic"].keys()) + + master_stat = set(self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"].keys()) + + master_sell = master_equipment | master_staff | master_shield + + await asyncio.create_task(self.IsInShop(master_sell)) + + for item_name in master_amount: + item_data = self.item_name_to_data[item_name] + amount_of_items = 0 + amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Amount"][item_name] + + if item_name == "Torn Page": + # Torn Pages are handled differently because they can be consumed. + # Will check the progression in 100 acre and - the amount of visits + # amountofitems-amount of visits done + for location, data in tornPageLocks.items(): + if self.kh2_read_byte(self.Save + data.addrObtained) & 0x1 << data.bitIndex > 0: + amount_of_items -= 1 + if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and amount_of_items >= 0: + self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) + + for item_name in master_keyblade: + item_data = self.item_name_to_data[item_name] + # if the inventory slot for that keyblade is less than the amount they should have, + # and they are not in stt + if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte(self.Save + 0x1CFF) != 13: + # Checking form anchors for the keyblade to remove extra keyblades + if self.kh2_read_short(self.Save + 0x24F0) == item_data.kh2id \ + or self.kh2_read_short(self.Save + 0x32F4) == item_data.kh2id \ + or self.kh2_read_short(self.Save + 0x339C) == item_data.kh2id \ + or self.kh2_read_short(self.Save + 0x33D4) == item_data.kh2id: + self.kh2_write_byte(self.Save + item_data.memaddr, 0) + else: + self.kh2_write_byte(self.Save + item_data.memaddr, 1) + + for item_name in master_staff: + item_data = self.item_name_to_data[item_name] + if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 \ + and self.kh2_read_short(self.Save + 0x2604) != item_data.kh2id \ + and item_name not in self.kh2_seed_save["SoldEquipment"]: + self.kh2_write_byte(self.Save + item_data.memaddr, 1) + + for item_name in master_shield: + item_data = self.item_name_to_data[item_name] + if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 \ + and self.kh2_read_short(self.Save + 0x2718) != item_data.kh2id \ + and item_name not in self.kh2_seed_save["SoldEquipment"]: + self.kh2_write_byte(self.Save + item_data.memaddr, 1) + + for item_name in master_ability: + item_data = self.item_name_to_data[item_name] + ability_slot = [] + ability_slot += self.kh2_seed_save_cache["AmountInvo"]["Ability"][item_name] + for slot in ability_slot: + current = self.kh2_read_short(self.Save + slot) + ability = current & 0x0FFF + if ability | 0x8000 != (0x8000 + item_data.memaddr): + if current - 0x8000 > 0: + self.kh2_write_short(self.Save + slot, 0x8000 + item_data.memaddr) + else: + self.kh2_write_short(self.Save + slot, item_data.memaddr) + # removes the duped ability if client gave faster than the game. + + for charInvo in {"Sora", "Donald", "Goofy"}: + if self.kh2_read_short(self.Save + self.front_of_inventory[charInvo]) != 0: + print(f"removed {self.Save + self.front_of_inventory[charInvo]} from {charInvo}") + self.kh2_write_short(self.Save + self.front_of_inventory[charInvo], 0) + + # remove the dummy level 1 growths if they are in these invo slots. + for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}: + current = self.kh2_read_short(self.Save + inventorySlot) + ability = current & 0x0FFF + if 0x05E <= ability <= 0x06D: + self.kh2_write_short(self.Save + inventorySlot, 0) + + for item_name in self.master_growth: + growthLevel = self.kh2_seed_save_cache["AmountInvo"]["Growth"][item_name] + if growthLevel > 0: + slot = self.growth_values_dict[item_name][2] + min_growth = self.growth_values_dict[item_name][0] + max_growth = self.growth_values_dict[item_name][1] + if growthLevel > 4: + growthLevel = 4 + current_growth_level = self.kh2_read_short(self.Save + slot) + ability = current_growth_level & 0x0FFF + + # if the player should be getting a growth ability + if ability | 0x8000 != 0x8000 + min_growth - 1 + growthLevel: + # if it should be level one of that growth + if 0x8000 + min_growth - 1 + growthLevel <= 0x8000 + min_growth or ability < min_growth: + self.kh2_write_short(self.Save + slot, min_growth) + # if it is already in the inventory + elif ability | 0x8000 < (0x8000 + max_growth): + self.kh2_write_short(self.Save + slot, current_growth_level + 1) + + for item_name in master_bitmask: + item_data = self.item_name_to_data[item_name] + itemMemory = self.kh2_read_byte(self.Save + item_data.memaddr) + if self.kh2_read_byte(self.Save + item_data.memaddr) & 0x1 << item_data.bitmask == 0: + # when getting a form anti points should be reset to 0 but bit-shift doesn't trigger the game. + if item_name in {"Valor Form", "Wisdom Form", "Limit Form", "Master Form", "Final Form"}: + self.kh2_write_byte(self.Save + 0x3410, 0) + self.kh2_write_byte(self.Save + item_data.memaddr, itemMemory | 0x01 << item_data.bitmask) + + for item_name in master_equipment: + item_data = self.item_name_to_data[item_name] + is_there = False + if item_name in self.accessories_set: + Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"] + else: + Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"] + # Checking form anchors for the equipment + for slot in Equipment_Anchor_List: + if self.kh2_read_short(self.Save + slot) == item_data.kh2id: + is_there = True + if self.kh2_read_byte(self.Save + item_data.memaddr) != 0: + self.kh2_write_byte(self.Save + item_data.memaddr, 0) + break + if not is_there and item_name not in self.kh2_seed_save["SoldEquipment"]: + if self.kh2_read_byte(self.Save + item_data.memaddr) != 1: + self.kh2_write_byte(self.Save + item_data.memaddr, 1) + + for item_name in master_magic: + item_data = self.item_name_to_data[item_name] + amount_of_items = 0 + amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name] + if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(0x741320) in {10, 8}: + self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) + + for item_name in master_stat: + item_data = self.item_name_to_data[item_name] + amount_of_items = 0 + amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][item_name] + + # if slot1 has 5 drive gauge and goa lost illusion is checked and they are not in a cutscene + if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \ + and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \ + self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}: + self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) + if "PoptrackerVersionCheck" in self.kh2slotdata: + if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3 + self.kh2_write_byte(self.Save + 0x3607, 1) + + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info(e) + logger.info("line 840") + + +def finishedGame(ctx: KH2Context, message): + if ctx.kh2slotdata['FinalXemnas'] == 1: + if not ctx.final_xemnas and ctx.kh2_loc_name_to_id[LocationName.FinalXemnas] in ctx.locations_checked: + ctx.final_xemnas = True + # three proofs + if ctx.kh2slotdata['Goal'] == 0: + if ctx.kh2_read_byte(ctx.Save + 0x36B2) > 0 \ + and ctx.kh2_read_byte(ctx.Save + 0x36B3) > 0 \ + and ctx.kh2_read_byte(ctx.Save + 0x36B4) > 0: + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.final_xemnas: + return True + return False + return True + return False + elif ctx.kh2slotdata['Goal'] == 1: + if ctx.kh2_read_byte(ctx.Save + 0x3641) >= ctx.kh2slotdata['LuckyEmblemsRequired']: + if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1: + ctx.kh2_write_byte(ctx.Save + 0x36B2, 1) + ctx.kh2_write_byte(ctx.Save + 0x36B3, 1) + ctx.kh2_write_byte(ctx.Save + 0x36B4, 1) + logger.info("The Final Door is now Open") + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.final_xemnas: + return True + return False + return True + return False + elif ctx.kh2slotdata['Goal'] == 2: + # for backwards compat + if "hitlist" in ctx.kh2slotdata: + for boss in ctx.kh2slotdata["hitlist"]: + if boss in message[0]["locations"]: + ctx.hitlist_bounties += 1 + if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"]: + if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1: + ctx.kh2_write_byte(ctx.Save + 0x36B2, 1) + ctx.kh2_write_byte(ctx.Save + 0x36B3, 1) + ctx.kh2_write_byte(ctx.Save + 0x36B4, 1) + logger.info("The Final Door is now Open") + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.final_xemnas: + return True + return False + return True + return False + elif ctx.kh2slotdata["Goal"] == 3: + if ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"] and \ + ctx.kh2_read_byte(ctx.Save + 0x3641) >= ctx.kh2slotdata['LuckyEmblemsRequired']: + if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1: + ctx.kh2_write_byte(ctx.Save + 0x36B2, 1) + ctx.kh2_write_byte(ctx.Save + 0x36B3, 1) + ctx.kh2_write_byte(ctx.Save + 0x36B4, 1) + logger.info("The Final Door is now Open") + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.final_xemnas: + return True + return False + return True + return False + + +async def kh2_watcher(ctx: KH2Context): + while not ctx.exit_event.is_set(): + try: + if ctx.kh2connected and ctx.serverconneced: + ctx.sending = [] + await asyncio.create_task(ctx.checkWorldLocations()) + await asyncio.create_task(ctx.checkLevels()) + await asyncio.create_task(ctx.checkSlots()) + await asyncio.create_task(ctx.verifyChests()) + await asyncio.create_task(ctx.verifyItems()) + await asyncio.create_task(ctx.verifyLevel()) + message = [{"cmd": 'LocationChecks', "locations": ctx.sending}] + if finishedGame(ctx, message): + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + await ctx.send_msgs(message) + elif not ctx.kh2connected and ctx.serverconneced: + logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.") + ctx.kh2 = None + while not ctx.kh2connected and ctx.serverconneced: + await asyncio.sleep(15) + ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") + if ctx.kh2 is not None: + logger.info("You are now auto-tracking") + ctx.kh2connected = True + except Exception as e: + if ctx.kh2connected: + ctx.kh2connected = False + logger.info(e) + logger.info("line 940") + await asyncio.sleep(0.5) + + +def launch(): + async def main(args): + ctx = KH2Context(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + progression_watcher = asyncio.create_task( + kh2_watcher(ctx), name="KH2ProgressionWatcher") + + await ctx.exit_event.wait() + ctx.server_address = None + + await progression_watcher + + await ctx.shutdown() + + import colorama + + parser = get_base_parser(description="KH2 Client, for text interfacing.") + + args, rest = parser.parse_known_args() + colorama.init() + asyncio.run(main(args)) + colorama.deinit() diff --git a/worlds/kh2/Items.py b/worlds/kh2/Items.py index aa0e326c3da7..3e656b418bfc 100644 --- a/worlds/kh2/Items.py +++ b/worlds/kh2/Items.py @@ -9,7 +9,6 @@ class KH2Item(Item): class ItemData(typing.NamedTuple): - code: typing.Optional[int] quantity: int = 0 kh2id: int = 0 # Save+ mem addr @@ -20,336 +19,421 @@ class ItemData(typing.NamedTuple): ability: bool = False +# 0x130000 Reports_Table = { - ItemName.SecretAnsemsReport1: ItemData(0x130000, 1, 226, 0x36C4, 6), - ItemName.SecretAnsemsReport2: ItemData(0x130001, 1, 227, 0x36C4, 7), - ItemName.SecretAnsemsReport3: ItemData(0x130002, 1, 228, 0x36C5, 0), - ItemName.SecretAnsemsReport4: ItemData(0x130003, 1, 229, 0x36C5, 1), - ItemName.SecretAnsemsReport5: ItemData(0x130004, 1, 230, 0x36C5, 2), - ItemName.SecretAnsemsReport6: ItemData(0x130005, 1, 231, 0x36C5, 3), - ItemName.SecretAnsemsReport7: ItemData(0x130006, 1, 232, 0x36C5, 4), - ItemName.SecretAnsemsReport8: ItemData(0x130007, 1, 233, 0x36C5, 5), - ItemName.SecretAnsemsReport9: ItemData(0x130008, 1, 234, 0x36C5, 6), - ItemName.SecretAnsemsReport10: ItemData(0x130009, 1, 235, 0x36C5, 7), - ItemName.SecretAnsemsReport11: ItemData(0x13000A, 1, 236, 0x36C6, 0), - ItemName.SecretAnsemsReport12: ItemData(0x13000B, 1, 237, 0x36C6, 1), - ItemName.SecretAnsemsReport13: ItemData(0x13000C, 1, 238, 0x36C6, 2), + ItemName.SecretAnsemsReport1: ItemData(1, 226, 0x36C4, 6), + ItemName.SecretAnsemsReport2: ItemData(1, 227, 0x36C4, 7), + ItemName.SecretAnsemsReport3: ItemData(1, 228, 0x36C5, 0), + ItemName.SecretAnsemsReport4: ItemData(1, 229, 0x36C5, 1), + ItemName.SecretAnsemsReport5: ItemData(1, 230, 0x36C5, 2), + ItemName.SecretAnsemsReport6: ItemData(1, 231, 0x36C5, 3), + ItemName.SecretAnsemsReport7: ItemData(1, 232, 0x36C5, 4), + ItemName.SecretAnsemsReport8: ItemData(1, 233, 0x36C5, 5), + ItemName.SecretAnsemsReport9: ItemData(1, 234, 0x36C5, 6), + ItemName.SecretAnsemsReport10: ItemData(1, 235, 0x36C5, 7), + ItemName.SecretAnsemsReport11: ItemData(1, 236, 0x36C6, 0), + ItemName.SecretAnsemsReport12: ItemData(1, 237, 0x36C6, 1), + ItemName.SecretAnsemsReport13: ItemData(1, 238, 0x36C6, 2), } Progression_Table = { - ItemName.ProofofConnection: ItemData(0x13000D, 1, 593, 0x36B2), - ItemName.ProofofNonexistence: ItemData(0x13000E, 1, 594, 0x36B3), - ItemName.ProofofPeace: ItemData(0x13000F, 1, 595, 0x36B4), - ItemName.PromiseCharm: ItemData(0x130010, 1, 524, 0x3694), - ItemName.NamineSketches: ItemData(0x130011, 1, 368, 0x3642), - ItemName.CastleKey: ItemData(0x130012, 2, 460, 0x365D), # dummy 13 - ItemName.BattlefieldsofWar: ItemData(0x130013, 2, 54, 0x35AE), - ItemName.SwordoftheAncestor: ItemData(0x130014, 2, 55, 0x35AF), - ItemName.BeastsClaw: ItemData(0x130015, 2, 59, 0x35B3), - ItemName.BoneFist: ItemData(0x130016, 2, 60, 0x35B4), - ItemName.ProudFang: ItemData(0x130017, 2, 61, 0x35B5), - ItemName.SkillandCrossbones: ItemData(0x130018, 2, 62, 0x35B6), - ItemName.Scimitar: ItemData(0x130019, 2, 72, 0x35C0), - ItemName.MembershipCard: ItemData(0x13001A, 2, 369, 0x3643), - ItemName.IceCream: ItemData(0x13001B, 3, 375, 0x3649), + ItemName.ProofofConnection: ItemData(1, 593, 0x36B2), + ItemName.ProofofNonexistence: ItemData(1, 594, 0x36B3), + ItemName.ProofofPeace: ItemData(1, 595, 0x36B4), + ItemName.PromiseCharm: ItemData(1, 524, 0x3694), + ItemName.NamineSketches: ItemData(1, 368, 0x3642), + ItemName.CastleKey: ItemData(2, 460, 0x365D), # dummy 13 + ItemName.BattlefieldsofWar: ItemData(2, 54, 0x35AE), + ItemName.SwordoftheAncestor: ItemData(2, 55, 0x35AF), + ItemName.BeastsClaw: ItemData(2, 59, 0x35B3), + ItemName.BoneFist: ItemData(2, 60, 0x35B4), + ItemName.ProudFang: ItemData(2, 61, 0x35B5), + ItemName.SkillandCrossbones: ItemData(2, 62, 0x35B6), + ItemName.Scimitar: ItemData(2, 72, 0x35C0), + ItemName.MembershipCard: ItemData(2, 369, 0x3643), + ItemName.IceCream: ItemData(3, 375, 0x3649), # Changed to 3 instead of one poster, picture and ice cream respectively - ItemName.WaytotheDawn: ItemData(0x13001C, 1, 73, 0x35C1), + ItemName.WaytotheDawn: ItemData(2, 73, 0x35C1), # currently first visit locking doesn't work for twtnw.When goa is updated should be 2 - ItemName.IdentityDisk: ItemData(0x13001D, 2, 74, 0x35C2), - ItemName.TornPages: ItemData(0x13001E, 5, 32, 0x3598), + ItemName.IdentityDisk: ItemData(2, 74, 0x35C2), + ItemName.TornPages: ItemData(5, 32, 0x3598), } Forms_Table = { - ItemName.ValorForm: ItemData(0x13001F, 1, 26, 0x36C0, 1), - ItemName.WisdomForm: ItemData(0x130020, 1, 27, 0x36C0, 2), - ItemName.LimitForm: ItemData(0x130021, 1, 563, 0x36CA, 3), - ItemName.MasterForm: ItemData(0x130022, 1, 31, 0x36C0, 6), - ItemName.FinalForm: ItemData(0x130023, 1, 29, 0x36C0, 4), + ItemName.ValorForm: ItemData(1, 26, 0x36C0, 1), + ItemName.WisdomForm: ItemData(1, 27, 0x36C0, 2), + ItemName.LimitForm: ItemData(1, 563, 0x36CA, 3), + ItemName.MasterForm: ItemData(1, 31, 0x36C0, 6), + ItemName.FinalForm: ItemData(1, 29, 0x36C0, 4), + ItemName.AntiForm: ItemData(1, 30, 0x36C0, 5) } Magic_Table = { - ItemName.FireElement: ItemData(0x130024, 3, 21, 0x3594), - ItemName.BlizzardElement: ItemData(0x130025, 3, 22, 0x3595), - ItemName.ThunderElement: ItemData(0x130026, 3, 23, 0x3596), - ItemName.CureElement: ItemData(0x130027, 3, 24, 0x3597), - ItemName.MagnetElement: ItemData(0x130028, 3, 87, 0x35CF), - ItemName.ReflectElement: ItemData(0x130029, 3, 88, 0x35D0), - ItemName.Genie: ItemData(0x13002A, 1, 159, 0x36C4, 4), - ItemName.PeterPan: ItemData(0x13002B, 1, 160, 0x36C4, 5), - ItemName.Stitch: ItemData(0x13002C, 1, 25, 0x36C0, 0), - ItemName.ChickenLittle: ItemData(0x13002D, 1, 383, 0x36C0, 3), + ItemName.FireElement: ItemData(3, 21, 0x3594), + ItemName.BlizzardElement: ItemData(3, 22, 0x3595), + ItemName.ThunderElement: ItemData(3, 23, 0x3596), + ItemName.CureElement: ItemData(3, 24, 0x3597), + ItemName.MagnetElement: ItemData(3, 87, 0x35CF), + ItemName.ReflectElement: ItemData(3, 88, 0x35D0), +} +Summon_Table = { + ItemName.Genie: ItemData(1, 159, 0x36C4, 4), + ItemName.PeterPan: ItemData(1, 160, 0x36C4, 5), + ItemName.Stitch: ItemData(1, 25, 0x36C0, 0), + ItemName.ChickenLittle: ItemData(1, 383, 0x36C0, 3), } - Movement_Table = { - ItemName.HighJump: ItemData(0x13002E, 4, 94, 0x05E, 0, True), - ItemName.QuickRun: ItemData(0x13002F, 4, 98, 0x062, 0, True), - ItemName.DodgeRoll: ItemData(0x130030, 4, 564, 0x234, 0, True), - ItemName.AerialDodge: ItemData(0x130031, 4, 102, 0x066, 0, True), - ItemName.Glide: ItemData(0x130032, 4, 106, 0x06A, 0, True), + ItemName.HighJump: ItemData(4, 94, 0x05E, ability=True), + ItemName.QuickRun: ItemData(4, 98, 0x062, ability=True), + ItemName.DodgeRoll: ItemData(4, 564, 0x234, ability=True), + ItemName.AerialDodge: ItemData(4, 102, 0x066, ability=True), + ItemName.Glide: ItemData(4, 106, 0x06A, ability=True), } Keyblade_Table = { - ItemName.Oathkeeper: ItemData(0x130033, 1, 42, 0x35A2), - ItemName.Oblivion: ItemData(0x130034, 1, 43, 0x35A3), - ItemName.StarSeeker: ItemData(0x130035, 1, 480, 0x367B), - ItemName.HiddenDragon: ItemData(0x130036, 1, 481, 0x367C), - ItemName.HerosCrest: ItemData(0x130037, 1, 484, 0x367F), - ItemName.Monochrome: ItemData(0x130038, 1, 485, 0x3680), - ItemName.FollowtheWind: ItemData(0x130039, 1, 486, 0x3681), - ItemName.CircleofLife: ItemData(0x13003A, 1, 487, 0x3682), - ItemName.PhotonDebugger: ItemData(0x13003B, 1, 488, 0x3683), - ItemName.GullWing: ItemData(0x13003C, 1, 489, 0x3684), - ItemName.RumblingRose: ItemData(0x13003D, 1, 490, 0x3685), - ItemName.GuardianSoul: ItemData(0x13003E, 1, 491, 0x3686), - ItemName.WishingLamp: ItemData(0x13003F, 1, 492, 0x3687), - ItemName.DecisivePumpkin: ItemData(0x130040, 1, 493, 0x3688), - ItemName.SleepingLion: ItemData(0x130041, 1, 494, 0x3689), - ItemName.SweetMemories: ItemData(0x130042, 1, 495, 0x368A), - ItemName.MysteriousAbyss: ItemData(0x130043, 1, 496, 0x368B), - ItemName.TwoBecomeOne: ItemData(0x130044, 1, 543, 0x3698), - ItemName.FatalCrest: ItemData(0x130045, 1, 497, 0x368C), - ItemName.BondofFlame: ItemData(0x130046, 1, 498, 0x368D), - ItemName.Fenrir: ItemData(0x130047, 1, 499, 0x368E), - ItemName.UltimaWeapon: ItemData(0x130048, 1, 500, 0x368F), - ItemName.WinnersProof: ItemData(0x130049, 1, 544, 0x3699), - ItemName.Pureblood: ItemData(0x13004A, 1, 71, 0x35BF), + ItemName.Oathkeeper: ItemData(1, 42, 0x35A2), + ItemName.Oblivion: ItemData(1, 43, 0x35A3), + ItemName.StarSeeker: ItemData(1, 480, 0x367B), + ItemName.HiddenDragon: ItemData(1, 481, 0x367C), + ItemName.HerosCrest: ItemData(1, 484, 0x367F), + ItemName.Monochrome: ItemData(1, 485, 0x3680), + ItemName.FollowtheWind: ItemData(1, 486, 0x3681), + ItemName.CircleofLife: ItemData(1, 487, 0x3682), + ItemName.PhotonDebugger: ItemData(1, 488, 0x3683), + ItemName.GullWing: ItemData(1, 489, 0x3684), + ItemName.RumblingRose: ItemData(1, 490, 0x3685), + ItemName.GuardianSoul: ItemData(1, 491, 0x3686), + ItemName.WishingLamp: ItemData(1, 492, 0x3687), + ItemName.DecisivePumpkin: ItemData(1, 493, 0x3688), + ItemName.SleepingLion: ItemData(1, 494, 0x3689), + ItemName.SweetMemories: ItemData(1, 495, 0x368A), + ItemName.MysteriousAbyss: ItemData(1, 496, 0x368B), + ItemName.TwoBecomeOne: ItemData(1, 543, 0x3698), + ItemName.FatalCrest: ItemData(1, 497, 0x368C), + ItemName.BondofFlame: ItemData(1, 498, 0x368D), + ItemName.Fenrir: ItemData(1, 499, 0x368E), + ItemName.UltimaWeapon: ItemData(1, 500, 0x368F), + ItemName.WinnersProof: ItemData(1, 544, 0x3699), + ItemName.Pureblood: ItemData(1, 71, 0x35BF), } Staffs_Table = { - ItemName.Centurion2: ItemData(0x13004B, 1, 546, 0x369B), - ItemName.MeteorStaff: ItemData(0x13004C, 1, 150, 0x35F1), - ItemName.NobodyLance: ItemData(0x13004D, 1, 155, 0x35F6), - ItemName.PreciousMushroom: ItemData(0x13004E, 1, 549, 0x369E), - ItemName.PreciousMushroom2: ItemData(0x13004F, 1, 550, 0x369F), - ItemName.PremiumMushroom: ItemData(0x130050, 1, 551, 0x36A0), - ItemName.RisingDragon: ItemData(0x130051, 1, 154, 0x35F5), - ItemName.SaveTheQueen2: ItemData(0x130052, 1, 503, 0x3692), - ItemName.ShamansRelic: ItemData(0x130053, 1, 156, 0x35F7), + ItemName.Centurion2: ItemData(1, 546, 0x369B), + ItemName.MeteorStaff: ItemData(1, 150, 0x35F1), + ItemName.NobodyLance: ItemData(1, 155, 0x35F6), + ItemName.PreciousMushroom: ItemData(1, 549, 0x369E), + ItemName.PreciousMushroom2: ItemData(1, 550, 0x369F), + ItemName.PremiumMushroom: ItemData(1, 551, 0x36A0), + ItemName.RisingDragon: ItemData(1, 154, 0x35F5), + ItemName.SaveTheQueen2: ItemData(1, 503, 0x3692), + ItemName.ShamansRelic: ItemData(1, 156, 0x35F7), } Shields_Table = { - ItemName.AkashicRecord: ItemData(0x130054, 1, 146, 0x35ED), - ItemName.FrozenPride2: ItemData(0x130055, 1, 553, 0x36A2), - ItemName.GenjiShield: ItemData(0x130056, 1, 145, 0x35EC), - ItemName.MajesticMushroom: ItemData(0x130057, 1, 556, 0x36A5), - ItemName.MajesticMushroom2: ItemData(0x130058, 1, 557, 0x36A6), - ItemName.NobodyGuard: ItemData(0x130059, 1, 147, 0x35EE), - ItemName.OgreShield: ItemData(0x13005A, 1, 141, 0x35E8), - ItemName.SaveTheKing2: ItemData(0x13005B, 1, 504, 0x3693), - ItemName.UltimateMushroom: ItemData(0x13005C, 1, 558, 0x36A7), + ItemName.AkashicRecord: ItemData(1, 146, 0x35ED), + ItemName.FrozenPride2: ItemData(1, 553, 0x36A2), + ItemName.GenjiShield: ItemData(1, 145, 0x35EC), + ItemName.MajesticMushroom: ItemData(1, 556, 0x36A5), + ItemName.MajesticMushroom2: ItemData(1, 557, 0x36A6), + ItemName.NobodyGuard: ItemData(1, 147, 0x35EE), + ItemName.OgreShield: ItemData(1, 141, 0x35E8), + ItemName.SaveTheKing2: ItemData(1, 504, 0x3693), + ItemName.UltimateMushroom: ItemData(1, 558, 0x36A7), } Accessory_Table = { - ItemName.AbilityRing: ItemData(0x13005D, 1, 8, 0x3587), - ItemName.EngineersRing: ItemData(0x13005E, 1, 9, 0x3588), - ItemName.TechniciansRing: ItemData(0x13005F, 1, 10, 0x3589), - ItemName.SkillRing: ItemData(0x130060, 1, 38, 0x359F), - ItemName.SkillfulRing: ItemData(0x130061, 1, 39, 0x35A0), - ItemName.ExpertsRing: ItemData(0x130062, 1, 11, 0x358A), - ItemName.MastersRing: ItemData(0x130063, 1, 34, 0x359B), - ItemName.CosmicRing: ItemData(0x130064, 1, 52, 0x35AD), - ItemName.ExecutivesRing: ItemData(0x130065, 1, 599, 0x36B5), - ItemName.SardonyxRing: ItemData(0x130066, 1, 12, 0x358B), - ItemName.TourmalineRing: ItemData(0x130067, 1, 13, 0x358C), - ItemName.AquamarineRing: ItemData(0x130068, 1, 14, 0x358D), - ItemName.GarnetRing: ItemData(0x130069, 1, 15, 0x358E), - ItemName.DiamondRing: ItemData(0x13006A, 1, 16, 0x358F), - ItemName.SilverRing: ItemData(0x13006B, 1, 17, 0x3590), - ItemName.GoldRing: ItemData(0x13006C, 1, 18, 0x3591), - ItemName.PlatinumRing: ItemData(0x13006D, 1, 19, 0x3592), - ItemName.MythrilRing: ItemData(0x13006E, 1, 20, 0x3593), - ItemName.OrichalcumRing: ItemData(0x13006F, 1, 28, 0x359A), - ItemName.SoldierEarring: ItemData(0x130070, 1, 40, 0x35A6), - ItemName.FencerEarring: ItemData(0x130071, 1, 46, 0x35A7), - ItemName.MageEarring: ItemData(0x130072, 1, 47, 0x35A8), - ItemName.SlayerEarring: ItemData(0x130073, 1, 48, 0x35AC), - ItemName.Medal: ItemData(0x130074, 1, 53, 0x35B0), - ItemName.MoonAmulet: ItemData(0x130075, 1, 35, 0x359C), - ItemName.StarCharm: ItemData(0x130076, 1, 36, 0x359E), - ItemName.CosmicArts: ItemData(0x130077, 1, 56, 0x35B1), - ItemName.ShadowArchive: ItemData(0x130078, 1, 57, 0x35B2), - ItemName.ShadowArchive2: ItemData(0x130079, 1, 58, 0x35B7), - ItemName.FullBloom: ItemData(0x13007A, 1, 64, 0x35B9), - ItemName.FullBloom2: ItemData(0x13007B, 1, 66, 0x35BB), - ItemName.DrawRing: ItemData(0x13007C, 1, 65, 0x35BA), - ItemName.LuckyRing: ItemData(0x13007D, 1, 63, 0x35B8), + ItemName.AbilityRing: ItemData(1, 8, 0x3587), + ItemName.EngineersRing: ItemData(1, 9, 0x3588), + ItemName.TechniciansRing: ItemData(1, 10, 0x3589), + ItemName.SkillRing: ItemData(1, 38, 0x359F), + ItemName.SkillfulRing: ItemData(1, 39, 0x35A0), + ItemName.ExpertsRing: ItemData(1, 11, 0x358A), + ItemName.MastersRing: ItemData(1, 34, 0x359B), + ItemName.CosmicRing: ItemData(1, 52, 0x35AD), + ItemName.ExecutivesRing: ItemData(1, 599, 0x36B5), + ItemName.SardonyxRing: ItemData(1, 12, 0x358B), + ItemName.TourmalineRing: ItemData(1, 13, 0x358C), + ItemName.AquamarineRing: ItemData(1, 14, 0x358D), + ItemName.GarnetRing: ItemData(1, 15, 0x358E), + ItemName.DiamondRing: ItemData(1, 16, 0x358F), + ItemName.SilverRing: ItemData(1, 17, 0x3590), + ItemName.GoldRing: ItemData(1, 18, 0x3591), + ItemName.PlatinumRing: ItemData(1, 19, 0x3592), + ItemName.MythrilRing: ItemData(1, 20, 0x3593), + ItemName.OrichalcumRing: ItemData(1, 28, 0x359A), + ItemName.SoldierEarring: ItemData(1, 40, 0x35A6), + ItemName.FencerEarring: ItemData(1, 46, 0x35A7), + ItemName.MageEarring: ItemData(1, 47, 0x35A8), + ItemName.SlayerEarring: ItemData(1, 48, 0x35AC), + ItemName.Medal: ItemData(1, 53, 0x35B0), + ItemName.MoonAmulet: ItemData(1, 35, 0x359C), + ItemName.StarCharm: ItemData(1, 36, 0x359E), + ItemName.CosmicArts: ItemData(1, 56, 0x35B1), + ItemName.ShadowArchive: ItemData(1, 57, 0x35B2), + ItemName.ShadowArchive2: ItemData(1, 58, 0x35B7), + ItemName.FullBloom: ItemData(1, 64, 0x35B9), + ItemName.FullBloom2: ItemData(1, 66, 0x35BB), + ItemName.DrawRing: ItemData(1, 65, 0x35BA), + ItemName.LuckyRing: ItemData(1, 63, 0x35B8), } Armor_Table = { - ItemName.ElvenBandana: ItemData(0x13007E, 1, 67, 0x35BC), - ItemName.DivineBandana: ItemData(0x13007F, 1, 68, 0x35BD), - ItemName.ProtectBelt: ItemData(0x130080, 1, 78, 0x35C7), - ItemName.GaiaBelt: ItemData(0x130081, 1, 79, 0x35CA), - ItemName.PowerBand: ItemData(0x130082, 1, 69, 0x35BE), - ItemName.BusterBand: ItemData(0x130083, 1, 70, 0x35C6), - ItemName.CosmicBelt: ItemData(0x130084, 1, 111, 0x35D1), - ItemName.FireBangle: ItemData(0x130085, 1, 173, 0x35D7), - ItemName.FiraBangle: ItemData(0x130086, 1, 174, 0x35D8), - ItemName.FiragaBangle: ItemData(0x130087, 1, 197, 0x35D9), - ItemName.FiragunBangle: ItemData(0x130088, 1, 284, 0x35DA), - ItemName.BlizzardArmlet: ItemData(0x130089, 1, 286, 0x35DC), - ItemName.BlizzaraArmlet: ItemData(0x13008A, 1, 287, 0x35DD), - ItemName.BlizzagaArmlet: ItemData(0x13008B, 1, 288, 0x35DE), - ItemName.BlizzagunArmlet: ItemData(0x13008C, 1, 289, 0x35DF), - ItemName.ThunderTrinket: ItemData(0x13008D, 1, 291, 0x35E2), - ItemName.ThundaraTrinket: ItemData(0x13008E, 1, 292, 0x35E3), - ItemName.ThundagaTrinket: ItemData(0x13008F, 1, 293, 0x35E4), - ItemName.ThundagunTrinket: ItemData(0x130090, 1, 294, 0x35E5), - ItemName.ShockCharm: ItemData(0x130091, 1, 132, 0x35D2), - ItemName.ShockCharm2: ItemData(0x130092, 1, 133, 0x35D3), - ItemName.ShadowAnklet: ItemData(0x130093, 1, 296, 0x35F9), - ItemName.DarkAnklet: ItemData(0x130094, 1, 297, 0x35FB), - ItemName.MidnightAnklet: ItemData(0x130095, 1, 298, 0x35FC), - ItemName.ChaosAnklet: ItemData(0x130096, 1, 299, 0x35FD), - ItemName.ChampionBelt: ItemData(0x130097, 1, 305, 0x3603), - ItemName.AbasChain: ItemData(0x130098, 1, 301, 0x35FF), - ItemName.AegisChain: ItemData(0x130099, 1, 302, 0x3600), - ItemName.Acrisius: ItemData(0x13009A, 1, 303, 0x3601), - ItemName.Acrisius2: ItemData(0x13009B, 1, 307, 0x3605), - ItemName.CosmicChain: ItemData(0x13009C, 1, 308, 0x3606), - ItemName.PetiteRibbon: ItemData(0x13009D, 1, 306, 0x3604), - ItemName.Ribbon: ItemData(0x13009E, 1, 304, 0x3602), - ItemName.GrandRibbon: ItemData(0x13009F, 1, 157, 0x35D4), + ItemName.ElvenBandana: ItemData(1, 67, 0x35BC), + ItemName.DivineBandana: ItemData(1, 68, 0x35BD), + ItemName.ProtectBelt: ItemData(1, 78, 0x35C7), + ItemName.GaiaBelt: ItemData(1, 79, 0x35CA), + ItemName.PowerBand: ItemData(1, 69, 0x35BE), + ItemName.BusterBand: ItemData(1, 70, 0x35C6), + ItemName.CosmicBelt: ItemData(1, 111, 0x35D1), + ItemName.FireBangle: ItemData(1, 173, 0x35D7), + ItemName.FiraBangle: ItemData(1, 174, 0x35D8), + ItemName.FiragaBangle: ItemData(1, 197, 0x35D9), + ItemName.FiragunBangle: ItemData(1, 284, 0x35DA), + ItemName.BlizzardArmlet: ItemData(1, 286, 0x35DC), + ItemName.BlizzaraArmlet: ItemData(1, 287, 0x35DD), + ItemName.BlizzagaArmlet: ItemData(1, 288, 0x35DE), + ItemName.BlizzagunArmlet: ItemData(1, 289, 0x35DF), + ItemName.ThunderTrinket: ItemData(1, 291, 0x35E2), + ItemName.ThundaraTrinket: ItemData(1, 292, 0x35E3), + ItemName.ThundagaTrinket: ItemData(1, 293, 0x35E4), + ItemName.ThundagunTrinket: ItemData(1, 294, 0x35E5), + ItemName.ShockCharm: ItemData(1, 132, 0x35D2), + ItemName.ShockCharm2: ItemData(1, 133, 0x35D3), + ItemName.ShadowAnklet: ItemData(1, 296, 0x35F9), + ItemName.DarkAnklet: ItemData(1, 297, 0x35FB), + ItemName.MidnightAnklet: ItemData(1, 298, 0x35FC), + ItemName.ChaosAnklet: ItemData(1, 299, 0x35FD), + ItemName.ChampionBelt: ItemData(1, 305, 0x3603), + ItemName.AbasChain: ItemData(1, 301, 0x35FF), + ItemName.AegisChain: ItemData(1, 302, 0x3600), + ItemName.Acrisius: ItemData(1, 303, 0x3601), + ItemName.Acrisius2: ItemData(1, 307, 0x3605), + ItemName.CosmicChain: ItemData(1, 308, 0x3606), + ItemName.PetiteRibbon: ItemData(1, 306, 0x3604), + ItemName.Ribbon: ItemData(1, 304, 0x3602), + ItemName.GrandRibbon: ItemData(1, 157, 0x35D4), } Usefull_Table = { - ItemName.MickyMunnyPouch: ItemData(0x1300A0, 3, 535, 0x3695), # 5000 munny per - ItemName.OletteMunnyPouch: ItemData(0x1300A1, 6, 362, 0x363C), # 2500 munny per - ItemName.HadesCupTrophy: ItemData(0x1300A2, 1, 537, 0x3696), - ItemName.UnknownDisk: ItemData(0x1300A3, 1, 462, 0x365F), - ItemName.OlympusStone: ItemData(0x1300A4, 1, 370, 0x3644), - ItemName.MaxHPUp: ItemData(0x1300A5, 20, 470, 0x3671), - ItemName.MaxMPUp: ItemData(0x1300A6, 4, 471, 0x3672), - ItemName.DriveGaugeUp: ItemData(0x1300A7, 6, 472, 0x3673), - ItemName.ArmorSlotUp: ItemData(0x1300A8, 3, 473, 0x3674), - ItemName.AccessorySlotUp: ItemData(0x1300A9, 3, 474, 0x3675), - ItemName.ItemSlotUp: ItemData(0x1300AA, 5, 463, 0x3660), + ItemName.MickeyMunnyPouch: ItemData(1, 535, 0x3695), # 5000 munny per + ItemName.OletteMunnyPouch: ItemData(2, 362, 0x363C), # 2500 munny per + ItemName.HadesCupTrophy: ItemData(1, 537, 0x3696), + ItemName.UnknownDisk: ItemData(1, 462, 0x365F), + ItemName.OlympusStone: ItemData(1, 370, 0x3644), + ItemName.MaxHPUp: ItemData(20, 112, 0x3671), # 470 is DUMMY 23, 112 is Encampment Area Map + ItemName.MaxMPUp: ItemData(4, 113, 0x3672), # 471 is DUMMY 24, 113 is Village Area Map + ItemName.DriveGaugeUp: ItemData(6, 114, 0x3673), # 472 is DUMMY 25, 114 is Cornerstone Hill Map + ItemName.ArmorSlotUp: ItemData(3, 116, 0x3674), # 473 is DUMMY 26, 116 is Lilliput Map + ItemName.AccessorySlotUp: ItemData(3, 117, 0x3675), # 474 is DUMMY 27, 117 is Building Site Map + ItemName.ItemSlotUp: ItemData(5, 118, 0x3660), # 463 is DUMMY 16, 118 is Mickey’s House Map } SupportAbility_Table = { - ItemName.Scan: ItemData(0x1300AB, 2, 138, 0x08A, 0, True), - ItemName.AerialRecovery: ItemData(0x1300AC, 1, 158, 0x09E, 0, True), - ItemName.ComboMaster: ItemData(0x1300AD, 1, 539, 0x21B, 0, True), - ItemName.ComboPlus: ItemData(0x1300AE, 3, 162, 0x0A2, 0, True), - ItemName.AirComboPlus: ItemData(0x1300AF, 3, 163, 0x0A3, 0, True), - ItemName.ComboBoost: ItemData(0x1300B0, 2, 390, 0x186, 0, True), - ItemName.AirComboBoost: ItemData(0x1300B1, 2, 391, 0x187, 0, True), - ItemName.ReactionBoost: ItemData(0x1300B2, 3, 392, 0x188, 0, True), - ItemName.FinishingPlus: ItemData(0x1300B3, 3, 393, 0x189, 0, True), - ItemName.NegativeCombo: ItemData(0x1300B4, 2, 394, 0x18A, 0, True), - ItemName.BerserkCharge: ItemData(0x1300B5, 2, 395, 0x18B, 0, True), - ItemName.DamageDrive: ItemData(0x1300B6, 2, 396, 0x18C, 0, True), - ItemName.DriveBoost: ItemData(0x1300B7, 2, 397, 0x18D, 0, True), - ItemName.FormBoost: ItemData(0x1300B8, 3, 398, 0x18E, 0, True), - ItemName.SummonBoost: ItemData(0x1300B9, 1, 399, 0x18F, 0, True), - ItemName.ExperienceBoost: ItemData(0x1300BA, 2, 401, 0x191, 0, True), - ItemName.Draw: ItemData(0x1300BB, 4, 405, 0x195, 0, True), - ItemName.Jackpot: ItemData(0x1300BC, 2, 406, 0x196, 0, True), - ItemName.LuckyLucky: ItemData(0x1300BD, 3, 407, 0x197, 0, True), - ItemName.DriveConverter: ItemData(0x1300BE, 2, 540, 0x21C, 0, True), - ItemName.FireBoost: ItemData(0x1300BF, 2, 408, 0x198, 0, True), - ItemName.BlizzardBoost: ItemData(0x1300C0, 2, 409, 0x199, 0, True), - ItemName.ThunderBoost: ItemData(0x1300C1, 2, 410, 0x19A, 0, True), - ItemName.ItemBoost: ItemData(0x1300C2, 2, 411, 0x19B, 0, True), - ItemName.MPRage: ItemData(0x1300C3, 2, 412, 0x19C, 0, True), - ItemName.MPHaste: ItemData(0x1300C4, 2, 413, 0x19D, 0, True), - ItemName.MPHastera: ItemData(0x1300C5, 2, 421, 0x1A5, 0, True), - ItemName.MPHastega: ItemData(0x1300C6, 1, 422, 0x1A6, 0, True), - ItemName.Defender: ItemData(0x1300C7, 2, 414, 0x19E, 0, True), - ItemName.DamageControl: ItemData(0x1300C8, 2, 542, 0x21E, 0, True), - ItemName.NoExperience: ItemData(0x1300C9, 1, 404, 0x194, 0, True), - ItemName.LightDarkness: ItemData(0x1300CA, 1, 541, 0x21D, 0, True), - ItemName.MagicLock: ItemData(0x1300CB, 1, 403, 0x193, 0, True), - ItemName.LeafBracer: ItemData(0x1300CC, 1, 402, 0x192, 0, True), - ItemName.CombinationBoost: ItemData(0x1300CD, 1, 400, 0x190, 0, True), - ItemName.OnceMore: ItemData(0x1300CE, 1, 416, 0x1A0, 0, True), - ItemName.SecondChance: ItemData(0x1300CF, 1, 415, 0x19F, 0, True), + ItemName.Scan: ItemData(2, 138, 0x08A, ability=True), + ItemName.AerialRecovery: ItemData(1, 158, 0x09E, ability=True), + ItemName.ComboMaster: ItemData(1, 539, 0x21B, ability=True), + ItemName.ComboPlus: ItemData(3, 162, 0x0A2, ability=True), + ItemName.AirComboPlus: ItemData(3, 163, 0x0A3, ability=True), + ItemName.ComboBoost: ItemData(2, 390, 0x186, ability=True), + ItemName.AirComboBoost: ItemData(2, 391, 0x187, ability=True), + ItemName.ReactionBoost: ItemData(3, 392, 0x188, ability=True), + ItemName.FinishingPlus: ItemData(3, 393, 0x189, ability=True), + ItemName.NegativeCombo: ItemData(2, 394, 0x18A, ability=True), + ItemName.BerserkCharge: ItemData(2, 395, 0x18B, ability=True), + ItemName.DamageDrive: ItemData(2, 396, 0x18C, ability=True), + ItemName.DriveBoost: ItemData(2, 397, 0x18D, ability=True), + ItemName.FormBoost: ItemData(3, 398, 0x18E, ability=True), + ItemName.SummonBoost: ItemData(1, 399, 0x18F, ability=True), + ItemName.ExperienceBoost: ItemData(2, 401, 0x191, ability=True), + ItemName.Draw: ItemData(4, 405, 0x195, ability=True), + ItemName.Jackpot: ItemData(2, 406, 0x196, ability=True), + ItemName.LuckyLucky: ItemData(3, 407, 0x197, ability=True), + ItemName.DriveConverter: ItemData(2, 540, 0x21C, ability=True), + ItemName.FireBoost: ItemData(2, 408, 0x198, ability=True), + ItemName.BlizzardBoost: ItemData(2, 409, 0x199, ability=True), + ItemName.ThunderBoost: ItemData(2, 410, 0x19A, ability=True), + ItemName.ItemBoost: ItemData(2, 411, 0x19B, ability=True), + ItemName.MPRage: ItemData(2, 412, 0x19C, ability=True), + ItemName.MPHaste: ItemData(2, 413, 0x19D, ability=True), + ItemName.MPHastera: ItemData(2, 421, 0x1A5, ability=True), + ItemName.MPHastega: ItemData(1, 422, 0x1A6, ability=True), + ItemName.Defender: ItemData(2, 414, 0x19E, ability=True), + ItemName.DamageControl: ItemData(2, 542, 0x21E, ability=True), + ItemName.NoExperience: ItemData(0, 404, 0x194, ability=True), # quantity changed to 0 because the player starts with one always. + ItemName.LightDarkness: ItemData(1, 541, 0x21D, ability=True), + ItemName.MagicLock: ItemData(1, 403, 0x193, ability=True), + ItemName.LeafBracer: ItemData(1, 402, 0x192, ability=True), + ItemName.CombinationBoost: ItemData(1, 400, 0x190, ability=True), + ItemName.OnceMore: ItemData(1, 416, 0x1A0, ability=True), + ItemName.SecondChance: ItemData(1, 415, 0x19F, ability=True), } ActionAbility_Table = { - ItemName.Guard: ItemData(0x1300D0, 1, 82, 0x052, 0, True), - ItemName.UpperSlash: ItemData(0x1300D1, 1, 137, 0x089, 0, True), - ItemName.HorizontalSlash: ItemData(0x1300D2, 1, 271, 0x10F, 0, True), - ItemName.FinishingLeap: ItemData(0x1300D3, 1, 267, 0x10B, 0, True), - ItemName.RetaliatingSlash: ItemData(0x1300D4, 1, 273, 0x111, 0, True), - ItemName.Slapshot: ItemData(0x1300D5, 1, 262, 0x106, 0, True), - ItemName.DodgeSlash: ItemData(0x1300D6, 1, 263, 0x107, 0, True), - ItemName.FlashStep: ItemData(0x1300D7, 1, 559, 0x22F, 0, True), - ItemName.SlideDash: ItemData(0x1300D8, 1, 264, 0x108, 0, True), - ItemName.VicinityBreak: ItemData(0x1300D9, 1, 562, 0x232, 0, True), - ItemName.GuardBreak: ItemData(0x1300DA, 1, 265, 0x109, 0, True), - ItemName.Explosion: ItemData(0x1300DB, 1, 266, 0x10A, 0, True), - ItemName.AerialSweep: ItemData(0x1300DC, 1, 269, 0x10D, 0, True), - ItemName.AerialDive: ItemData(0x1300DD, 1, 560, 0x230, 0, True), - ItemName.AerialSpiral: ItemData(0x1300DE, 1, 270, 0x10E, 0, True), - ItemName.AerialFinish: ItemData(0x1300DF, 1, 272, 0x110, 0, True), - ItemName.MagnetBurst: ItemData(0x1300E0, 1, 561, 0x231, 0, True), - ItemName.Counterguard: ItemData(0x1300E1, 1, 268, 0x10C, 0, True), - ItemName.AutoValor: ItemData(0x1300E2, 1, 385, 0x181, 0, True), - ItemName.AutoWisdom: ItemData(0x1300E3, 1, 386, 0x182, 0, True), - ItemName.AutoLimit: ItemData(0x1300E4, 1, 568, 0x238, 0, True), - ItemName.AutoMaster: ItemData(0x1300E5, 1, 387, 0x183, 0, True), - ItemName.AutoFinal: ItemData(0x1300E6, 1, 388, 0x184, 0, True), - ItemName.AutoSummon: ItemData(0x1300E7, 1, 389, 0x185, 0, True), - ItemName.TrinityLimit: ItemData(0x1300E8, 1, 198, 0x0C6, 0, True), + ItemName.Guard: ItemData(1, 82, 0x052, ability=True), + ItemName.UpperSlash: ItemData(1, 137, 0x089, ability=True), + ItemName.HorizontalSlash: ItemData(1, 271, 0x10F, ability=True), + ItemName.FinishingLeap: ItemData(1, 267, 0x10B, ability=True), + ItemName.RetaliatingSlash: ItemData(1, 273, 0x111, ability=True), + ItemName.Slapshot: ItemData(1, 262, 0x106, ability=True), + ItemName.DodgeSlash: ItemData(1, 263, 0x107, ability=True), + ItemName.FlashStep: ItemData(1, 559, 0x22F, ability=True), + ItemName.SlideDash: ItemData(1, 264, 0x108, ability=True), + ItemName.VicinityBreak: ItemData(1, 562, 0x232, ability=True), + ItemName.GuardBreak: ItemData(1, 265, 0x109, ability=True), + ItemName.Explosion: ItemData(1, 266, 0x10A, ability=True), + ItemName.AerialSweep: ItemData(1, 269, 0x10D, ability=True), + ItemName.AerialDive: ItemData(1, 560, 0x230, ability=True), + ItemName.AerialSpiral: ItemData(1, 270, 0x10E, ability=True), + ItemName.AerialFinish: ItemData(1, 272, 0x110, ability=True), + ItemName.MagnetBurst: ItemData(1, 561, 0x231, ability=True), + ItemName.Counterguard: ItemData(1, 268, 0x10C, ability=True), + ItemName.AutoValor: ItemData(1, 385, 0x181, ability=True), + ItemName.AutoWisdom: ItemData(1, 386, 0x182, ability=True), + ItemName.AutoLimit: ItemData(1, 568, 0x238, ability=True), + ItemName.AutoMaster: ItemData(1, 387, 0x183, ability=True), + ItemName.AutoFinal: ItemData(1, 388, 0x184, ability=True), + ItemName.AutoSummon: ItemData(1, 389, 0x185, ability=True), + ItemName.TrinityLimit: ItemData(1, 198, 0x0C6, ability=True), } -Items_Table = { - ItemName.PowerBoost: ItemData(0x1300E9, 1, 276, 0x3666), - ItemName.MagicBoost: ItemData(0x1300EA, 1, 277, 0x3667), - ItemName.DefenseBoost: ItemData(0x1300EB, 1, 278, 0x3668), - ItemName.APBoost: ItemData(0x1300EC, 1, 279, 0x3669), +Boosts_Table = { + ItemName.PowerBoost: ItemData(1, 253, 0x359D), # 276, 0x3666, market place map + ItemName.MagicBoost: ItemData(1, 586, 0x35E0), # 277, 0x3667, dark rememberance map + ItemName.DefenseBoost: ItemData(1, 590, 0x35F8), # 278, 0x3668, depths of remembrance map + ItemName.APBoost: ItemData(1, 532, 0x35FE), # 279, 0x3669, mansion map } # These items cannot be in other games so these are done locally in kh2 DonaldAbility_Table = { - ItemName.DonaldFire: ItemData(0x1300ED, 1, 165, 0xA5, 0, True), - ItemName.DonaldBlizzard: ItemData(0x1300EE, 1, 166, 0xA6, 0, True), - ItemName.DonaldThunder: ItemData(0x1300EF, 1, 167, 0xA7, 0, True), - ItemName.DonaldCure: ItemData(0x1300F0, 1, 168, 0xA8, 0, True), - ItemName.Fantasia: ItemData(0x1300F1, 1, 199, 0xC7, 0, True), - ItemName.FlareForce: ItemData(0x1300F2, 1, 200, 0xC8, 0, True), - ItemName.DonaldMPRage: ItemData(0x1300F3, 3, 412, 0x19C, 0, True), - ItemName.DonaldJackpot: ItemData(0x1300F4, 1, 406, 0x196, 0, True), - ItemName.DonaldLuckyLucky: ItemData(0x1300F5, 3, 407, 0x197, 0, True), - ItemName.DonaldFireBoost: ItemData(0x1300F6, 2, 408, 0x198, 0, True), - ItemName.DonaldBlizzardBoost: ItemData(0x1300F7, 2, 409, 0x199, 0, True), - ItemName.DonaldThunderBoost: ItemData(0x1300F8, 2, 410, 0x19A, 0, True), - ItemName.DonaldMPHaste: ItemData(0x1300F9, 1, 413, 0x19D, 0, True), - ItemName.DonaldMPHastera: ItemData(0x1300FA, 2, 421, 0x1A5, 0, True), - ItemName.DonaldMPHastega: ItemData(0x1300FB, 2, 422, 0x1A6, 0, True), - ItemName.DonaldAutoLimit: ItemData(0x1300FC, 1, 417, 0x1A1, 0, True), - ItemName.DonaldHyperHealing: ItemData(0x1300FD, 2, 419, 0x1A3, 0, True), - ItemName.DonaldAutoHealing: ItemData(0x1300FE, 1, 420, 0x1A4, 0, True), - ItemName.DonaldItemBoost: ItemData(0x1300FF, 1, 411, 0x19B, 0, True), - ItemName.DonaldDamageControl: ItemData(0x130100, 2, 542, 0x21E, 0, True), - ItemName.DonaldDraw: ItemData(0x130101, 1, 405, 0x195, 0, True), + ItemName.DonaldFire: ItemData(1, 165, 0xA5, ability=True), + ItemName.DonaldBlizzard: ItemData(1, 166, 0xA6, ability=True), + ItemName.DonaldThunder: ItemData(1, 167, 0xA7, ability=True), + ItemName.DonaldCure: ItemData(1, 168, 0xA8, ability=True), + ItemName.Fantasia: ItemData(1, 199, 0xC7, ability=True), + ItemName.FlareForce: ItemData(1, 200, 0xC8, ability=True), + ItemName.DonaldMPRage: ItemData(1, 412, 0x19C, ability=True), # originally 3 but swapped to 1 because crit checks + ItemName.DonaldJackpot: ItemData(1, 406, 0x196, ability=True), + ItemName.DonaldLuckyLucky: ItemData(3, 407, 0x197, ability=True), + ItemName.DonaldFireBoost: ItemData(2, 408, 0x198, ability=True), + ItemName.DonaldBlizzardBoost: ItemData(2, 409, 0x199, ability=True), + ItemName.DonaldThunderBoost: ItemData(2, 410, 0x19A, ability=True), + ItemName.DonaldMPHaste: ItemData(1, 413, 0x19D, ability=True), + ItemName.DonaldMPHastera: ItemData(2, 421, 0x1A5, ability=True), + ItemName.DonaldMPHastega: ItemData(2, 422, 0x1A6, ability=True), + ItemName.DonaldAutoLimit: ItemData(1, 417, 0x1A1, ability=True), + ItemName.DonaldHyperHealing: ItemData(2, 419, 0x1A3, ability=True), + ItemName.DonaldAutoHealing: ItemData(1, 420, 0x1A4, ability=True), + ItemName.DonaldItemBoost: ItemData(1, 411, 0x19B, ability=True), + ItemName.DonaldDamageControl: ItemData(2, 542, 0x21E, ability=True), + ItemName.DonaldDraw: ItemData(1, 405, 0x195, ability=True), } + GoofyAbility_Table = { - ItemName.GoofyTornado: ItemData(0x130102, 1, 423, 0x1A7, 0, True), - ItemName.GoofyTurbo: ItemData(0x130103, 1, 425, 0x1A9, 0, True), - ItemName.GoofyBash: ItemData(0x130104, 1, 429, 0x1AD, 0, True), - ItemName.TornadoFusion: ItemData(0x130105, 1, 201, 0xC9, 0, True), - ItemName.Teamwork: ItemData(0x130106, 1, 202, 0xCA, 0, True), - ItemName.GoofyDraw: ItemData(0x130107, 1, 405, 0x195, 0, True), - ItemName.GoofyJackpot: ItemData(0x130108, 1, 406, 0x196, 0, True), - ItemName.GoofyLuckyLucky: ItemData(0x130109, 1, 407, 0x197, 0, True), - ItemName.GoofyItemBoost: ItemData(0x13010A, 2, 411, 0x19B, 0, True), - ItemName.GoofyMPRage: ItemData(0x13010B, 2, 412, 0x19C, 0, True), - ItemName.GoofyDefender: ItemData(0x13010C, 2, 414, 0x19E, 0, True), - ItemName.GoofyDamageControl: ItemData(0x13010D, 3, 542, 0x21E, 0, True), - ItemName.GoofyAutoLimit: ItemData(0x13010E, 1, 417, 0x1A1, 0, True), - ItemName.GoofySecondChance: ItemData(0x13010F, 1, 415, 0x19F, 0, True), - ItemName.GoofyOnceMore: ItemData(0x130110, 1, 416, 0x1A0, 0, True), - ItemName.GoofyAutoChange: ItemData(0x130111, 1, 418, 0x1A2, 0, True), - ItemName.GoofyHyperHealing: ItemData(0x130112, 2, 419, 0x1A3, 0, True), - ItemName.GoofyAutoHealing: ItemData(0x130113, 1, 420, 0x1A4, 0, True), - ItemName.GoofyMPHaste: ItemData(0x130114, 1, 413, 0x19D, 0, True), - ItemName.GoofyMPHastera: ItemData(0x130115, 1, 421, 0x1A5, 0, True), - ItemName.GoofyMPHastega: ItemData(0x130116, 1, 422, 0x1A6, 0, True), - ItemName.GoofyProtect: ItemData(0x130117, 2, 596, 0x254, 0, True), - ItemName.GoofyProtera: ItemData(0x130118, 2, 597, 0x255, 0, True), - ItemName.GoofyProtega: ItemData(0x130119, 2, 598, 0x256, 0, True), + ItemName.GoofyTornado: ItemData(1, 423, 0x1A7, ability=True), + ItemName.GoofyTurbo: ItemData(1, 425, 0x1A9, ability=True), + ItemName.GoofyBash: ItemData(1, 429, 0x1AD, ability=True), + ItemName.TornadoFusion: ItemData(1, 201, 0xC9, ability=True), + ItemName.Teamwork: ItemData(1, 202, 0xCA, ability=True), + ItemName.GoofyDraw: ItemData(1, 405, 0x195, ability=True), + ItemName.GoofyJackpot: ItemData(1, 406, 0x196, ability=True), + ItemName.GoofyLuckyLucky: ItemData(1, 407, 0x197, ability=True), + ItemName.GoofyItemBoost: ItemData(2, 411, 0x19B, ability=True), + ItemName.GoofyMPRage: ItemData(2, 412, 0x19C, ability=True), + ItemName.GoofyDefender: ItemData(2, 414, 0x19E, ability=True), + ItemName.GoofyDamageControl: ItemData(1, 542, 0x21E, ability=True), # originally 3 but swapped to 1 because crit checks + ItemName.GoofyAutoLimit: ItemData(1, 417, 0x1A1, ability=True), + ItemName.GoofySecondChance: ItemData(1, 415, 0x19F, ability=True), + ItemName.GoofyOnceMore: ItemData(1, 416, 0x1A0, ability=True), + ItemName.GoofyAutoChange: ItemData(1, 418, 0x1A2, ability=True), + ItemName.GoofyHyperHealing: ItemData(2, 419, 0x1A3, ability=True), + ItemName.GoofyAutoHealing: ItemData(1, 420, 0x1A4, ability=True), + ItemName.GoofyMPHaste: ItemData(1, 413, 0x19D, ability=True), + ItemName.GoofyMPHastera: ItemData(1, 421, 0x1A5, ability=True), + ItemName.GoofyMPHastega: ItemData(1, 422, 0x1A6, ability=True), + ItemName.GoofyProtect: ItemData(2, 596, 0x254, ability=True), + ItemName.GoofyProtera: ItemData(2, 597, 0x255, ability=True), + ItemName.GoofyProtega: ItemData(2, 598, 0x256, ability=True), } -Misc_Table = { - ItemName.LuckyEmblem: ItemData(0x13011A, 0, 367, 0x3641), # letter item - ItemName.Victory: ItemData(0x13011B, 0, 263, 0x111), - ItemName.Bounty: ItemData(0x13011C, 0, 461, 0, 0), # Dummy 14 - # ItemName.UniversalKey:ItemData(0x130129,0,365,0x363F,0)#Tournament Poster +Wincon_Table = { + ItemName.LuckyEmblem: ItemData(kh2id=367, memaddr=0x3641), # letter item + ItemName.Victory: ItemData(kh2id=263, memaddr=0x111), + ItemName.Bounty: ItemData(kh2id=461, memaddr=0x365E), # Dummy 14 + # ItemName.UniversalKey:ItemData(,365,0x363F,0)#Tournament Poster +} +Consumable_Table = { + ItemName.Potion: ItemData(1, 127, 0x36B8), # 1, 0x3580, piglets house map + ItemName.HiPotion: ItemData(1, 126, 0x36B9), # 2, 0x03581, rabbits house map + ItemName.Ether: ItemData(1, 128, 0x36BA), # 3, 0x3582, kangas house map + ItemName.Elixir: ItemData(1, 129, 0x36BB), # 4, 0x3583, spooky cave map + ItemName.Megalixir: ItemData(1, 124, 0x36BC), # 7, 0x3586, starry hill map + ItemName.Tent: ItemData(1, 512, 0x36BD), # 131,0x35E1, savannah map + ItemName.DriveRecovery: ItemData(1, 252, 0x36BE), # 274,0x3664, pride rock map + ItemName.HighDriveRecovery: ItemData(1, 511, 0x36BF), # 275,0x3665, oasis map +} + +Events_Table = { + ItemName.HostileProgramEvent, + ItemName.McpEvent, + ItemName.ASLarxeneEvent, + ItemName.DataLarxeneEvent, + ItemName.BarbosaEvent, + ItemName.GrimReaper1Event, + ItemName.GrimReaper2Event, + ItemName.DataLuxordEvent, + ItemName.DataAxelEvent, + ItemName.CerberusEvent, + ItemName.OlympusPeteEvent, + ItemName.HydraEvent, + ItemName.OcPainAndPanicCupEvent, + ItemName.OcCerberusCupEvent, + ItemName.HadesEvent, + ItemName.ASZexionEvent, + ItemName.DataZexionEvent, + ItemName.Oc2TitanCupEvent, + ItemName.Oc2GofCupEvent, + ItemName.Oc2CupsEvent, + ItemName.HadesCupEvents, + ItemName.PrisonKeeperEvent, + ItemName.OogieBoogieEvent, + ItemName.ExperimentEvent, + ItemName.ASVexenEvent, + ItemName.DataVexenEvent, + ItemName.ShanYuEvent, + ItemName.AnsemRikuEvent, + ItemName.StormRiderEvent, + ItemName.DataXigbarEvent, + ItemName.RoxasEvent, + ItemName.XigbarEvent, + ItemName.LuxordEvent, + ItemName.SaixEvent, + ItemName.XemnasEvent, + ItemName.ArmoredXemnasEvent, + ItemName.ArmoredXemnas2Event, + ItemName.FinalXemnasEvent, + ItemName.DataXemnasEvent, + ItemName.ThresholderEvent, + ItemName.BeastEvent, + ItemName.DarkThornEvent, + ItemName.XaldinEvent, + ItemName.DataXaldinEvent, + ItemName.TwinLordsEvent, + ItemName.GenieJafarEvent, + ItemName.ASLexaeusEvent, + ItemName.DataLexaeusEvent, + ItemName.ScarEvent, + ItemName.GroundShakerEvent, + ItemName.DataSaixEvent, + ItemName.HBDemyxEvent, + ItemName.ThousandHeartlessEvent, + ItemName.Mushroom13Event, + ItemName.SephiEvent, + ItemName.DataDemyxEvent, + ItemName.CorFirstFightEvent, + ItemName.CorSecondFightEvent, + ItemName.TransportEvent, + ItemName.OldPeteEvent, + ItemName.FuturePeteEvent, + ItemName.ASMarluxiaEvent, + ItemName.DataMarluxiaEvent, + ItemName.TerraEvent, + ItemName.TwilightThornEvent, + ItemName.Axel1Event, + ItemName.Axel2Event, + ItemName.DataRoxasEvent, } # Items that are prone to duping. # anchors for checking form keyblade @@ -358,185 +442,37 @@ class ItemData(typing.NamedTuple): # Equipped abilities have an offset of 0x8000 so check for if whatever || whatever+0x8000 CheckDupingItems = { "Items": { - ItemName.ProofofConnection, - ItemName.ProofofNonexistence, - ItemName.ProofofPeace, - ItemName.PromiseCharm, - ItemName.NamineSketches, - ItemName.CastleKey, - ItemName.BattlefieldsofWar, - ItemName.SwordoftheAncestor, - ItemName.BeastsClaw, - ItemName.BoneFist, - ItemName.ProudFang, - ItemName.SkillandCrossbones, - ItemName.Scimitar, - ItemName.MembershipCard, - ItemName.IceCream, - ItemName.WaytotheDawn, - ItemName.IdentityDisk, - ItemName.TornPages, - ItemName.LuckyEmblem, - ItemName.MickyMunnyPouch, - ItemName.OletteMunnyPouch, - ItemName.HadesCupTrophy, - ItemName.UnknownDisk, - ItemName.OlympusStone, + item_name for keys in [Progression_Table.keys(), Wincon_Table.keys(), Consumable_Table, [ItemName.MickeyMunnyPouch, + ItemName.OletteMunnyPouch, + ItemName.HadesCupTrophy, + ItemName.UnknownDisk, + ItemName.OlympusStone, ], Boosts_Table.keys()] + for item_name in keys + }, "Magic": { - ItemName.FireElement, - ItemName.BlizzardElement, - ItemName.ThunderElement, - ItemName.CureElement, - ItemName.MagnetElement, - ItemName.ReflectElement, + magic for magic in Magic_Table.keys() }, "Bitmask": { - ItemName.ValorForm, - ItemName.WisdomForm, - ItemName.LimitForm, - ItemName.MasterForm, - ItemName.FinalForm, - ItemName.Genie, - ItemName.PeterPan, - ItemName.Stitch, - ItemName.ChickenLittle, - ItemName.SecretAnsemsReport1, - ItemName.SecretAnsemsReport2, - ItemName.SecretAnsemsReport3, - ItemName.SecretAnsemsReport4, - ItemName.SecretAnsemsReport5, - ItemName.SecretAnsemsReport6, - ItemName.SecretAnsemsReport7, - ItemName.SecretAnsemsReport8, - ItemName.SecretAnsemsReport9, - ItemName.SecretAnsemsReport10, - ItemName.SecretAnsemsReport11, - ItemName.SecretAnsemsReport12, - ItemName.SecretAnsemsReport13, - + item_name for keys in [Forms_Table.keys(), Summon_Table.keys(), Reports_Table.keys()] for item_name in keys }, "Weapons": { "Keyblades": { - ItemName.Oathkeeper, - ItemName.Oblivion, - ItemName.StarSeeker, - ItemName.HiddenDragon, - ItemName.HerosCrest, - ItemName.Monochrome, - ItemName.FollowtheWind, - ItemName.CircleofLife, - ItemName.PhotonDebugger, - ItemName.GullWing, - ItemName.RumblingRose, - ItemName.GuardianSoul, - ItemName.WishingLamp, - ItemName.DecisivePumpkin, - ItemName.SleepingLion, - ItemName.SweetMemories, - ItemName.MysteriousAbyss, - ItemName.TwoBecomeOne, - ItemName.FatalCrest, - ItemName.BondofFlame, - ItemName.Fenrir, - ItemName.UltimaWeapon, - ItemName.WinnersProof, - ItemName.Pureblood, + keyblade for keyblade in Keyblade_Table.keys() }, "Staffs": { - ItemName.Centurion2, - ItemName.MeteorStaff, - ItemName.NobodyLance, - ItemName.PreciousMushroom, - ItemName.PreciousMushroom2, - ItemName.PremiumMushroom, - ItemName.RisingDragon, - ItemName.SaveTheQueen2, - ItemName.ShamansRelic, + staff for staff in Staffs_Table.keys() }, "Shields": { - ItemName.AkashicRecord, - ItemName.FrozenPride2, - ItemName.GenjiShield, - ItemName.MajesticMushroom, - ItemName.MajesticMushroom2, - ItemName.NobodyGuard, - ItemName.OgreShield, - ItemName.SaveTheKing2, - ItemName.UltimateMushroom, + shield for shield in Shields_Table.keys() } }, "Equipment": { "Accessories": { - ItemName.AbilityRing, - ItemName.EngineersRing, - ItemName.TechniciansRing, - ItemName.SkillRing, - ItemName.SkillfulRing, - ItemName.ExpertsRing, - ItemName.MastersRing, - ItemName.CosmicRing, - ItemName.ExecutivesRing, - ItemName.SardonyxRing, - ItemName.TourmalineRing, - ItemName.AquamarineRing, - ItemName.GarnetRing, - ItemName.DiamondRing, - ItemName.SilverRing, - ItemName.GoldRing, - ItemName.PlatinumRing, - ItemName.MythrilRing, - ItemName.OrichalcumRing, - ItemName.SoldierEarring, - ItemName.FencerEarring, - ItemName.MageEarring, - ItemName.SlayerEarring, - ItemName.Medal, - ItemName.MoonAmulet, - ItemName.StarCharm, - ItemName.CosmicArts, - ItemName.ShadowArchive, - ItemName.ShadowArchive2, - ItemName.FullBloom, - ItemName.FullBloom2, - ItemName.DrawRing, - ItemName.LuckyRing, + accessory for accessory in Accessory_Table.keys() }, "Armor": { - ItemName.ElvenBandana, - ItemName.DivineBandana, - ItemName.ProtectBelt, - ItemName.GaiaBelt, - ItemName.PowerBand, - ItemName.BusterBand, - ItemName.CosmicBelt, - ItemName.FireBangle, - ItemName.FiraBangle, - ItemName.FiragaBangle, - ItemName.FiragunBangle, - ItemName.BlizzardArmlet, - ItemName.BlizzaraArmlet, - ItemName.BlizzagaArmlet, - ItemName.BlizzagunArmlet, - ItemName.ThunderTrinket, - ItemName.ThundaraTrinket, - ItemName.ThundagaTrinket, - ItemName.ThundagunTrinket, - ItemName.ShockCharm, - ItemName.ShockCharm2, - ItemName.ShadowAnklet, - ItemName.DarkAnklet, - ItemName.MidnightAnklet, - ItemName.ChaosAnklet, - ItemName.ChampionBelt, - ItemName.AbasChain, - ItemName.AegisChain, - ItemName.Acrisius, - ItemName.Acrisius2, - ItemName.CosmicChain, - ItemName.PetiteRibbon, - ItemName.Ribbon, - ItemName.GrandRibbon, + armor for armor in Armor_Table.keys() } }, "Stat Increases": { @@ -549,297 +485,103 @@ class ItemData(typing.NamedTuple): }, "Abilities": { "Sora": { - ItemName.Scan, + item_name for keys in [SupportAbility_Table.keys(), ActionAbility_Table.keys(), Movement_Table.keys()] for item_name in keys + }, + "Donald": { + donald_ability for donald_ability in DonaldAbility_Table.keys() + }, + "Goofy": { + goofy_ability for goofy_ability in GoofyAbility_Table.keys() + } + }, +} +progression_set = { + # abilities + item_name for keys in [ + Wincon_Table.keys(), + Progression_Table.keys(), + Forms_Table.keys(), + Magic_Table.keys(), + Summon_Table.keys(), + Movement_Table.keys(), + Keyblade_Table.keys(), + Staffs_Table.keys(), + Shields_Table.keys(), + [ ItemName.AerialRecovery, ItemName.ComboMaster, ItemName.ComboPlus, ItemName.AirComboPlus, - ItemName.ComboBoost, - ItemName.AirComboBoost, - ItemName.ReactionBoost, ItemName.FinishingPlus, ItemName.NegativeCombo, ItemName.BerserkCharge, - ItemName.DamageDrive, - ItemName.DriveBoost, ItemName.FormBoost, - ItemName.SummonBoost, - ItemName.ExperienceBoost, - ItemName.Draw, - ItemName.Jackpot, - ItemName.LuckyLucky, - ItemName.DriveConverter, - ItemName.FireBoost, - ItemName.BlizzardBoost, - ItemName.ThunderBoost, - ItemName.ItemBoost, - ItemName.MPRage, - ItemName.MPHaste, - ItemName.MPHastera, - ItemName.MPHastega, - ItemName.Defender, - ItemName.DamageControl, - ItemName.NoExperience, ItemName.LightDarkness, - ItemName.MagicLock, - ItemName.LeafBracer, - ItemName.CombinationBoost, ItemName.OnceMore, ItemName.SecondChance, ItemName.Guard, - ItemName.UpperSlash, ItemName.HorizontalSlash, ItemName.FinishingLeap, - ItemName.RetaliatingSlash, ItemName.Slapshot, - ItemName.DodgeSlash, ItemName.FlashStep, ItemName.SlideDash, - ItemName.VicinityBreak, ItemName.GuardBreak, ItemName.Explosion, ItemName.AerialSweep, ItemName.AerialDive, ItemName.AerialSpiral, ItemName.AerialFinish, - ItemName.MagnetBurst, - ItemName.Counterguard, ItemName.AutoValor, ItemName.AutoWisdom, ItemName.AutoLimit, ItemName.AutoMaster, ItemName.AutoFinal, - ItemName.AutoSummon, ItemName.TrinityLimit, - ItemName.HighJump, - ItemName.QuickRun, - ItemName.DodgeRoll, - ItemName.AerialDodge, - ItemName.Glide, - }, - "Donald": { - ItemName.DonaldFire, - ItemName.DonaldBlizzard, - ItemName.DonaldThunder, - ItemName.DonaldCure, - ItemName.Fantasia, + ItemName.DriveConverter, + # Party Limits ItemName.FlareForce, - ItemName.DonaldMPRage, - ItemName.DonaldJackpot, - ItemName.DonaldLuckyLucky, - ItemName.DonaldFireBoost, - ItemName.DonaldBlizzardBoost, - ItemName.DonaldThunderBoost, - ItemName.DonaldMPHaste, - ItemName.DonaldMPHastera, - ItemName.DonaldMPHastega, - ItemName.DonaldAutoLimit, - ItemName.DonaldHyperHealing, - ItemName.DonaldAutoHealing, - ItemName.DonaldItemBoost, - ItemName.DonaldDamageControl, - ItemName.DonaldDraw, - }, - "Goofy": { - ItemName.GoofyTornado, - ItemName.GoofyTurbo, - ItemName.GoofyBash, - ItemName.TornadoFusion, + ItemName.Fantasia, ItemName.Teamwork, - ItemName.GoofyDraw, - ItemName.GoofyJackpot, - ItemName.GoofyLuckyLucky, - ItemName.GoofyItemBoost, - ItemName.GoofyMPRage, - ItemName.GoofyDefender, - ItemName.GoofyDamageControl, - ItemName.GoofyAutoLimit, - ItemName.GoofySecondChance, - ItemName.GoofyOnceMore, - ItemName.GoofyAutoChange, - ItemName.GoofyHyperHealing, - ItemName.GoofyAutoHealing, - ItemName.GoofyMPHaste, - ItemName.GoofyMPHastera, - ItemName.GoofyMPHastega, - ItemName.GoofyProtect, - ItemName.GoofyProtera, - ItemName.GoofyProtega, - } - }, - "Boosts": { - ItemName.PowerBoost, - ItemName.MagicBoost, - ItemName.DefenseBoost, - ItemName.APBoost, - } + ItemName.TornadoFusion, + ItemName.HadesCupTrophy], + Events_Table] + for item_name in keys } +party_filler_set = { + ItemName.GoofyAutoHealing, + ItemName.GoofyMPHaste, + ItemName.GoofyMPHastera, + ItemName.GoofyMPHastega, + ItemName.GoofyProtect, + ItemName.GoofyProtera, + ItemName.GoofyProtega, + ItemName.GoofyMPRage, + ItemName.GoofyDefender, + ItemName.GoofyDamageControl, -Progression_Dicts = { - # Items that are classified as progression - "Progression": { - # Wincons - ItemName.Victory, - ItemName.LuckyEmblem, - ItemName.Bounty, - ItemName.ProofofConnection, - ItemName.ProofofNonexistence, - ItemName.ProofofPeace, - ItemName.PromiseCharm, - # visit locking - ItemName.NamineSketches, - # dummy 13 - ItemName.CastleKey, - ItemName.BattlefieldsofWar, - ItemName.SwordoftheAncestor, - ItemName.BeastsClaw, - ItemName.BoneFist, - ItemName.ProudFang, - ItemName.SkillandCrossbones, - ItemName.Scimitar, - ItemName.MembershipCard, - ItemName.IceCream, - ItemName.WaytotheDawn, - ItemName.IdentityDisk, - ItemName.TornPages, - # forms - ItemName.ValorForm, - ItemName.WisdomForm, - ItemName.LimitForm, - ItemName.MasterForm, - ItemName.FinalForm, - # magic - ItemName.FireElement, - ItemName.BlizzardElement, - ItemName.ThunderElement, - ItemName.CureElement, - ItemName.MagnetElement, - ItemName.ReflectElement, - ItemName.Genie, - ItemName.PeterPan, - ItemName.Stitch, - ItemName.ChickenLittle, - # movement - ItemName.HighJump, - ItemName.QuickRun, - ItemName.DodgeRoll, - ItemName.AerialDodge, - ItemName.Glide, - # abilities - ItemName.Scan, - ItemName.AerialRecovery, - ItemName.ComboMaster, - ItemName.ComboPlus, - ItemName.AirComboPlus, - ItemName.ComboBoost, - ItemName.AirComboBoost, - ItemName.ReactionBoost, - ItemName.FinishingPlus, - ItemName.NegativeCombo, - ItemName.BerserkCharge, - ItemName.DamageDrive, - ItemName.DriveBoost, - ItemName.FormBoost, - ItemName.SummonBoost, - ItemName.ExperienceBoost, - ItemName.Draw, - ItemName.Jackpot, - ItemName.LuckyLucky, - ItemName.DriveConverter, - ItemName.FireBoost, - ItemName.BlizzardBoost, - ItemName.ThunderBoost, - ItemName.ItemBoost, - ItemName.MPRage, - ItemName.MPHaste, - ItemName.MPHastera, - ItemName.MPHastega, - ItemName.Defender, - ItemName.DamageControl, - ItemName.NoExperience, - ItemName.LightDarkness, - ItemName.MagicLock, - ItemName.LeafBracer, - ItemName.CombinationBoost, - ItemName.OnceMore, - ItemName.SecondChance, - ItemName.Guard, - ItemName.UpperSlash, - ItemName.HorizontalSlash, - ItemName.FinishingLeap, - ItemName.RetaliatingSlash, - ItemName.Slapshot, - ItemName.DodgeSlash, - ItemName.FlashStep, - ItemName.SlideDash, - ItemName.VicinityBreak, - ItemName.GuardBreak, - ItemName.Explosion, - ItemName.AerialSweep, - ItemName.AerialDive, - ItemName.AerialSpiral, - ItemName.AerialFinish, - ItemName.MagnetBurst, - ItemName.Counterguard, - ItemName.AutoValor, - ItemName.AutoWisdom, - ItemName.AutoLimit, - ItemName.AutoMaster, - ItemName.AutoFinal, - ItemName.AutoSummon, - ItemName.TrinityLimit, - # keyblades - ItemName.Oathkeeper, - ItemName.Oblivion, - ItemName.StarSeeker, - ItemName.HiddenDragon, - ItemName.HerosCrest, - ItemName.Monochrome, - ItemName.FollowtheWind, - ItemName.CircleofLife, - ItemName.PhotonDebugger, - ItemName.GullWing, - ItemName.RumblingRose, - ItemName.GuardianSoul, - ItemName.WishingLamp, - ItemName.DecisivePumpkin, - ItemName.SleepingLion, - ItemName.SweetMemories, - ItemName.MysteriousAbyss, - ItemName.TwoBecomeOne, - ItemName.FatalCrest, - ItemName.BondofFlame, - ItemName.Fenrir, - ItemName.UltimaWeapon, - ItemName.WinnersProof, - ItemName.Pureblood, - # Staffs - ItemName.Centurion2, - ItemName.MeteorStaff, - ItemName.NobodyLance, - ItemName.PreciousMushroom, - ItemName.PreciousMushroom2, - ItemName.PremiumMushroom, - ItemName.RisingDragon, - ItemName.SaveTheQueen2, - ItemName.ShamansRelic, - # Shields - ItemName.AkashicRecord, - ItemName.FrozenPride2, - ItemName.GenjiShield, - ItemName.MajesticMushroom, - ItemName.MajesticMushroom2, - ItemName.NobodyGuard, - ItemName.OgreShield, - ItemName.SaveTheKing2, - ItemName.UltimateMushroom, - # Party Limits - ItemName.FlareForce, - ItemName.Fantasia, - ItemName.Teamwork, - ItemName.TornadoFusion - }, - "2VisitLocking": { + ItemName.DonaldFireBoost, + ItemName.DonaldBlizzardBoost, + ItemName.DonaldThunderBoost, + ItemName.DonaldMPHaste, + ItemName.DonaldMPHastera, + ItemName.DonaldMPHastega, + ItemName.DonaldAutoHealing, + ItemName.DonaldDamageControl, + ItemName.DonaldDraw, + ItemName.DonaldMPRage, +} +useful_set = {item_name for keys in [ + SupportAbility_Table.keys(), + ActionAbility_Table.keys(), + DonaldAbility_Table.keys(), + GoofyAbility_Table.keys(), + Armor_Table.keys(), + Usefull_Table.keys(), + Accessory_Table.keys()] + for item_name in keys if item_name not in progression_set and item_name not in party_filler_set} + +visit_locking_dict = { + "2VisitLocking": [ ItemName.CastleKey, ItemName.BattlefieldsofWar, ItemName.SwordoftheAncestor, @@ -854,7 +596,7 @@ class ItemData(typing.NamedTuple): ItemName.IdentityDisk, ItemName.IceCream, ItemName.NamineSketches - }, + ], "AllVisitLocking": { ItemName.CastleKey: 2, ItemName.BattlefieldsofWar: 2, @@ -865,84 +607,13 @@ class ItemData(typing.NamedTuple): ItemName.SkillandCrossbones: 2, ItemName.Scimitar: 2, ItemName.MembershipCard: 2, - ItemName.WaytotheDawn: 1, + ItemName.WaytotheDawn: 2, ItemName.IdentityDisk: 2, ItemName.IceCream: 3, ItemName.NamineSketches: 1, } } - -exclusionItem_table = { - "Ability": { - ItemName.Scan, - ItemName.AerialRecovery, - ItemName.ComboMaster, - ItemName.ComboPlus, - ItemName.AirComboPlus, - ItemName.ComboBoost, - ItemName.AirComboBoost, - ItemName.ReactionBoost, - ItemName.FinishingPlus, - ItemName.NegativeCombo, - ItemName.BerserkCharge, - ItemName.DamageDrive, - ItemName.DriveBoost, - ItemName.FormBoost, - ItemName.SummonBoost, - ItemName.ExperienceBoost, - ItemName.Draw, - ItemName.Jackpot, - ItemName.LuckyLucky, - ItemName.DriveConverter, - ItemName.FireBoost, - ItemName.BlizzardBoost, - ItemName.ThunderBoost, - ItemName.ItemBoost, - ItemName.MPRage, - ItemName.MPHaste, - ItemName.MPHastera, - ItemName.MPHastega, - ItemName.Defender, - ItemName.DamageControl, - ItemName.NoExperience, - ItemName.LightDarkness, - ItemName.MagicLock, - ItemName.LeafBracer, - ItemName.CombinationBoost, - ItemName.DamageDrive, - ItemName.OnceMore, - ItemName.SecondChance, - ItemName.Guard, - ItemName.UpperSlash, - ItemName.HorizontalSlash, - ItemName.FinishingLeap, - ItemName.RetaliatingSlash, - ItemName.Slapshot, - ItemName.DodgeSlash, - ItemName.FlashStep, - ItemName.SlideDash, - ItemName.VicinityBreak, - ItemName.GuardBreak, - ItemName.Explosion, - ItemName.AerialSweep, - ItemName.AerialDive, - ItemName.AerialSpiral, - ItemName.AerialFinish, - ItemName.MagnetBurst, - ItemName.Counterguard, - ItemName.AutoValor, - ItemName.AutoWisdom, - ItemName.AutoLimit, - ItemName.AutoMaster, - ItemName.AutoFinal, - ItemName.AutoSummon, - ItemName.TrinityLimit, - ItemName.HighJump, - ItemName.QuickRun, - ItemName.DodgeRoll, - ItemName.AerialDodge, - ItemName.Glide, - }, +exclusion_item_table = { "StatUps": { ItemName.MaxHPUp, ItemName.MaxMPUp, @@ -951,59 +622,64 @@ class ItemData(typing.NamedTuple): ItemName.AccessorySlotUp, ItemName.ItemSlotUp, }, + "Ability": { + item_name for keys in [SupportAbility_Table.keys(), ActionAbility_Table.keys(), Movement_Table.keys()] for item_name in keys + } } -item_dictionary_table = {**Reports_Table, - **Progression_Table, - **Forms_Table, - **Magic_Table, - **Armor_Table, - **Movement_Table, - **Staffs_Table, - **Shields_Table, - **Keyblade_Table, - **Accessory_Table, - **Usefull_Table, - **SupportAbility_Table, - **ActionAbility_Table, - **Items_Table, - **Misc_Table, - **Items_Table, - **DonaldAbility_Table, - **GoofyAbility_Table, - } - -lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_dictionary_table.items() if - data.code} - -item_groups: typing.Dict[str, list] = {"Drive Form": [item_name for item_name in Forms_Table.keys()], - "Growth": [item_name for item_name in Movement_Table.keys()], - "Donald Limit": [ItemName.FlareForce, ItemName.Fantasia], - "Goofy Limit": [ItemName.Teamwork, ItemName.TornadoFusion], - "Magic": [ItemName.FireElement, ItemName.BlizzardElement, - ItemName.ThunderElement, - ItemName.CureElement, ItemName.MagnetElement, - ItemName.ReflectElement], - "Summon": [ItemName.ChickenLittle, ItemName.Genie, ItemName.Stitch, - ItemName.PeterPan], - "Gap Closer": [ItemName.SlideDash, ItemName.FlashStep], - "Ground Finisher": [ItemName.GuardBreak, ItemName.Explosion, - ItemName.FinishingLeap], - "Visit Lock": [item_name for item_name in - Progression_Dicts["2VisitLocking"]], - "Keyblade": [item_name for item_name in Keyblade_Table.keys()], - "Fire": [ItemName.FireElement], - "Blizzard": [ItemName.BlizzardElement], - "Thunder": [ItemName.ThunderElement], - "Cure": [ItemName.CureElement], - "Magnet": [ItemName.MagnetElement], - "Reflect": [ItemName.ReflectElement], - "Proof": [ItemName.ProofofNonexistence, ItemName.ProofofPeace, - ItemName.ProofofConnection], - "Filler": [ - ItemName.PowerBoost, ItemName.MagicBoost, - ItemName.DefenseBoost, ItemName.APBoost] - } - -# lookup_kh2id_to_name: typing.Dict[int, str] = {data.kh2id: item_name for item_name, data in -# item_dictionary_table.items() if data.kh2id} +default_itempool_option = { + item_name: ItemData.quantity for dic in [Magic_Table, Progression_Table, Summon_Table, Movement_Table, Forms_Table] for item_name, ItemData in dic.items() +} +item_dictionary_table = { + **Reports_Table, + **Progression_Table, + **Forms_Table, + **Magic_Table, + **Summon_Table, + **Armor_Table, + **Movement_Table, + **Staffs_Table, + **Shields_Table, + **Keyblade_Table, + **Accessory_Table, + **Usefull_Table, + **SupportAbility_Table, + **ActionAbility_Table, + **Boosts_Table, + **Wincon_Table, + **Boosts_Table, + **DonaldAbility_Table, + **GoofyAbility_Table, + **Consumable_Table +} +filler_items = [ItemName.PowerBoost, ItemName.MagicBoost, ItemName.DefenseBoost, ItemName.APBoost, + ItemName.Potion, ItemName.HiPotion, ItemName.Ether, ItemName.Elixir, ItemName.Megalixir, + ItemName.Tent, ItemName.DriveRecovery, ItemName.HighDriveRecovery, + ] +item_groups: typing.Dict[str, list] = { + "Drive Form": [item_name for item_name in Forms_Table.keys()], + "Growth": [item_name for item_name in Movement_Table.keys()], + "Donald Limit": [ItemName.FlareForce, ItemName.Fantasia], + "Goofy Limit": [ItemName.Teamwork, ItemName.TornadoFusion], + "Magic": [ItemName.FireElement, ItemName.BlizzardElement, + ItemName.ThunderElement, + ItemName.CureElement, ItemName.MagnetElement, + ItemName.ReflectElement], + "Summon": [ItemName.ChickenLittle, ItemName.Genie, ItemName.Stitch, + ItemName.PeterPan], + "Gap Closer": [ItemName.SlideDash, ItemName.FlashStep], + "Ground Finisher": [ItemName.GuardBreak, ItemName.Explosion, + ItemName.FinishingLeap], + "Visit Lock": [item_name for item_name in + visit_locking_dict["2VisitLocking"]], + "Keyblade": [item_name for item_name in Keyblade_Table.keys()], + "Fire": [ItemName.FireElement], + "Blizzard": [ItemName.BlizzardElement], + "Thunder": [ItemName.ThunderElement], + "Cure": [ItemName.CureElement], + "Magnet": [ItemName.MagnetElement], + "Reflect": [ItemName.ReflectElement], + "Proof": [ItemName.ProofofNonexistence, ItemName.ProofofPeace, + ItemName.ProofofConnection], + "hitlist": [ItemName.Bounty], +} diff --git a/worlds/kh2/Locations.py b/worlds/kh2/Locations.py index 9046dfc67be5..9d7d948443cd 100644 --- a/worlds/kh2/Locations.py +++ b/worlds/kh2/Locations.py @@ -1,7 +1,7 @@ import typing from BaseClasses import Location -from .Names import LocationName, RegionName, ItemName +from .Names import LocationName, ItemName class KH2Location(Location): @@ -9,7 +9,6 @@ class KH2Location(Location): class LocationData(typing.NamedTuple): - code: typing.Optional[int] locid: int yml: str charName: str = "Sora" @@ -18,950 +17,1072 @@ class LocationData(typing.NamedTuple): # data's addrcheck sys3 addr obtained roomid bit index is eventid LoD_Checks = { - LocationName.BambooGroveDarkShard: LocationData(0x130000, 245, "Chest"), - LocationName.BambooGroveEther: LocationData(0x130001, 497, "Chest"), - LocationName.BambooGroveMythrilShard: LocationData(0x130002, 498, "Chest"), - LocationName.EncampmentAreaMap: LocationData(0x130003, 350, "Chest"), - LocationName.Mission3: LocationData(0x130004, 417, "Chest"), - LocationName.CheckpointHiPotion: LocationData(0x130005, 21, "Chest"), - LocationName.CheckpointMythrilShard: LocationData(0x130006, 121, "Chest"), - LocationName.MountainTrailLightningShard: LocationData(0x130007, 22, "Chest"), - LocationName.MountainTrailRecoveryRecipe: LocationData(0x130008, 23, "Chest"), - LocationName.MountainTrailEther: LocationData(0x130009, 122, "Chest"), - LocationName.MountainTrailMythrilShard: LocationData(0x13000A, 123, "Chest"), - LocationName.VillageCaveAreaMap: LocationData(0x13000B, 495, "Chest"), - LocationName.VillageCaveDarkShard: LocationData(0x13000C, 125, "Chest"), - LocationName.VillageCaveAPBoost: LocationData(0x13000D, 124, "Chest"), - LocationName.VillageCaveBonus: LocationData(0x13000E, 43, "Get Bonus"), - LocationName.RidgeFrostShard: LocationData(0x13000F, 24, "Chest"), - LocationName.RidgeAPBoost: LocationData(0x130010, 126, "Chest"), - LocationName.ShanYu: LocationData(0x130011, 9, "Double Get Bonus"), - LocationName.ShanYuGetBonus: LocationData(0x130012, 9, "Second Get Bonus"), - LocationName.HiddenDragon: LocationData(0x130013, 257, "Chest"), - -} -LoD2_Checks = { - LocationName.ThroneRoomTornPages: LocationData(0x130014, 25, "Chest"), - LocationName.ThroneRoomPalaceMap: LocationData(0x130015, 127, "Chest"), - LocationName.ThroneRoomAPBoost: LocationData(0x130016, 26, "Chest"), - LocationName.ThroneRoomQueenRecipe: LocationData(0x130017, 27, "Chest"), - LocationName.ThroneRoomAPBoost2: LocationData(0x130018, 128, "Chest"), - LocationName.ThroneRoomOgreShield: LocationData(0x130019, 129, "Chest"), - LocationName.ThroneRoomMythrilCrystal: LocationData(0x13001A, 130, "Chest"), - LocationName.ThroneRoomOrichalcum: LocationData(0x13001B, 131, "Chest"), - LocationName.StormRider: LocationData(0x13001C, 10, "Get Bonus"), - LocationName.XigbarDataDefenseBoost: LocationData(0x13001D, 555, "Chest"), + LocationName.BambooGroveDarkShard: LocationData(245, "Chest"), + LocationName.BambooGroveEther: LocationData(497, "Chest"), + LocationName.BambooGroveMythrilShard: LocationData(498, "Chest"), + LocationName.EncampmentAreaMap: LocationData(350, "Chest"), + LocationName.Mission3: LocationData(417, "Chest"), + LocationName.CheckpointHiPotion: LocationData(21, "Chest"), + LocationName.CheckpointMythrilShard: LocationData(121, "Chest"), + LocationName.MountainTrailLightningShard: LocationData(22, "Chest"), + LocationName.MountainTrailRecoveryRecipe: LocationData(23, "Chest"), + LocationName.MountainTrailEther: LocationData(122, "Chest"), + LocationName.MountainTrailMythrilShard: LocationData(123, "Chest"), + LocationName.VillageCaveAreaMap: LocationData(495, "Chest"), + LocationName.VillageCaveDarkShard: LocationData(125, "Chest"), + LocationName.VillageCaveAPBoost: LocationData(124, "Chest"), + LocationName.VillageCaveBonus: LocationData(43, "Get Bonus"), + LocationName.RidgeFrostShard: LocationData(24, "Chest"), + LocationName.RidgeAPBoost: LocationData(126, "Chest"), + LocationName.ShanYu: LocationData(9, "Double Get Bonus"), + LocationName.ShanYuGetBonus: LocationData(9, "Second Get Bonus"), + LocationName.HiddenDragon: LocationData(257, "Chest"), + LocationName.ThroneRoomTornPages: LocationData(25, "Chest"), + LocationName.ThroneRoomPalaceMap: LocationData(127, "Chest"), + LocationName.ThroneRoomAPBoost: LocationData(26, "Chest"), + LocationName.ThroneRoomQueenRecipe: LocationData(27, "Chest"), + LocationName.ThroneRoomAPBoost2: LocationData(128, "Chest"), + LocationName.ThroneRoomOgreShield: LocationData(129, "Chest"), + LocationName.ThroneRoomMythrilCrystal: LocationData(130, "Chest"), + LocationName.ThroneRoomOrichalcum: LocationData(131, "Chest"), + LocationName.StormRider: LocationData(10, "Get Bonus"), + LocationName.XigbarDataDefenseBoost: LocationData(555, "Chest"), } AG_Checks = { - LocationName.AgrabahMap: LocationData(0x13001E, 353, "Chest"), - LocationName.AgrabahDarkShard: LocationData(0x13001F, 28, "Chest"), - LocationName.AgrabahMythrilShard: LocationData(0x130020, 29, "Chest"), - LocationName.AgrabahHiPotion: LocationData(0x130021, 30, "Chest"), - LocationName.AgrabahAPBoost: LocationData(0x130022, 132, "Chest"), - LocationName.AgrabahMythrilStone: LocationData(0x130023, 133, "Chest"), - LocationName.AgrabahMythrilShard2: LocationData(0x130024, 249, "Chest"), - LocationName.AgrabahSerenityShard: LocationData(0x130025, 501, "Chest"), - LocationName.BazaarMythrilGem: LocationData(0x130026, 31, "Chest"), - LocationName.BazaarPowerShard: LocationData(0x130027, 32, "Chest"), - LocationName.BazaarHiPotion: LocationData(0x130028, 33, "Chest"), - LocationName.BazaarAPBoost: LocationData(0x130029, 134, "Chest"), - LocationName.BazaarMythrilShard: LocationData(0x13002A, 135, "Chest"), - LocationName.PalaceWallsSkillRing: LocationData(0x13002B, 136, "Chest"), - LocationName.PalaceWallsMythrilStone: LocationData(0x13002C, 520, "Chest"), - LocationName.CaveEntrancePowerStone: LocationData(0x13002D, 250, "Chest"), - LocationName.CaveEntranceMythrilShard: LocationData(0x13002E, 251, "Chest"), - LocationName.ValleyofStoneMythrilStone: LocationData(0x13002F, 35, "Chest"), - LocationName.ValleyofStoneAPBoost: LocationData(0x130030, 36, "Chest"), - LocationName.ValleyofStoneMythrilShard: LocationData(0x130031, 137, "Chest"), - LocationName.ValleyofStoneHiPotion: LocationData(0x130032, 138, "Chest"), - LocationName.AbuEscort: LocationData(0x130033, 42, "Get Bonus"), - LocationName.ChasmofChallengesCaveofWondersMap: LocationData(0x130034, 487, "Chest"), - LocationName.ChasmofChallengesAPBoost: LocationData(0x130035, 37, "Chest"), - LocationName.TreasureRoom: LocationData(0x130036, 46, "Get Bonus"), - LocationName.TreasureRoomAPBoost: LocationData(0x130037, 502, "Chest"), - LocationName.TreasureRoomSerenityGem: LocationData(0x130038, 503, "Chest"), - LocationName.ElementalLords: LocationData(0x130039, 37, "Get Bonus"), - LocationName.LampCharm: LocationData(0x13003A, 300, "Chest"), - -} -AG2_Checks = { - LocationName.RuinedChamberTornPages: LocationData(0x13003B, 34, "Chest"), - LocationName.RuinedChamberRuinsMap: LocationData(0x13003C, 486, "Chest"), - LocationName.GenieJafar: LocationData(0x13003D, 15, "Get Bonus"), - LocationName.WishingLamp: LocationData(0x13003E, 303, "Chest"), - LocationName.LexaeusBonus: LocationData(0x13003F, 65, "Get Bonus"), - LocationName.LexaeusASStrengthBeyondStrength: LocationData(0x130040, 545, "Chest"), - LocationName.LexaeusDataLostIllusion: LocationData(0x130041, 550, "Chest"), + LocationName.AgrabahMap: LocationData(353, "Chest"), + LocationName.AgrabahDarkShard: LocationData(28, "Chest"), + LocationName.AgrabahMythrilShard: LocationData(29, "Chest"), + LocationName.AgrabahHiPotion: LocationData(30, "Chest"), + LocationName.AgrabahAPBoost: LocationData(132, "Chest"), + LocationName.AgrabahMythrilStone: LocationData(133, "Chest"), + LocationName.AgrabahMythrilShard2: LocationData(249, "Chest"), + LocationName.AgrabahSerenityShard: LocationData(501, "Chest"), + LocationName.BazaarMythrilGem: LocationData(31, "Chest"), + LocationName.BazaarPowerShard: LocationData(32, "Chest"), + LocationName.BazaarHiPotion: LocationData(33, "Chest"), + LocationName.BazaarAPBoost: LocationData(134, "Chest"), + LocationName.BazaarMythrilShard: LocationData(135, "Chest"), + LocationName.PalaceWallsSkillRing: LocationData(136, "Chest"), + LocationName.PalaceWallsMythrilStone: LocationData(520, "Chest"), + LocationName.CaveEntrancePowerStone: LocationData(250, "Chest"), + LocationName.CaveEntranceMythrilShard: LocationData(251, "Chest"), + LocationName.ValleyofStoneMythrilStone: LocationData(35, "Chest"), + LocationName.ValleyofStoneAPBoost: LocationData(36, "Chest"), + LocationName.ValleyofStoneMythrilShard: LocationData(137, "Chest"), + LocationName.ValleyofStoneHiPotion: LocationData(138, "Chest"), + LocationName.AbuEscort: LocationData(42, "Get Bonus"), + LocationName.ChasmofChallengesCaveofWondersMap: LocationData(487, "Chest"), + LocationName.ChasmofChallengesAPBoost: LocationData(37, "Chest"), + LocationName.TreasureRoom: LocationData(46, "Get Bonus"), + LocationName.TreasureRoomAPBoost: LocationData(502, "Chest"), + LocationName.TreasureRoomSerenityGem: LocationData(503, "Chest"), + LocationName.ElementalLords: LocationData(37, "Get Bonus"), + LocationName.LampCharm: LocationData(300, "Chest"), + LocationName.RuinedChamberTornPages: LocationData(34, "Chest"), + LocationName.RuinedChamberRuinsMap: LocationData(486, "Chest"), + LocationName.GenieJafar: LocationData(15, "Get Bonus"), + LocationName.WishingLamp: LocationData(303, "Chest"), + LocationName.LexaeusBonus: LocationData(65, "Get Bonus"), + LocationName.LexaeusASStrengthBeyondStrength: LocationData(545, "Chest"), + LocationName.LexaeusDataLostIllusion: LocationData(550, "Chest"), } DC_Checks = { - LocationName.DCCourtyardMythrilShard: LocationData(0x130042, 16, "Chest"), - LocationName.DCCourtyardStarRecipe: LocationData(0x130043, 17, "Chest"), - LocationName.DCCourtyardAPBoost: LocationData(0x130044, 18, "Chest"), - LocationName.DCCourtyardMythrilStone: LocationData(0x130045, 92, "Chest"), - LocationName.DCCourtyardBlazingStone: LocationData(0x130046, 93, "Chest"), - LocationName.DCCourtyardBlazingShard: LocationData(0x130047, 247, "Chest"), - LocationName.DCCourtyardMythrilShard2: LocationData(0x130048, 248, "Chest"), - LocationName.LibraryTornPages: LocationData(0x130049, 91, "Chest"), - LocationName.DisneyCastleMap: LocationData(0x13004A, 332, "Chest"), - LocationName.MinnieEscort: LocationData(0x13004B, 38, "Double Get Bonus"), - LocationName.MinnieEscortGetBonus: LocationData(0x13004C, 38, "Second Get Bonus"), + LocationName.DCCourtyardMythrilShard: LocationData(16, "Chest"), + LocationName.DCCourtyardStarRecipe: LocationData(17, "Chest"), + LocationName.DCCourtyardAPBoost: LocationData(18, "Chest"), + LocationName.DCCourtyardMythrilStone: LocationData(92, "Chest"), + LocationName.DCCourtyardBlazingStone: LocationData(93, "Chest"), + LocationName.DCCourtyardBlazingShard: LocationData(247, "Chest"), + LocationName.DCCourtyardMythrilShard2: LocationData(248, "Chest"), + LocationName.LibraryTornPages: LocationData(91, "Chest"), + LocationName.DisneyCastleMap: LocationData(332, "Chest"), + LocationName.MinnieEscort: LocationData(38, "Double Get Bonus"), + LocationName.MinnieEscortGetBonus: LocationData(38, "Second Get Bonus"), + LocationName.CornerstoneHillMap: LocationData(79, "Chest"), + LocationName.CornerstoneHillFrostShard: LocationData(12, "Chest"), + LocationName.PierMythrilShard: LocationData(81, "Chest"), + LocationName.PierHiPotion: LocationData(82, "Chest"), + LocationName.WaterwayMythrilStone: LocationData(83, "Chest"), + LocationName.WaterwayAPBoost: LocationData(84, "Chest"), + LocationName.WaterwayFrostStone: LocationData(85, "Chest"), + LocationName.WindowofTimeMap: LocationData(368, "Chest"), + LocationName.BoatPete: LocationData(16, "Get Bonus"), + LocationName.FuturePete: LocationData(17, "Double Get Bonus"), + LocationName.FuturePeteGetBonus: LocationData(17, "Second Get Bonus"), + LocationName.Monochrome: LocationData(261, "Chest"), + LocationName.WisdomForm: LocationData(262, "Chest"), + LocationName.MarluxiaGetBonus: LocationData(67, "Get Bonus"), + LocationName.MarluxiaASEternalBlossom: LocationData(548, "Chest"), + LocationName.MarluxiaDataLostIllusion: LocationData(553, "Chest"), + LocationName.LingeringWillBonus: LocationData(70, "Get Bonus"), + LocationName.LingeringWillProofofConnection: LocationData(587, "Chest"), + LocationName.LingeringWillManifestIllusion: LocationData(591, "Chest"), } -TR_Checks = { - LocationName.CornerstoneHillMap: LocationData(0x13004D, 79, "Chest"), - LocationName.CornerstoneHillFrostShard: LocationData(0x13004E, 12, "Chest"), - LocationName.PierMythrilShard: LocationData(0x13004F, 81, "Chest"), - LocationName.PierHiPotion: LocationData(0x130050, 82, "Chest"), - LocationName.WaterwayMythrilStone: LocationData(0x130051, 83, "Chest"), - LocationName.WaterwayAPBoost: LocationData(0x130052, 84, "Chest"), - LocationName.WaterwayFrostStone: LocationData(0x130053, 85, "Chest"), - LocationName.WindowofTimeMap: LocationData(0x130054, 368, "Chest"), - LocationName.BoatPete: LocationData(0x130055, 16, "Get Bonus"), - LocationName.FuturePete: LocationData(0x130056, 17, "Double Get Bonus"), - LocationName.FuturePeteGetBonus: LocationData(0x130057, 17, "Second Get Bonus"), - LocationName.Monochrome: LocationData(0x130058, 261, "Chest"), - LocationName.WisdomForm: LocationData(0x130059, 262, "Chest"), - LocationName.MarluxiaGetBonus: LocationData(0x13005A, 67, "Get Bonus"), - LocationName.MarluxiaASEternalBlossom: LocationData(0x13005B, 548, "Chest"), - LocationName.MarluxiaDataLostIllusion: LocationData(0x13005C, 553, "Chest"), - LocationName.LingeringWillBonus: LocationData(0x13005D, 70, "Get Bonus"), - LocationName.LingeringWillProofofConnection: LocationData(0x13005E, 587, "Chest"), - LocationName.LingeringWillManifestIllusion: LocationData(0x13005F, 591, "Chest"), -} -# the mismatch might be here -HundredAcre1_Checks = { - LocationName.PoohsHouse100AcreWoodMap: LocationData(0x130060, 313, "Chest"), - LocationName.PoohsHouseAPBoost: LocationData(0x130061, 97, "Chest"), - LocationName.PoohsHouseMythrilStone: LocationData(0x130062, 98, "Chest"), -} -HundredAcre2_Checks = { - LocationName.PigletsHouseDefenseBoost: LocationData(0x130063, 105, "Chest"), - LocationName.PigletsHouseAPBoost: LocationData(0x130064, 103, "Chest"), - LocationName.PigletsHouseMythrilGem: LocationData(0x130065, 104, "Chest"), -} -HundredAcre3_Checks = { - LocationName.RabbitsHouseDrawRing: LocationData(0x130066, 314, "Chest"), - LocationName.RabbitsHouseMythrilCrystal: LocationData(0x130067, 100, "Chest"), - LocationName.RabbitsHouseAPBoost: LocationData(0x130068, 101, "Chest"), -} -HundredAcre4_Checks = { - LocationName.KangasHouseMagicBoost: LocationData(0x130069, 108, "Chest"), - LocationName.KangasHouseAPBoost: LocationData(0x13006A, 106, "Chest"), - LocationName.KangasHouseOrichalcum: LocationData(0x13006B, 107, "Chest"), -} -HundredAcre5_Checks = { - LocationName.SpookyCaveMythrilGem: LocationData(0x13006C, 110, "Chest"), - LocationName.SpookyCaveAPBoost: LocationData(0x13006D, 111, "Chest"), - LocationName.SpookyCaveOrichalcum: LocationData(0x13006E, 112, "Chest"), - LocationName.SpookyCaveGuardRecipe: LocationData(0x13006F, 113, "Chest"), - LocationName.SpookyCaveMythrilCrystal: LocationData(0x130070, 115, "Chest"), - LocationName.SpookyCaveAPBoost2: LocationData(0x130071, 116, "Chest"), - LocationName.SweetMemories: LocationData(0x130072, 284, "Chest"), - LocationName.SpookyCaveMap: LocationData(0x130073, 485, "Chest"), -} -HundredAcre6_Checks = { - LocationName.StarryHillCosmicRing: LocationData(0x130074, 312, "Chest"), - LocationName.StarryHillStyleRecipe: LocationData(0x130075, 94, "Chest"), - LocationName.StarryHillCureElement: LocationData(0x130076, 285, "Chest"), - LocationName.StarryHillOrichalcumPlus: LocationData(0x130077, 539, "Chest"), +HundredAcre_Checks = { + LocationName.PoohsHouse100AcreWoodMap: LocationData(313, "Chest"), + LocationName.PoohsHouseAPBoost: LocationData(97, "Chest"), + LocationName.PoohsHouseMythrilStone: LocationData(98, "Chest"), + LocationName.PigletsHouseDefenseBoost: LocationData(105, "Chest"), + LocationName.PigletsHouseAPBoost: LocationData(103, "Chest"), + LocationName.PigletsHouseMythrilGem: LocationData(104, "Chest"), + LocationName.RabbitsHouseDrawRing: LocationData(314, "Chest"), + LocationName.RabbitsHouseMythrilCrystal: LocationData(100, "Chest"), + LocationName.RabbitsHouseAPBoost: LocationData(101, "Chest"), + LocationName.KangasHouseMagicBoost: LocationData(108, "Chest"), + LocationName.KangasHouseAPBoost: LocationData(106, "Chest"), + LocationName.KangasHouseOrichalcum: LocationData(107, "Chest"), + LocationName.SpookyCaveMythrilGem: LocationData(110, "Chest"), + LocationName.SpookyCaveAPBoost: LocationData(111, "Chest"), + LocationName.SpookyCaveOrichalcum: LocationData(112, "Chest"), + LocationName.SpookyCaveGuardRecipe: LocationData(113, "Chest"), + LocationName.SpookyCaveMythrilCrystal: LocationData(115, "Chest"), + LocationName.SpookyCaveAPBoost2: LocationData(116, "Chest"), + LocationName.SweetMemories: LocationData(284, "Chest"), + LocationName.SpookyCaveMap: LocationData(485, "Chest"), + LocationName.StarryHillCosmicRing: LocationData(312, "Chest"), + LocationName.StarryHillStyleRecipe: LocationData(94, "Chest"), + LocationName.StarryHillCureElement: LocationData(285, "Chest"), + LocationName.StarryHillOrichalcumPlus: LocationData(539, "Chest"), } Oc_Checks = { - LocationName.PassageMythrilShard: LocationData(0x130078, 7, "Chest"), - LocationName.PassageMythrilStone: LocationData(0x130079, 8, "Chest"), - LocationName.PassageEther: LocationData(0x13007A, 144, "Chest"), - LocationName.PassageAPBoost: LocationData(0x13007B, 145, "Chest"), - LocationName.PassageHiPotion: LocationData(0x13007C, 146, "Chest"), - LocationName.InnerChamberUnderworldMap: LocationData(0x13007D, 2, "Chest"), - LocationName.InnerChamberMythrilShard: LocationData(0x13007E, 243, "Chest"), - LocationName.Cerberus: LocationData(0x13007F, 5, "Get Bonus"), - LocationName.ColiseumMap: LocationData(0x130080, 338, "Chest"), - LocationName.Urns: LocationData(0x130081, 57, "Get Bonus"), - LocationName.UnderworldEntrancePowerBoost: LocationData(0x130082, 242, "Chest"), - LocationName.CavernsEntranceLucidShard: LocationData(0x130083, 3, "Chest"), - LocationName.CavernsEntranceAPBoost: LocationData(0x130084, 11, "Chest"), - LocationName.CavernsEntranceMythrilShard: LocationData(0x130085, 504, "Chest"), - LocationName.TheLostRoadBrightShard: LocationData(0x130086, 9, "Chest"), - LocationName.TheLostRoadEther: LocationData(0x130087, 10, "Chest"), - LocationName.TheLostRoadMythrilShard: LocationData(0x130088, 148, "Chest"), - LocationName.TheLostRoadMythrilStone: LocationData(0x130089, 149, "Chest"), - LocationName.AtriumLucidStone: LocationData(0x13008A, 150, "Chest"), - LocationName.AtriumAPBoost: LocationData(0x13008B, 151, "Chest"), - LocationName.DemyxOC: LocationData(0x13008C, 58, "Get Bonus"), - LocationName.SecretAnsemReport5: LocationData(0x13008D, 529, "Chest"), - LocationName.OlympusStone: LocationData(0x13008E, 293, "Chest"), - LocationName.TheLockCavernsMap: LocationData(0x13008F, 244, "Chest"), - LocationName.TheLockMythrilShard: LocationData(0x130090, 5, "Chest"), - LocationName.TheLockAPBoost: LocationData(0x130091, 142, "Chest"), - LocationName.PeteOC: LocationData(0x130092, 6, "Get Bonus"), - LocationName.Hydra: LocationData(0x130093, 7, "Double Get Bonus"), - LocationName.HydraGetBonus: LocationData(0x130094, 7, "Second Get Bonus"), - LocationName.HerosCrest: LocationData(0x130095, 260, "Chest"), - -} -Oc2_Checks = { - LocationName.AuronsStatue: LocationData(0x130096, 295, "Chest"), - LocationName.Hades: LocationData(0x130097, 8, "Double Get Bonus"), - LocationName.HadesGetBonus: LocationData(0x130098, 8, "Second Get Bonus"), - LocationName.GuardianSoul: LocationData(0x130099, 272, "Chest"), - LocationName.ZexionBonus: LocationData(0x13009A, 66, "Get Bonus"), - LocationName.ZexionASBookofShadows: LocationData(0x13009B, 546, "Chest"), - LocationName.ZexionDataLostIllusion: LocationData(0x13009C, 551, "Chest"), -} -Oc2Cups = { - LocationName.ProtectBeltPainandPanicCup: LocationData(0x13009D, 513, "Chest"), - LocationName.SerenityGemPainandPanicCup: LocationData(0x13009E, 540, "Chest"), - LocationName.RisingDragonCerberusCup: LocationData(0x13009F, 515, "Chest"), - LocationName.SerenityCrystalCerberusCup: LocationData(0x1300A0, 542, "Chest"), - LocationName.GenjiShieldTitanCup: LocationData(0x1300A1, 514, "Chest"), - LocationName.SkillfulRingTitanCup: LocationData(0x1300A2, 541, "Chest"), - LocationName.FatalCrestGoddessofFateCup: LocationData(0x1300A3, 516, "Chest"), - LocationName.OrichalcumPlusGoddessofFateCup: LocationData(0x1300A4, 517, "Chest"), - LocationName.HadesCupTrophyParadoxCups: LocationData(0x1300A5, 518, "Chest"), + LocationName.PassageMythrilShard: LocationData(7, "Chest"), + LocationName.PassageMythrilStone: LocationData(8, "Chest"), + LocationName.PassageEther: LocationData(144, "Chest"), + LocationName.PassageAPBoost: LocationData(145, "Chest"), + LocationName.PassageHiPotion: LocationData(146, "Chest"), + LocationName.InnerChamberUnderworldMap: LocationData(2, "Chest"), + LocationName.InnerChamberMythrilShard: LocationData(243, "Chest"), + LocationName.Cerberus: LocationData(5, "Get Bonus"), + LocationName.ColiseumMap: LocationData(338, "Chest"), + LocationName.Urns: LocationData(57, "Get Bonus"), + LocationName.UnderworldEntrancePowerBoost: LocationData(242, "Chest"), + LocationName.CavernsEntranceLucidShard: LocationData(3, "Chest"), + LocationName.CavernsEntranceAPBoost: LocationData(11, "Chest"), + LocationName.CavernsEntranceMythrilShard: LocationData(504, "Chest"), + LocationName.TheLostRoadBrightShard: LocationData(9, "Chest"), + LocationName.TheLostRoadEther: LocationData(10, "Chest"), + LocationName.TheLostRoadMythrilShard: LocationData(148, "Chest"), + LocationName.TheLostRoadMythrilStone: LocationData(149, "Chest"), + LocationName.AtriumLucidStone: LocationData(150, "Chest"), + LocationName.AtriumAPBoost: LocationData(151, "Chest"), + LocationName.DemyxOC: LocationData(58, "Get Bonus"), + LocationName.SecretAnsemReport5: LocationData(529, "Chest"), + LocationName.OlympusStone: LocationData(293, "Chest"), + LocationName.TheLockCavernsMap: LocationData(244, "Chest"), + LocationName.TheLockMythrilShard: LocationData(5, "Chest"), + LocationName.TheLockAPBoost: LocationData(142, "Chest"), + LocationName.PeteOC: LocationData(6, "Get Bonus"), + LocationName.Hydra: LocationData(7, "Double Get Bonus"), + LocationName.HydraGetBonus: LocationData(7, "Second Get Bonus"), + LocationName.HerosCrest: LocationData(260, "Chest"), + LocationName.AuronsStatue: LocationData(295, "Chest"), + LocationName.Hades: LocationData(8, "Double Get Bonus"), + LocationName.HadesGetBonus: LocationData(8, "Second Get Bonus"), + LocationName.GuardianSoul: LocationData(272, "Chest"), + LocationName.ZexionBonus: LocationData(66, "Get Bonus"), + LocationName.ZexionASBookofShadows: LocationData(546, "Chest"), + LocationName.ZexionDataLostIllusion: LocationData(551, "Chest"), + LocationName.ProtectBeltPainandPanicCup: LocationData(513, "Chest"), + LocationName.SerenityGemPainandPanicCup: LocationData(540, "Chest"), + LocationName.RisingDragonCerberusCup: LocationData(515, "Chest"), + LocationName.SerenityCrystalCerberusCup: LocationData(542, "Chest"), + LocationName.GenjiShieldTitanCup: LocationData(514, "Chest"), + LocationName.SkillfulRingTitanCup: LocationData(541, "Chest"), + LocationName.FatalCrestGoddessofFateCup: LocationData(516, "Chest"), + LocationName.OrichalcumPlusGoddessofFateCup: LocationData(517, "Chest"), + LocationName.HadesCupTrophyParadoxCups: LocationData(518, "Chest"), } BC_Checks = { - LocationName.BCCourtyardAPBoost: LocationData(0x1300A6, 39, "Chest"), - LocationName.BCCourtyardHiPotion: LocationData(0x1300A7, 40, "Chest"), - LocationName.BCCourtyardMythrilShard: LocationData(0x1300A8, 505, "Chest"), - LocationName.BellesRoomCastleMap: LocationData(0x1300A9, 46, "Chest"), - LocationName.BellesRoomMegaRecipe: LocationData(0x1300AA, 240, "Chest"), - LocationName.TheEastWingMythrilShard: LocationData(0x1300AB, 63, "Chest"), - LocationName.TheEastWingTent: LocationData(0x1300AC, 155, "Chest"), - LocationName.TheWestHallHiPotion: LocationData(0x1300AD, 41, "Chest"), - LocationName.TheWestHallPowerShard: LocationData(0x1300AE, 207, "Chest"), - LocationName.TheWestHallAPBoostPostDungeon: LocationData(0x1300AF, 158, "Chest"), - LocationName.TheWestHallBrightStone: LocationData(0x1300B0, 159, "Chest"), - LocationName.TheWestHallMythrilShard: LocationData(0x1300B1, 206, "Chest"), - LocationName.Thresholder: LocationData(0x1300B2, 2, "Get Bonus"), - LocationName.DungeonBasementMap: LocationData(0x1300B3, 239, "Chest"), - LocationName.DungeonAPBoost: LocationData(0x1300B4, 43, "Chest"), - LocationName.SecretPassageMythrilShard: LocationData(0x1300B5, 44, "Chest"), - LocationName.SecretPassageHiPotion: LocationData(0x1300B6, 168, "Chest"), - LocationName.SecretPassageLucidShard: LocationData(0x1300B7, 45, "Chest"), - LocationName.TheWestHallMythrilShard2: LocationData(0x1300B8, 208, "Chest"), - LocationName.TheWestWingMythrilShard: LocationData(0x1300B9, 42, "Chest"), - LocationName.TheWestWingTent: LocationData(0x1300BA, 164, "Chest"), - LocationName.Beast: LocationData(0x1300BB, 12, "Get Bonus"), - LocationName.TheBeastsRoomBlazingShard: LocationData(0x1300BC, 241, "Chest"), - LocationName.DarkThorn: LocationData(0x1300BD, 3, "Double Get Bonus"), - LocationName.DarkThornGetBonus: LocationData(0x1300BE, 3, "Second Get Bonus"), - LocationName.DarkThornCureElement: LocationData(0x1300BF, 299, "Chest"), - -} -BC2_Checks = { - LocationName.RumblingRose: LocationData(0x1300C0, 270, "Chest"), - LocationName.CastleWallsMap: LocationData(0x1300C1, 325, "Chest"), - LocationName.Xaldin: LocationData(0x1300C2, 4, "Double Get Bonus"), - LocationName.XaldinGetBonus: LocationData(0x1300C3, 4, "Second Get Bonus"), - LocationName.SecretAnsemReport4: LocationData(0x1300C4, 528, "Chest"), - LocationName.XaldinDataDefenseBoost: LocationData(0x1300C5, 559, "Chest"), + LocationName.BCCourtyardAPBoost: LocationData(39, "Chest"), + LocationName.BCCourtyardHiPotion: LocationData(40, "Chest"), + LocationName.BCCourtyardMythrilShard: LocationData(505, "Chest"), + LocationName.BellesRoomCastleMap: LocationData(46, "Chest"), + LocationName.BellesRoomMegaRecipe: LocationData(240, "Chest"), + LocationName.TheEastWingMythrilShard: LocationData(63, "Chest"), + LocationName.TheEastWingTent: LocationData(155, "Chest"), + LocationName.TheWestHallHiPotion: LocationData(41, "Chest"), + LocationName.TheWestHallPowerShard: LocationData(207, "Chest"), + LocationName.TheWestHallAPBoostPostDungeon: LocationData(158, "Chest"), + LocationName.TheWestHallBrightStone: LocationData(159, "Chest"), + LocationName.TheWestHallMythrilShard: LocationData(206, "Chest"), + LocationName.Thresholder: LocationData(2, "Get Bonus"), + LocationName.DungeonBasementMap: LocationData(239, "Chest"), + LocationName.DungeonAPBoost: LocationData(43, "Chest"), + LocationName.SecretPassageMythrilShard: LocationData(44, "Chest"), + LocationName.SecretPassageHiPotion: LocationData(168, "Chest"), + LocationName.SecretPassageLucidShard: LocationData(45, "Chest"), + LocationName.TheWestHallMythrilShard2: LocationData(208, "Chest"), + LocationName.TheWestWingMythrilShard: LocationData(42, "Chest"), + LocationName.TheWestWingTent: LocationData(164, "Chest"), + LocationName.Beast: LocationData(12, "Get Bonus"), + LocationName.TheBeastsRoomBlazingShard: LocationData(241, "Chest"), + LocationName.DarkThorn: LocationData(3, "Double Get Bonus"), + LocationName.DarkThornGetBonus: LocationData(3, "Second Get Bonus"), + LocationName.DarkThornCureElement: LocationData(299, "Chest"), + LocationName.RumblingRose: LocationData(270, "Chest"), + LocationName.CastleWallsMap: LocationData(325, "Chest"), + LocationName.Xaldin: LocationData(4, "Double Get Bonus"), + LocationName.XaldinGetBonus: LocationData(4, "Second Get Bonus"), + LocationName.SecretAnsemReport4: LocationData(528, "Chest"), + LocationName.XaldinDataDefenseBoost: LocationData(559, "Chest"), } SP_Checks = { - LocationName.PitCellAreaMap: LocationData(0x1300C6, 316, "Chest"), - LocationName.PitCellMythrilCrystal: LocationData(0x1300C7, 64, "Chest"), - LocationName.CanyonDarkCrystal: LocationData(0x1300C8, 65, "Chest"), - LocationName.CanyonMythrilStone: LocationData(0x1300C9, 171, "Chest"), - LocationName.CanyonMythrilGem: LocationData(0x1300CA, 253, "Chest"), - LocationName.CanyonFrostCrystal: LocationData(0x1300CB, 521, "Chest"), - LocationName.Screens: LocationData(0x1300CC, 45, "Get Bonus"), - LocationName.HallwayPowerCrystal: LocationData(0x1300CD, 49, "Chest"), - LocationName.HallwayAPBoost: LocationData(0x1300CE, 50, "Chest"), - LocationName.CommunicationsRoomIOTowerMap: LocationData(0x1300CF, 255, "Chest"), - LocationName.CommunicationsRoomGaiaBelt: LocationData(0x1300D0, 499, "Chest"), - LocationName.HostileProgram: LocationData(0x1300D1, 31, "Double Get Bonus"), - LocationName.HostileProgramGetBonus: LocationData(0x1300D2, 31, "Second Get Bonus"), - LocationName.PhotonDebugger: LocationData(0x1300D3, 267, "Chest"), - -} -SP2_Checks = { - LocationName.SolarSailer: LocationData(0x1300D4, 61, "Get Bonus"), - LocationName.CentralComputerCoreAPBoost: LocationData(0x1300D5, 177, "Chest"), - LocationName.CentralComputerCoreOrichalcumPlus: LocationData(0x1300D6, 178, "Chest"), - LocationName.CentralComputerCoreCosmicArts: LocationData(0x1300D7, 51, "Chest"), - LocationName.CentralComputerCoreMap: LocationData(0x1300D8, 488, "Chest"), - LocationName.MCP: LocationData(0x1300D9, 32, "Double Get Bonus"), - LocationName.MCPGetBonus: LocationData(0x1300DA, 32, "Second Get Bonus"), - LocationName.LarxeneBonus: LocationData(0x1300DB, 68, "Get Bonus"), - LocationName.LarxeneASCloakedThunder: LocationData(0x1300DC, 547, "Chest"), - LocationName.LarxeneDataLostIllusion: LocationData(0x1300DD, 552, "Chest"), + LocationName.PitCellAreaMap: LocationData(316, "Chest"), + LocationName.PitCellMythrilCrystal: LocationData(64, "Chest"), + LocationName.CanyonDarkCrystal: LocationData(65, "Chest"), + LocationName.CanyonMythrilStone: LocationData(171, "Chest"), + LocationName.CanyonMythrilGem: LocationData(253, "Chest"), + LocationName.CanyonFrostCrystal: LocationData(521, "Chest"), + LocationName.Screens: LocationData(45, "Get Bonus"), + LocationName.HallwayPowerCrystal: LocationData(49, "Chest"), + LocationName.HallwayAPBoost: LocationData(50, "Chest"), + LocationName.CommunicationsRoomIOTowerMap: LocationData(255, "Chest"), + LocationName.CommunicationsRoomGaiaBelt: LocationData(499, "Chest"), + LocationName.HostileProgram: LocationData(31, "Double Get Bonus"), + LocationName.HostileProgramGetBonus: LocationData(31, "Second Get Bonus"), + LocationName.PhotonDebugger: LocationData(267, "Chest"), + LocationName.SolarSailer: LocationData(61, "Get Bonus"), + LocationName.CentralComputerCoreAPBoost: LocationData(177, "Chest"), + LocationName.CentralComputerCoreOrichalcumPlus: LocationData(178, "Chest"), + LocationName.CentralComputerCoreCosmicArts: LocationData(51, "Chest"), + LocationName.CentralComputerCoreMap: LocationData(488, "Chest"), + LocationName.MCP: LocationData(32, "Double Get Bonus"), + LocationName.MCPGetBonus: LocationData(32, "Second Get Bonus"), + LocationName.LarxeneBonus: LocationData(68, "Get Bonus"), + LocationName.LarxeneASCloakedThunder: LocationData(547, "Chest"), + LocationName.LarxeneDataLostIllusion: LocationData(552, "Chest"), } HT_Checks = { - LocationName.GraveyardMythrilShard: LocationData(0x1300DE, 53, "Chest"), - LocationName.GraveyardSerenityGem: LocationData(0x1300DF, 212, "Chest"), - LocationName.FinklesteinsLabHalloweenTownMap: LocationData(0x1300E0, 211, "Chest"), - LocationName.TownSquareMythrilStone: LocationData(0x1300E1, 209, "Chest"), - LocationName.TownSquareEnergyShard: LocationData(0x1300E2, 210, "Chest"), - LocationName.HinterlandsLightningShard: LocationData(0x1300E3, 54, "Chest"), - LocationName.HinterlandsMythrilStone: LocationData(0x1300E4, 213, "Chest"), - LocationName.HinterlandsAPBoost: LocationData(0x1300E5, 214, "Chest"), - LocationName.CandyCaneLaneMegaPotion: LocationData(0x1300E6, 55, "Chest"), - LocationName.CandyCaneLaneMythrilGem: LocationData(0x1300E7, 56, "Chest"), - LocationName.CandyCaneLaneLightningStone: LocationData(0x1300E8, 216, "Chest"), - LocationName.CandyCaneLaneMythrilStone: LocationData(0x1300E9, 217, "Chest"), - LocationName.SantasHouseChristmasTownMap: LocationData(0x1300EA, 57, "Chest"), - LocationName.SantasHouseAPBoost: LocationData(0x1300EB, 58, "Chest"), - LocationName.PrisonKeeper: LocationData(0x1300EC, 18, "Get Bonus"), - LocationName.OogieBoogie: LocationData(0x1300ED, 19, "Get Bonus"), - LocationName.OogieBoogieMagnetElement: LocationData(0x1300EE, 301, "Chest"), -} -HT2_Checks = { - LocationName.Lock: LocationData(0x1300EF, 40, "Get Bonus"), - LocationName.Present: LocationData(0x1300F0, 297, "Chest"), - LocationName.DecoyPresents: LocationData(0x1300F1, 298, "Chest"), - LocationName.Experiment: LocationData(0x1300F2, 20, "Get Bonus"), - LocationName.DecisivePumpkin: LocationData(0x1300F3, 275, "Chest"), - LocationName.VexenBonus: LocationData(0x1300F4, 64, "Get Bonus"), - LocationName.VexenASRoadtoDiscovery: LocationData(0x1300F5, 544, "Chest"), - LocationName.VexenDataLostIllusion: LocationData(0x1300F6, 549, "Chest"), + LocationName.GraveyardMythrilShard: LocationData(53, "Chest"), + LocationName.GraveyardSerenityGem: LocationData(212, "Chest"), + LocationName.FinklesteinsLabHalloweenTownMap: LocationData(211, "Chest"), + LocationName.TownSquareMythrilStone: LocationData(209, "Chest"), + LocationName.TownSquareEnergyShard: LocationData(210, "Chest"), + LocationName.HinterlandsLightningShard: LocationData(54, "Chest"), + LocationName.HinterlandsMythrilStone: LocationData(213, "Chest"), + LocationName.HinterlandsAPBoost: LocationData(214, "Chest"), + LocationName.CandyCaneLaneMegaPotion: LocationData(55, "Chest"), + LocationName.CandyCaneLaneMythrilGem: LocationData(56, "Chest"), + LocationName.CandyCaneLaneLightningStone: LocationData(216, "Chest"), + LocationName.CandyCaneLaneMythrilStone: LocationData(217, "Chest"), + LocationName.SantasHouseChristmasTownMap: LocationData(57, "Chest"), + LocationName.SantasHouseAPBoost: LocationData(58, "Chest"), + LocationName.PrisonKeeper: LocationData(18, "Get Bonus"), + LocationName.OogieBoogie: LocationData(19, "Get Bonus"), + LocationName.OogieBoogieMagnetElement: LocationData(301, "Chest"), + LocationName.Lock: LocationData(40, "Get Bonus"), + LocationName.Present: LocationData(297, "Chest"), + LocationName.DecoyPresents: LocationData(298, "Chest"), + LocationName.Experiment: LocationData(20, "Get Bonus"), + LocationName.DecisivePumpkin: LocationData(275, "Chest"), + LocationName.VexenBonus: LocationData(64, "Get Bonus"), + LocationName.VexenASRoadtoDiscovery: LocationData(544, "Chest"), + LocationName.VexenDataLostIllusion: LocationData(549, "Chest"), } PR_Checks = { - LocationName.RampartNavalMap: LocationData(0x1300F7, 70, "Chest"), - LocationName.RampartMythrilStone: LocationData(0x1300F8, 219, "Chest"), - LocationName.RampartDarkShard: LocationData(0x1300F9, 220, "Chest"), - LocationName.TownDarkStone: LocationData(0x1300FA, 71, "Chest"), - LocationName.TownAPBoost: LocationData(0x1300FB, 72, "Chest"), - LocationName.TownMythrilShard: LocationData(0x1300FC, 73, "Chest"), - LocationName.TownMythrilGem: LocationData(0x1300FD, 221, "Chest"), - LocationName.CaveMouthBrightShard: LocationData(0x1300FE, 74, "Chest"), - LocationName.CaveMouthMythrilShard: LocationData(0x1300FF, 223, "Chest"), - LocationName.IsladeMuertaMap: LocationData(0x130100, 329, "Chest"), - LocationName.BoatFight: LocationData(0x130101, 62, "Get Bonus"), - LocationName.InterceptorBarrels: LocationData(0x130102, 39, "Get Bonus"), - LocationName.PowderStoreAPBoost1: LocationData(0x130103, 369, "Chest"), - LocationName.PowderStoreAPBoost2: LocationData(0x130104, 370, "Chest"), - LocationName.MoonlightNookMythrilShard: LocationData(0x130105, 75, "Chest"), - LocationName.MoonlightNookSerenityGem: LocationData(0x130106, 224, "Chest"), - LocationName.MoonlightNookPowerStone: LocationData(0x130107, 371, "Chest"), - LocationName.Barbossa: LocationData(0x130108, 21, "Double Get Bonus"), - LocationName.BarbossaGetBonus: LocationData(0x130109, 21, "Second Get Bonus"), - LocationName.FollowtheWind: LocationData(0x13010A, 263, "Chest"), - -} -PR2_Checks = { - LocationName.GrimReaper1: LocationData(0x13010B, 59, "Get Bonus"), - LocationName.InterceptorsHoldFeatherCharm: LocationData(0x13010C, 252, "Chest"), - LocationName.SeadriftKeepAPBoost: LocationData(0x13010D, 76, "Chest"), - LocationName.SeadriftKeepOrichalcum: LocationData(0x13010E, 225, "Chest"), - LocationName.SeadriftKeepMeteorStaff: LocationData(0x13010F, 372, "Chest"), - LocationName.SeadriftRowSerenityGem: LocationData(0x130110, 77, "Chest"), - LocationName.SeadriftRowKingRecipe: LocationData(0x130111, 78, "Chest"), - LocationName.SeadriftRowMythrilCrystal: LocationData(0x130112, 373, "Chest"), - LocationName.SeadriftRowCursedMedallion: LocationData(0x130113, 296, "Chest"), - LocationName.SeadriftRowShipGraveyardMap: LocationData(0x130114, 331, "Chest"), - LocationName.GrimReaper2: LocationData(0x130115, 22, "Get Bonus"), - LocationName.SecretAnsemReport6: LocationData(0x130116, 530, "Chest"), - LocationName.LuxordDataAPBoost: LocationData(0x130117, 557, "Chest"), + LocationName.RampartNavalMap: LocationData(70, "Chest"), + LocationName.RampartMythrilStone: LocationData(219, "Chest"), + LocationName.RampartDarkShard: LocationData(220, "Chest"), + LocationName.TownDarkStone: LocationData(71, "Chest"), + LocationName.TownAPBoost: LocationData(72, "Chest"), + LocationName.TownMythrilShard: LocationData(73, "Chest"), + LocationName.TownMythrilGem: LocationData(221, "Chest"), + LocationName.CaveMouthBrightShard: LocationData(74, "Chest"), + LocationName.CaveMouthMythrilShard: LocationData(223, "Chest"), + LocationName.IsladeMuertaMap: LocationData(329, "Chest"), + LocationName.BoatFight: LocationData(62, "Get Bonus"), + LocationName.InterceptorBarrels: LocationData(39, "Get Bonus"), + LocationName.PowderStoreAPBoost1: LocationData(369, "Chest"), + LocationName.PowderStoreAPBoost2: LocationData(370, "Chest"), + LocationName.MoonlightNookMythrilShard: LocationData(75, "Chest"), + LocationName.MoonlightNookSerenityGem: LocationData(224, "Chest"), + LocationName.MoonlightNookPowerStone: LocationData(371, "Chest"), + LocationName.Barbossa: LocationData(21, "Double Get Bonus"), + LocationName.BarbossaGetBonus: LocationData(21, "Second Get Bonus"), + LocationName.FollowtheWind: LocationData(263, "Chest"), + LocationName.GrimReaper1: LocationData(59, "Get Bonus"), + LocationName.InterceptorsHoldFeatherCharm: LocationData(252, "Chest"), + LocationName.SeadriftKeepAPBoost: LocationData(76, "Chest"), + LocationName.SeadriftKeepOrichalcum: LocationData(225, "Chest"), + LocationName.SeadriftKeepMeteorStaff: LocationData(372, "Chest"), + LocationName.SeadriftRowSerenityGem: LocationData(77, "Chest"), + LocationName.SeadriftRowKingRecipe: LocationData(78, "Chest"), + LocationName.SeadriftRowMythrilCrystal: LocationData(373, "Chest"), + LocationName.SeadriftRowCursedMedallion: LocationData(296, "Chest"), + LocationName.SeadriftRowShipGraveyardMap: LocationData(331, "Chest"), + LocationName.GrimReaper2: LocationData(22, "Get Bonus"), + LocationName.SecretAnsemReport6: LocationData(530, "Chest"), + LocationName.LuxordDataAPBoost: LocationData(557, "Chest"), } HB_Checks = { - LocationName.MarketplaceMap: LocationData(0x130118, 362, "Chest"), - LocationName.BoroughDriveRecovery: LocationData(0x130119, 194, "Chest"), - LocationName.BoroughAPBoost: LocationData(0x13011A, 195, "Chest"), - LocationName.BoroughHiPotion: LocationData(0x13011B, 196, "Chest"), - LocationName.BoroughMythrilShard: LocationData(0x13011C, 305, "Chest"), - LocationName.BoroughDarkShard: LocationData(0x13011D, 506, "Chest"), - LocationName.MerlinsHouseMembershipCard: LocationData(0x13011E, 256, "Chest"), - LocationName.MerlinsHouseBlizzardElement: LocationData(0x13011F, 292, "Chest"), - LocationName.Bailey: LocationData(0x130120, 47, "Get Bonus"), - LocationName.BaileySecretAnsemReport7: LocationData(0x130121, 531, "Chest"), - LocationName.BaseballCharm: LocationData(0x130122, 258, "Chest"), -} -HB2_Checks = { - LocationName.PosternCastlePerimeterMap: LocationData(0x130123, 310, "Chest"), - LocationName.PosternMythrilGem: LocationData(0x130124, 189, "Chest"), - LocationName.PosternAPBoost: LocationData(0x130125, 190, "Chest"), - LocationName.CorridorsMythrilStone: LocationData(0x130126, 200, "Chest"), - LocationName.CorridorsMythrilCrystal: LocationData(0x130127, 201, "Chest"), - LocationName.CorridorsDarkCrystal: LocationData(0x130128, 202, "Chest"), - LocationName.CorridorsAPBoost: LocationData(0x130129, 307, "Chest"), - LocationName.AnsemsStudyMasterForm: LocationData(0x13012A, 276, "Chest"), - LocationName.AnsemsStudySleepingLion: LocationData(0x13012B, 266, "Chest"), - LocationName.AnsemsStudySkillRecipe: LocationData(0x13012C, 184, "Chest"), - LocationName.AnsemsStudyUkuleleCharm: LocationData(0x13012D, 183, "Chest"), - LocationName.RestorationSiteMoonRecipe: LocationData(0x13012E, 309, "Chest"), - LocationName.RestorationSiteAPBoost: LocationData(0x13012F, 507, "Chest"), - LocationName.DemyxHB: LocationData(0x130130, 28, "Double Get Bonus"), - LocationName.DemyxHBGetBonus: LocationData(0x130131, 28, "Second Get Bonus"), - LocationName.FFFightsCureElement: LocationData(0x130132, 361, "Chest"), - LocationName.CrystalFissureTornPages: LocationData(0x130133, 179, "Chest"), - LocationName.CrystalFissureTheGreatMawMap: LocationData(0x130134, 489, "Chest"), - LocationName.CrystalFissureEnergyCrystal: LocationData(0x130135, 180, "Chest"), - LocationName.CrystalFissureAPBoost: LocationData(0x130136, 181, "Chest"), - LocationName.ThousandHeartless: LocationData(0x130137, 60, "Get Bonus"), - LocationName.ThousandHeartlessSecretAnsemReport1: LocationData(0x130138, 525, "Chest"), - LocationName.ThousandHeartlessIceCream: LocationData(0x130139, 269, "Chest"), - LocationName.ThousandHeartlessPicture: LocationData(0x13013A, 511, "Chest"), - LocationName.PosternGullWing: LocationData(0x13013B, 491, "Chest"), - LocationName.HeartlessManufactoryCosmicChain: LocationData(0x13013C, 311, "Chest"), - LocationName.SephirothBonus: LocationData(0x13013D, 35, "Get Bonus"), - LocationName.SephirothFenrir: LocationData(0x13013E, 282, "Chest"), - LocationName.WinnersProof: LocationData(0x13013F, 588, "Chest"), - LocationName.ProofofPeace: LocationData(0x130140, 589, "Chest"), - LocationName.DemyxDataAPBoost: LocationData(0x130141, 560, "Chest"), - LocationName.CoRDepthsAPBoost: LocationData(0x130142, 562, "Chest"), - LocationName.CoRDepthsPowerCrystal: LocationData(0x130143, 563, "Chest"), - LocationName.CoRDepthsFrostCrystal: LocationData(0x130144, 564, "Chest"), - LocationName.CoRDepthsManifestIllusion: LocationData(0x130145, 565, "Chest"), - LocationName.CoRDepthsAPBoost2: LocationData(0x130146, 566, "Chest"), - LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap: LocationData(0x130147, 580, "Chest"), - LocationName.CoRMineshaftLowerLevelAPBoost: LocationData(0x130148, 578, "Chest"), - -} -CoR_Checks = { - LocationName.CoRDepthsUpperLevelRemembranceGem: LocationData(0x130149, 567, "Chest"), - LocationName.CoRMiningAreaSerenityGem: LocationData(0x13014A, 568, "Chest"), - LocationName.CoRMiningAreaAPBoost: LocationData(0x13014B, 569, "Chest"), - LocationName.CoRMiningAreaSerenityCrystal: LocationData(0x13014C, 570, "Chest"), - LocationName.CoRMiningAreaManifestIllusion: LocationData(0x13014D, 571, "Chest"), - LocationName.CoRMiningAreaSerenityGem2: LocationData(0x13014E, 572, "Chest"), - LocationName.CoRMiningAreaDarkRemembranceMap: LocationData(0x13014F, 573, "Chest"), - LocationName.CoRMineshaftMidLevelPowerBoost: LocationData(0x130150, 581, "Chest"), - LocationName.CoREngineChamberSerenityCrystal: LocationData(0x130151, 574, "Chest"), - LocationName.CoREngineChamberRemembranceCrystal: LocationData(0x130152, 575, "Chest"), - LocationName.CoREngineChamberAPBoost: LocationData(0x130153, 576, "Chest"), - LocationName.CoREngineChamberManifestIllusion: LocationData(0x130154, 577, "Chest"), - LocationName.CoRMineshaftUpperLevelMagicBoost: LocationData(0x130155, 582, "Chest"), - LocationName.CoRMineshaftUpperLevelAPBoost: LocationData(0x130156, 579, "Chest"), - LocationName.TransporttoRemembrance: LocationData(0x130157, 72, "Get Bonus"), + LocationName.MarketplaceMap: LocationData(362, "Chest"), + LocationName.BoroughDriveRecovery: LocationData(194, "Chest"), + LocationName.BoroughAPBoost: LocationData(195, "Chest"), + LocationName.BoroughHiPotion: LocationData(196, "Chest"), + LocationName.BoroughMythrilShard: LocationData(305, "Chest"), + LocationName.BoroughDarkShard: LocationData(506, "Chest"), + LocationName.MerlinsHouseMembershipCard: LocationData(256, "Chest"), + LocationName.MerlinsHouseBlizzardElement: LocationData(292, "Chest"), + LocationName.Bailey: LocationData(47, "Get Bonus"), + LocationName.BaileySecretAnsemReport7: LocationData(531, "Chest"), + LocationName.BaseballCharm: LocationData(258, "Chest"), + LocationName.PosternCastlePerimeterMap: LocationData(310, "Chest"), + LocationName.PosternMythrilGem: LocationData(189, "Chest"), + LocationName.PosternAPBoost: LocationData(190, "Chest"), + LocationName.CorridorsMythrilStone: LocationData(200, "Chest"), + LocationName.CorridorsMythrilCrystal: LocationData(201, "Chest"), + LocationName.CorridorsDarkCrystal: LocationData(202, "Chest"), + LocationName.CorridorsAPBoost: LocationData(307, "Chest"), + LocationName.AnsemsStudyMasterForm: LocationData(276, "Chest"), + LocationName.AnsemsStudySleepingLion: LocationData(266, "Chest"), + LocationName.AnsemsStudySkillRecipe: LocationData(184, "Chest"), + LocationName.AnsemsStudyUkuleleCharm: LocationData(183, "Chest"), + LocationName.RestorationSiteMoonRecipe: LocationData(309, "Chest"), + LocationName.RestorationSiteAPBoost: LocationData(507, "Chest"), + LocationName.DemyxHB: LocationData(28, "Double Get Bonus"), + LocationName.DemyxHBGetBonus: LocationData(28, "Second Get Bonus"), + LocationName.FFFightsCureElement: LocationData(361, "Chest"), + LocationName.CrystalFissureTornPages: LocationData(179, "Chest"), + LocationName.CrystalFissureTheGreatMawMap: LocationData(489, "Chest"), + LocationName.CrystalFissureEnergyCrystal: LocationData(180, "Chest"), + LocationName.CrystalFissureAPBoost: LocationData(181, "Chest"), + LocationName.ThousandHeartless: LocationData(60, "Get Bonus"), + LocationName.ThousandHeartlessSecretAnsemReport1: LocationData(525, "Chest"), + LocationName.ThousandHeartlessIceCream: LocationData(269, "Chest"), + LocationName.ThousandHeartlessPicture: LocationData(511, "Chest"), + LocationName.PosternGullWing: LocationData(491, "Chest"), + LocationName.HeartlessManufactoryCosmicChain: LocationData(311, "Chest"), + LocationName.SephirothBonus: LocationData(35, "Get Bonus"), + LocationName.SephirothFenrir: LocationData(282, "Chest"), + LocationName.WinnersProof: LocationData(588, "Chest"), + LocationName.ProofofPeace: LocationData(589, "Chest"), + LocationName.DemyxDataAPBoost: LocationData(560, "Chest"), + LocationName.CoRDepthsAPBoost: LocationData(562, "Chest"), + LocationName.CoRDepthsPowerCrystal: LocationData(563, "Chest"), + LocationName.CoRDepthsFrostCrystal: LocationData(564, "Chest"), + LocationName.CoRDepthsManifestIllusion: LocationData(565, "Chest"), + LocationName.CoRDepthsAPBoost2: LocationData(566, "Chest"), + LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap: LocationData(580, "Chest"), + LocationName.CoRMineshaftLowerLevelAPBoost: LocationData(578, "Chest"), + LocationName.CoRDepthsUpperLevelRemembranceGem: LocationData(567, "Chest"), + LocationName.CoRMiningAreaSerenityGem: LocationData(568, "Chest"), + LocationName.CoRMiningAreaAPBoost: LocationData(569, "Chest"), + LocationName.CoRMiningAreaSerenityCrystal: LocationData(570, "Chest"), + LocationName.CoRMiningAreaManifestIllusion: LocationData(571, "Chest"), + LocationName.CoRMiningAreaSerenityGem2: LocationData(572, "Chest"), + LocationName.CoRMiningAreaDarkRemembranceMap: LocationData(573, "Chest"), + LocationName.CoRMineshaftMidLevelPowerBoost: LocationData(581, "Chest"), + LocationName.CoREngineChamberSerenityCrystal: LocationData(574, "Chest"), + LocationName.CoREngineChamberRemembranceCrystal: LocationData(575, "Chest"), + LocationName.CoREngineChamberAPBoost: LocationData(576, "Chest"), + LocationName.CoREngineChamberManifestIllusion: LocationData(577, "Chest"), + LocationName.CoRMineshaftUpperLevelMagicBoost: LocationData(582, "Chest"), + LocationName.CoRMineshaftUpperLevelAPBoost: LocationData(579, "Chest"), + LocationName.TransporttoRemembrance: LocationData(72, "Get Bonus"), } PL_Checks = { - LocationName.GorgeSavannahMap: LocationData(0x130158, 492, "Chest"), - LocationName.GorgeDarkGem: LocationData(0x130159, 404, "Chest"), - LocationName.GorgeMythrilStone: LocationData(0x13015A, 405, "Chest"), - LocationName.ElephantGraveyardFrostGem: LocationData(0x13015B, 401, "Chest"), - LocationName.ElephantGraveyardMythrilStone: LocationData(0x13015C, 402, "Chest"), - LocationName.ElephantGraveyardBrightStone: LocationData(0x13015D, 403, "Chest"), - LocationName.ElephantGraveyardAPBoost: LocationData(0x13015E, 508, "Chest"), - LocationName.ElephantGraveyardMythrilShard: LocationData(0x13015F, 509, "Chest"), - LocationName.PrideRockMap: LocationData(0x130160, 418, "Chest"), - LocationName.PrideRockMythrilStone: LocationData(0x130161, 392, "Chest"), - LocationName.PrideRockSerenityCrystal: LocationData(0x130162, 393, "Chest"), - LocationName.WildebeestValleyEnergyStone: LocationData(0x130163, 396, "Chest"), - LocationName.WildebeestValleyAPBoost: LocationData(0x130164, 397, "Chest"), - LocationName.WildebeestValleyMythrilGem: LocationData(0x130165, 398, "Chest"), - LocationName.WildebeestValleyMythrilStone: LocationData(0x130166, 399, "Chest"), - LocationName.WildebeestValleyLucidGem: LocationData(0x130167, 400, "Chest"), - LocationName.WastelandsMythrilShard: LocationData(0x130168, 406, "Chest"), - LocationName.WastelandsSerenityGem: LocationData(0x130169, 407, "Chest"), - LocationName.WastelandsMythrilStone: LocationData(0x13016A, 408, "Chest"), - LocationName.JungleSerenityGem: LocationData(0x13016B, 409, "Chest"), - LocationName.JungleMythrilStone: LocationData(0x13016C, 410, "Chest"), - LocationName.JungleSerenityCrystal: LocationData(0x13016D, 411, "Chest"), - LocationName.OasisMap: LocationData(0x13016E, 412, "Chest"), - LocationName.OasisTornPages: LocationData(0x13016F, 493, "Chest"), - LocationName.OasisAPBoost: LocationData(0x130170, 413, "Chest"), - LocationName.CircleofLife: LocationData(0x130171, 264, "Chest"), - LocationName.Hyenas1: LocationData(0x130172, 49, "Get Bonus"), - LocationName.Scar: LocationData(0x130173, 29, "Get Bonus"), - LocationName.ScarFireElement: LocationData(0x130174, 302, "Chest"), - -} -PL2_Checks = { - LocationName.Hyenas2: LocationData(0x130175, 50, "Get Bonus"), - LocationName.Groundshaker: LocationData(0x130176, 30, "Double Get Bonus"), - LocationName.GroundshakerGetBonus: LocationData(0x130177, 30, "Second Get Bonus"), - LocationName.SaixDataDefenseBoost: LocationData(0x130178, 556, "Chest"), + LocationName.GorgeSavannahMap: LocationData(492, "Chest"), + LocationName.GorgeDarkGem: LocationData(404, "Chest"), + LocationName.GorgeMythrilStone: LocationData(405, "Chest"), + LocationName.ElephantGraveyardFrostGem: LocationData(401, "Chest"), + LocationName.ElephantGraveyardMythrilStone: LocationData(402, "Chest"), + LocationName.ElephantGraveyardBrightStone: LocationData(403, "Chest"), + LocationName.ElephantGraveyardAPBoost: LocationData(508, "Chest"), + LocationName.ElephantGraveyardMythrilShard: LocationData(509, "Chest"), + LocationName.PrideRockMap: LocationData(418, "Chest"), + LocationName.PrideRockMythrilStone: LocationData(392, "Chest"), + LocationName.PrideRockSerenityCrystal: LocationData(393, "Chest"), + LocationName.WildebeestValleyEnergyStone: LocationData(396, "Chest"), + LocationName.WildebeestValleyAPBoost: LocationData(397, "Chest"), + LocationName.WildebeestValleyMythrilGem: LocationData(398, "Chest"), + LocationName.WildebeestValleyMythrilStone: LocationData(399, "Chest"), + LocationName.WildebeestValleyLucidGem: LocationData(400, "Chest"), + LocationName.WastelandsMythrilShard: LocationData(406, "Chest"), + LocationName.WastelandsSerenityGem: LocationData(407, "Chest"), + LocationName.WastelandsMythrilStone: LocationData(408, "Chest"), + LocationName.JungleSerenityGem: LocationData(409, "Chest"), + LocationName.JungleMythrilStone: LocationData(410, "Chest"), + LocationName.JungleSerenityCrystal: LocationData(411, "Chest"), + LocationName.OasisMap: LocationData(412, "Chest"), + LocationName.OasisTornPages: LocationData(493, "Chest"), + LocationName.OasisAPBoost: LocationData(413, "Chest"), + LocationName.CircleofLife: LocationData(264, "Chest"), + LocationName.Hyenas1: LocationData(49, "Get Bonus"), + LocationName.Scar: LocationData(29, "Get Bonus"), + LocationName.ScarFireElement: LocationData(302, "Chest"), + LocationName.Hyenas2: LocationData(50, "Get Bonus"), + LocationName.Groundshaker: LocationData(30, "Double Get Bonus"), + LocationName.GroundshakerGetBonus: LocationData(30, "Second Get Bonus"), + LocationName.SaixDataDefenseBoost: LocationData(556, "Chest"), } STT_Checks = { - LocationName.TwilightTownMap: LocationData(0x130179, 319, "Chest"), - LocationName.MunnyPouchOlette: LocationData(0x13017A, 288, "Chest"), - LocationName.StationDusks: LocationData(0x13017B, 54, "Get Bonus", "Roxas", 14), - LocationName.StationofSerenityPotion: LocationData(0x13017C, 315, "Chest"), - LocationName.StationofCallingPotion: LocationData(0x13017D, 472, "Chest"), - LocationName.TwilightThorn: LocationData(0x13017E, 33, "Get Bonus", "Roxas", 14), - LocationName.Axel1: LocationData(0x13017F, 73, "Get Bonus", "Roxas", 14), - LocationName.JunkChampionBelt: LocationData(0x130180, 389, "Chest"), - LocationName.JunkMedal: LocationData(0x130181, 390, "Chest"), - LocationName.TheStruggleTrophy: LocationData(0x130182, 519, "Chest"), - LocationName.CentralStationPotion1: LocationData(0x130183, 428, "Chest"), - LocationName.STTCentralStationHiPotion: LocationData(0x130184, 429, "Chest"), - LocationName.CentralStationPotion2: LocationData(0x130185, 430, "Chest"), - LocationName.SunsetTerraceAbilityRing: LocationData(0x130186, 434, "Chest"), - LocationName.SunsetTerraceHiPotion: LocationData(0x130187, 435, "Chest"), - LocationName.SunsetTerracePotion1: LocationData(0x130188, 436, "Chest"), - LocationName.SunsetTerracePotion2: LocationData(0x130189, 437, "Chest"), - LocationName.MansionFoyerHiPotion: LocationData(0x13018A, 449, "Chest"), - LocationName.MansionFoyerPotion1: LocationData(0x13018B, 450, "Chest"), - LocationName.MansionFoyerPotion2: LocationData(0x13018C, 451, "Chest"), - LocationName.MansionDiningRoomElvenBandanna: LocationData(0x13018D, 455, "Chest"), - LocationName.MansionDiningRoomPotion: LocationData(0x13018E, 456, "Chest"), - LocationName.NaminesSketches: LocationData(0x13018F, 289, "Chest"), - LocationName.MansionMap: LocationData(0x130190, 483, "Chest"), - LocationName.MansionLibraryHiPotion: LocationData(0x130191, 459, "Chest"), - LocationName.Axel2: LocationData(0x130192, 34, "Get Bonus", "Roxas", 14), - LocationName.MansionBasementCorridorHiPotion: LocationData(0x130193, 463, "Chest"), - LocationName.RoxasDataMagicBoost: LocationData(0x130194, 558, "Chest"), + LocationName.TwilightTownMap: LocationData(319, "Chest"), + LocationName.MunnyPouchOlette: LocationData(288, "Chest"), + LocationName.StationDusks: LocationData(54, "Get Bonus", "Roxas", 14), + LocationName.StationofSerenityPotion: LocationData(315, "Chest"), + LocationName.StationofCallingPotion: LocationData(472, "Chest"), + LocationName.TwilightThorn: LocationData(33, "Get Bonus", "Roxas", 14), + LocationName.Axel1: LocationData(73, "Get Bonus", "Roxas", 14), + LocationName.JunkChampionBelt: LocationData(389, "Chest"), + LocationName.JunkMedal: LocationData(390, "Chest"), + LocationName.TheStruggleTrophy: LocationData(519, "Chest"), + LocationName.CentralStationPotion1: LocationData(428, "Chest"), + LocationName.STTCentralStationHiPotion: LocationData(429, "Chest"), + LocationName.CentralStationPotion2: LocationData(430, "Chest"), + LocationName.SunsetTerraceAbilityRing: LocationData(434, "Chest"), + LocationName.SunsetTerraceHiPotion: LocationData(435, "Chest"), + LocationName.SunsetTerracePotion1: LocationData(436, "Chest"), + LocationName.SunsetTerracePotion2: LocationData(437, "Chest"), + LocationName.MansionFoyerHiPotion: LocationData(449, "Chest"), + LocationName.MansionFoyerPotion1: LocationData(450, "Chest"), + LocationName.MansionFoyerPotion2: LocationData(451, "Chest"), + LocationName.MansionDiningRoomElvenBandanna: LocationData(455, "Chest"), + LocationName.MansionDiningRoomPotion: LocationData(456, "Chest"), + LocationName.NaminesSketches: LocationData(289, "Chest"), + LocationName.MansionMap: LocationData(483, "Chest"), + LocationName.MansionLibraryHiPotion: LocationData(459, "Chest"), + LocationName.Axel2: LocationData(34, "Get Bonus", "Roxas", 14), + LocationName.MansionBasementCorridorHiPotion: LocationData(463, "Chest"), + LocationName.RoxasDataMagicBoost: LocationData(558, "Chest"), } TT_Checks = { - LocationName.OldMansionPotion: LocationData(0x130195, 447, "Chest"), - LocationName.OldMansionMythrilShard: LocationData(0x130196, 448, "Chest"), - LocationName.TheWoodsPotion: LocationData(0x130197, 442, "Chest"), - LocationName.TheWoodsMythrilShard: LocationData(0x130198, 443, "Chest"), - LocationName.TheWoodsHiPotion: LocationData(0x130199, 444, "Chest"), - LocationName.TramCommonHiPotion: LocationData(0x13019A, 420, "Chest"), - LocationName.TramCommonAPBoost: LocationData(0x13019B, 421, "Chest"), - LocationName.TramCommonTent: LocationData(0x13019C, 422, "Chest"), - LocationName.TramCommonMythrilShard1: LocationData(0x13019D, 423, "Chest"), - LocationName.TramCommonPotion1: LocationData(0x13019E, 424, "Chest"), - LocationName.TramCommonMythrilShard2: LocationData(0x13019F, 425, "Chest"), - LocationName.TramCommonPotion2: LocationData(0x1301A0, 484, "Chest"), - LocationName.StationPlazaSecretAnsemReport2: LocationData(0x1301A1, 526, "Chest"), - LocationName.MunnyPouchMickey: LocationData(0x1301A2, 290, "Chest"), - LocationName.CrystalOrb: LocationData(0x1301A3, 291, "Chest"), - LocationName.CentralStationTent: LocationData(0x1301A4, 431, "Chest"), - LocationName.TTCentralStationHiPotion: LocationData(0x1301A5, 432, "Chest"), - LocationName.CentralStationMythrilShard: LocationData(0x1301A6, 433, "Chest"), - LocationName.TheTowerPotion: LocationData(0x1301A7, 465, "Chest"), - LocationName.TheTowerHiPotion: LocationData(0x1301A8, 466, "Chest"), - LocationName.TheTowerEther: LocationData(0x1301A9, 522, "Chest"), - LocationName.TowerEntrywayEther: LocationData(0x1301AA, 467, "Chest"), - LocationName.TowerEntrywayMythrilShard: LocationData(0x1301AB, 468, "Chest"), - LocationName.SorcerersLoftTowerMap: LocationData(0x1301AC, 469, "Chest"), - LocationName.TowerWardrobeMythrilStone: LocationData(0x1301AD, 470, "Chest"), - LocationName.StarSeeker: LocationData(0x1301AE, 304, "Chest"), - LocationName.ValorForm: LocationData(0x1301AF, 286, "Chest"), - -} -TT2_Checks = { - LocationName.SeifersTrophy: LocationData(0x1301B0, 294, "Chest"), - LocationName.Oathkeeper: LocationData(0x1301B1, 265, "Chest"), - LocationName.LimitForm: LocationData(0x1301B2, 543, "Chest"), -} -TT3_Checks = { - LocationName.UndergroundConcourseMythrilGem: LocationData(0x1301B3, 479, "Chest"), - LocationName.UndergroundConcourseAPBoost: LocationData(0x1301B4, 481, "Chest"), - LocationName.UndergroundConcourseOrichalcum: LocationData(0x1301B5, 480, "Chest"), - LocationName.UndergroundConcourseMythrilCrystal: LocationData(0x1301B6, 482, "Chest"), - LocationName.TunnelwayOrichalcum: LocationData(0x1301B7, 477, "Chest"), - LocationName.TunnelwayMythrilCrystal: LocationData(0x1301B8, 478, "Chest"), - LocationName.SunsetTerraceOrichalcumPlus: LocationData(0x1301B9, 438, "Chest"), - LocationName.SunsetTerraceMythrilShard: LocationData(0x1301BA, 439, "Chest"), - LocationName.SunsetTerraceMythrilCrystal: LocationData(0x1301BB, 440, "Chest"), - LocationName.SunsetTerraceAPBoost: LocationData(0x1301BC, 441, "Chest"), - LocationName.MansionNobodies: LocationData(0x1301BD, 56, "Get Bonus"), - LocationName.MansionFoyerMythrilCrystal: LocationData(0x1301BE, 452, "Chest"), - LocationName.MansionFoyerMythrilStone: LocationData(0x1301BF, 453, "Chest"), - LocationName.MansionFoyerSerenityCrystal: LocationData(0x1301C0, 454, "Chest"), - LocationName.MansionDiningRoomMythrilCrystal: LocationData(0x1301C1, 457, "Chest"), - LocationName.MansionDiningRoomMythrilStone: LocationData(0x1301C2, 458, "Chest"), - LocationName.MansionLibraryOrichalcum: LocationData(0x1301C3, 460, "Chest"), - LocationName.BeamSecretAnsemReport10: LocationData(0x1301C4, 534, "Chest"), - LocationName.MansionBasementCorridorUltimateRecipe: LocationData(0x1301C5, 464, "Chest"), - LocationName.BetwixtandBetween: LocationData(0x1301C6, 63, "Get Bonus"), - LocationName.BetwixtandBetweenBondofFlame: LocationData(0x1301C7, 317, "Chest"), - LocationName.AxelDataMagicBoost: LocationData(0x1301C8, 561, "Chest"), + LocationName.OldMansionPotion: LocationData(447, "Chest"), + LocationName.OldMansionMythrilShard: LocationData(448, "Chest"), + LocationName.TheWoodsPotion: LocationData(442, "Chest"), + LocationName.TheWoodsMythrilShard: LocationData(443, "Chest"), + LocationName.TheWoodsHiPotion: LocationData(444, "Chest"), + LocationName.TramCommonHiPotion: LocationData(420, "Chest"), + LocationName.TramCommonAPBoost: LocationData(421, "Chest"), + LocationName.TramCommonTent: LocationData(422, "Chest"), + LocationName.TramCommonMythrilShard1: LocationData(423, "Chest"), + LocationName.TramCommonPotion1: LocationData(424, "Chest"), + LocationName.TramCommonMythrilShard2: LocationData(425, "Chest"), + LocationName.TramCommonPotion2: LocationData(484, "Chest"), + LocationName.StationPlazaSecretAnsemReport2: LocationData(526, "Chest"), + LocationName.MunnyPouchMickey: LocationData(290, "Chest"), + LocationName.CrystalOrb: LocationData(291, "Chest"), + LocationName.CentralStationTent: LocationData(431, "Chest"), + LocationName.TTCentralStationHiPotion: LocationData(432, "Chest"), + LocationName.CentralStationMythrilShard: LocationData(433, "Chest"), + LocationName.TheTowerPotion: LocationData(465, "Chest"), + LocationName.TheTowerHiPotion: LocationData(466, "Chest"), + LocationName.TheTowerEther: LocationData(522, "Chest"), + LocationName.TowerEntrywayEther: LocationData(467, "Chest"), + LocationName.TowerEntrywayMythrilShard: LocationData(468, "Chest"), + LocationName.SorcerersLoftTowerMap: LocationData(469, "Chest"), + LocationName.TowerWardrobeMythrilStone: LocationData(470, "Chest"), + LocationName.StarSeeker: LocationData(304, "Chest"), + LocationName.ValorForm: LocationData(286, "Chest"), + LocationName.SeifersTrophy: LocationData(294, "Chest"), + LocationName.Oathkeeper: LocationData(265, "Chest"), + LocationName.LimitForm: LocationData(543, "Chest"), + LocationName.UndergroundConcourseMythrilGem: LocationData(479, "Chest"), + LocationName.UndergroundConcourseAPBoost: LocationData(481, "Chest"), + LocationName.UndergroundConcourseOrichalcum: LocationData(480, "Chest"), + LocationName.UndergroundConcourseMythrilCrystal: LocationData(482, "Chest"), + LocationName.TunnelwayOrichalcum: LocationData(477, "Chest"), + LocationName.TunnelwayMythrilCrystal: LocationData(478, "Chest"), + LocationName.SunsetTerraceOrichalcumPlus: LocationData(438, "Chest"), + LocationName.SunsetTerraceMythrilShard: LocationData(439, "Chest"), + LocationName.SunsetTerraceMythrilCrystal: LocationData(440, "Chest"), + LocationName.SunsetTerraceAPBoost: LocationData(441, "Chest"), + LocationName.MansionNobodies: LocationData(56, "Get Bonus"), + LocationName.MansionFoyerMythrilCrystal: LocationData(452, "Chest"), + LocationName.MansionFoyerMythrilStone: LocationData(453, "Chest"), + LocationName.MansionFoyerSerenityCrystal: LocationData(454, "Chest"), + LocationName.MansionDiningRoomMythrilCrystal: LocationData(457, "Chest"), + LocationName.MansionDiningRoomMythrilStone: LocationData(458, "Chest"), + LocationName.MansionLibraryOrichalcum: LocationData(460, "Chest"), + LocationName.BeamSecretAnsemReport10: LocationData(534, "Chest"), + LocationName.MansionBasementCorridorUltimateRecipe: LocationData(464, "Chest"), + LocationName.BetwixtandBetween: LocationData(63, "Get Bonus"), + LocationName.BetwixtandBetweenBondofFlame: LocationData(317, "Chest"), + LocationName.AxelDataMagicBoost: LocationData(561, "Chest"), } TWTNW_Checks = { - LocationName.FragmentCrossingMythrilStone: LocationData(0x1301C9, 374, "Chest"), - LocationName.FragmentCrossingMythrilCrystal: LocationData(0x1301CA, 375, "Chest"), - LocationName.FragmentCrossingAPBoost: LocationData(0x1301CB, 376, "Chest"), - LocationName.FragmentCrossingOrichalcum: LocationData(0x1301CC, 377, "Chest"), - LocationName.Roxas: LocationData(0x1301CD, 69, "Double Get Bonus"), - LocationName.RoxasGetBonus: LocationData(0x1301CE, 69, "Second Get Bonus"), - LocationName.RoxasSecretAnsemReport8: LocationData(0x1301CF, 532, "Chest"), - LocationName.TwoBecomeOne: LocationData(0x1301D0, 277, "Chest"), - LocationName.MemorysSkyscaperMythrilCrystal: LocationData(0x1301D1, 391, "Chest"), - LocationName.MemorysSkyscaperAPBoost: LocationData(0x1301D2, 523, "Chest"), - LocationName.MemorysSkyscaperMythrilStone: LocationData(0x1301D3, 524, "Chest"), - LocationName.TheBrinkofDespairDarkCityMap: LocationData(0x1301D4, 335, "Chest"), - LocationName.TheBrinkofDespairOrichalcumPlus: LocationData(0x1301D5, 500, "Chest"), - LocationName.NothingsCallMythrilGem: LocationData(0x1301D6, 378, "Chest"), - LocationName.NothingsCallOrichalcum: LocationData(0x1301D7, 379, "Chest"), - LocationName.TwilightsViewCosmicBelt: LocationData(0x1301D8, 336, "Chest"), -} -TWTNW2_Checks = { - LocationName.XigbarBonus: LocationData(0x1301D9, 23, "Get Bonus"), - LocationName.XigbarSecretAnsemReport3: LocationData(0x1301DA, 527, "Chest"), - LocationName.NaughtsSkywayMythrilGem: LocationData(0x1301DB, 380, "Chest"), - LocationName.NaughtsSkywayOrichalcum: LocationData(0x1301DC, 381, "Chest"), - LocationName.NaughtsSkywayMythrilCrystal: LocationData(0x1301DD, 382, "Chest"), - LocationName.Oblivion: LocationData(0x1301DE, 278, "Chest"), - LocationName.CastleThatNeverWasMap: LocationData(0x1301DF, 496, "Chest"), - LocationName.Luxord: LocationData(0x1301E0, 24, "Double Get Bonus"), - LocationName.LuxordGetBonus: LocationData(0x1301E1, 24, "Second Get Bonus"), - LocationName.LuxordSecretAnsemReport9: LocationData(0x1301E2, 533, "Chest"), - LocationName.SaixBonus: LocationData(0x1301E3, 25, "Get Bonus"), - LocationName.SaixSecretAnsemReport12: LocationData(0x1301E4, 536, "Chest"), - LocationName.PreXemnas1SecretAnsemReport11: LocationData(0x1301E5, 535, "Chest"), - LocationName.RuinandCreationsPassageMythrilStone: LocationData(0x1301E6, 385, "Chest"), - LocationName.RuinandCreationsPassageAPBoost: LocationData(0x1301E7, 386, "Chest"), - LocationName.RuinandCreationsPassageMythrilCrystal: LocationData(0x1301E8, 387, "Chest"), - LocationName.RuinandCreationsPassageOrichalcum: LocationData(0x1301E9, 388, "Chest"), - LocationName.Xemnas1: LocationData(0x1301EA, 26, "Double Get Bonus"), - LocationName.Xemnas1GetBonus: LocationData(0x1301EB, 26, "Second Get Bonus"), - LocationName.Xemnas1SecretAnsemReport13: LocationData(0x1301EC, 537, "Chest"), - LocationName.FinalXemnas: LocationData(0x1301ED, 71, "Get Bonus"), - LocationName.XemnasDataPowerBoost: LocationData(0x1301EE, 554, "Chest"), + LocationName.FragmentCrossingMythrilStone: LocationData(374, "Chest"), + LocationName.FragmentCrossingMythrilCrystal: LocationData(375, "Chest"), + LocationName.FragmentCrossingAPBoost: LocationData(376, "Chest"), + LocationName.FragmentCrossingOrichalcum: LocationData(377, "Chest"), + LocationName.Roxas: LocationData(69, "Double Get Bonus"), + LocationName.RoxasGetBonus: LocationData(69, "Second Get Bonus"), + LocationName.RoxasSecretAnsemReport8: LocationData(532, "Chest"), + LocationName.TwoBecomeOne: LocationData(277, "Chest"), + LocationName.MemorysSkyscaperMythrilCrystal: LocationData(391, "Chest"), + LocationName.MemorysSkyscaperAPBoost: LocationData(523, "Chest"), + LocationName.MemorysSkyscaperMythrilStone: LocationData(524, "Chest"), + LocationName.TheBrinkofDespairDarkCityMap: LocationData(335, "Chest"), + LocationName.TheBrinkofDespairOrichalcumPlus: LocationData(500, "Chest"), + LocationName.NothingsCallMythrilGem: LocationData(378, "Chest"), + LocationName.NothingsCallOrichalcum: LocationData(379, "Chest"), + LocationName.TwilightsViewCosmicBelt: LocationData(336, "Chest"), + LocationName.XigbarBonus: LocationData(23, "Get Bonus"), + LocationName.XigbarSecretAnsemReport3: LocationData(527, "Chest"), + LocationName.NaughtsSkywayMythrilGem: LocationData(380, "Chest"), + LocationName.NaughtsSkywayOrichalcum: LocationData(381, "Chest"), + LocationName.NaughtsSkywayMythrilCrystal: LocationData(382, "Chest"), + LocationName.Oblivion: LocationData(278, "Chest"), + LocationName.CastleThatNeverWasMap: LocationData(496, "Chest"), + LocationName.Luxord: LocationData(24, "Double Get Bonus"), + LocationName.LuxordGetBonus: LocationData(24, "Second Get Bonus"), + LocationName.LuxordSecretAnsemReport9: LocationData(533, "Chest"), + LocationName.SaixBonus: LocationData(25, "Get Bonus"), + LocationName.SaixSecretAnsemReport12: LocationData(536, "Chest"), + LocationName.PreXemnas1SecretAnsemReport11: LocationData(535, "Chest"), + LocationName.RuinandCreationsPassageMythrilStone: LocationData(385, "Chest"), + LocationName.RuinandCreationsPassageAPBoost: LocationData(386, "Chest"), + LocationName.RuinandCreationsPassageMythrilCrystal: LocationData(387, "Chest"), + LocationName.RuinandCreationsPassageOrichalcum: LocationData(388, "Chest"), + LocationName.Xemnas1: LocationData(26, "Double Get Bonus"), + LocationName.Xemnas1GetBonus: LocationData(26, "Second Get Bonus"), + LocationName.Xemnas1SecretAnsemReport13: LocationData(537, "Chest"), + LocationName.FinalXemnas: LocationData(71, "Get Bonus"), + LocationName.XemnasDataPowerBoost: LocationData(554, "Chest"), } SoraLevels = { - LocationName.Lvl1: LocationData(0x1301EF, 1, "Levels"), - LocationName.Lvl2: LocationData(0x1301F0, 2, "Levels"), - LocationName.Lvl3: LocationData(0x1301F1, 3, "Levels"), - LocationName.Lvl4: LocationData(0x1301F2, 4, "Levels"), - LocationName.Lvl5: LocationData(0x1301F3, 5, "Levels"), - LocationName.Lvl6: LocationData(0x1301F4, 6, "Levels"), - LocationName.Lvl7: LocationData(0x1301F5, 7, "Levels"), - LocationName.Lvl8: LocationData(0x1301F6, 8, "Levels"), - LocationName.Lvl9: LocationData(0x1301F7, 9, "Levels"), - LocationName.Lvl10: LocationData(0x1301F8, 10, "Levels"), - LocationName.Lvl11: LocationData(0x1301F9, 11, "Levels"), - LocationName.Lvl12: LocationData(0x1301FA, 12, "Levels"), - LocationName.Lvl13: LocationData(0x1301FB, 13, "Levels"), - LocationName.Lvl14: LocationData(0x1301FC, 14, "Levels"), - LocationName.Lvl15: LocationData(0x1301FD, 15, "Levels"), - LocationName.Lvl16: LocationData(0x1301FE, 16, "Levels"), - LocationName.Lvl17: LocationData(0x1301FF, 17, "Levels"), - LocationName.Lvl18: LocationData(0x130200, 18, "Levels"), - LocationName.Lvl19: LocationData(0x130201, 19, "Levels"), - LocationName.Lvl20: LocationData(0x130202, 20, "Levels"), - LocationName.Lvl21: LocationData(0x130203, 21, "Levels"), - LocationName.Lvl22: LocationData(0x130204, 22, "Levels"), - LocationName.Lvl23: LocationData(0x130205, 23, "Levels"), - LocationName.Lvl24: LocationData(0x130206, 24, "Levels"), - LocationName.Lvl25: LocationData(0x130207, 25, "Levels"), - LocationName.Lvl26: LocationData(0x130208, 26, "Levels"), - LocationName.Lvl27: LocationData(0x130209, 27, "Levels"), - LocationName.Lvl28: LocationData(0x13020A, 28, "Levels"), - LocationName.Lvl29: LocationData(0x13020B, 29, "Levels"), - LocationName.Lvl30: LocationData(0x13020C, 30, "Levels"), - LocationName.Lvl31: LocationData(0x13020D, 31, "Levels"), - LocationName.Lvl32: LocationData(0x13020E, 32, "Levels"), - LocationName.Lvl33: LocationData(0x13020F, 33, "Levels"), - LocationName.Lvl34: LocationData(0x130210, 34, "Levels"), - LocationName.Lvl35: LocationData(0x130211, 35, "Levels"), - LocationName.Lvl36: LocationData(0x130212, 36, "Levels"), - LocationName.Lvl37: LocationData(0x130213, 37, "Levels"), - LocationName.Lvl38: LocationData(0x130214, 38, "Levels"), - LocationName.Lvl39: LocationData(0x130215, 39, "Levels"), - LocationName.Lvl40: LocationData(0x130216, 40, "Levels"), - LocationName.Lvl41: LocationData(0x130217, 41, "Levels"), - LocationName.Lvl42: LocationData(0x130218, 42, "Levels"), - LocationName.Lvl43: LocationData(0x130219, 43, "Levels"), - LocationName.Lvl44: LocationData(0x13021A, 44, "Levels"), - LocationName.Lvl45: LocationData(0x13021B, 45, "Levels"), - LocationName.Lvl46: LocationData(0x13021C, 46, "Levels"), - LocationName.Lvl47: LocationData(0x13021D, 47, "Levels"), - LocationName.Lvl48: LocationData(0x13021E, 48, "Levels"), - LocationName.Lvl49: LocationData(0x13021F, 49, "Levels"), - LocationName.Lvl50: LocationData(0x130220, 50, "Levels"), - LocationName.Lvl51: LocationData(0x130221, 51, "Levels"), - LocationName.Lvl52: LocationData(0x130222, 52, "Levels"), - LocationName.Lvl53: LocationData(0x130223, 53, "Levels"), - LocationName.Lvl54: LocationData(0x130224, 54, "Levels"), - LocationName.Lvl55: LocationData(0x130225, 55, "Levels"), - LocationName.Lvl56: LocationData(0x130226, 56, "Levels"), - LocationName.Lvl57: LocationData(0x130227, 57, "Levels"), - LocationName.Lvl58: LocationData(0x130228, 58, "Levels"), - LocationName.Lvl59: LocationData(0x130229, 59, "Levels"), - LocationName.Lvl60: LocationData(0x13022A, 60, "Levels"), - LocationName.Lvl61: LocationData(0x13022B, 61, "Levels"), - LocationName.Lvl62: LocationData(0x13022C, 62, "Levels"), - LocationName.Lvl63: LocationData(0x13022D, 63, "Levels"), - LocationName.Lvl64: LocationData(0x13022E, 64, "Levels"), - LocationName.Lvl65: LocationData(0x13022F, 65, "Levels"), - LocationName.Lvl66: LocationData(0x130230, 66, "Levels"), - LocationName.Lvl67: LocationData(0x130231, 67, "Levels"), - LocationName.Lvl68: LocationData(0x130232, 68, "Levels"), - LocationName.Lvl69: LocationData(0x130233, 69, "Levels"), - LocationName.Lvl70: LocationData(0x130234, 70, "Levels"), - LocationName.Lvl71: LocationData(0x130235, 71, "Levels"), - LocationName.Lvl72: LocationData(0x130236, 72, "Levels"), - LocationName.Lvl73: LocationData(0x130237, 73, "Levels"), - LocationName.Lvl74: LocationData(0x130238, 74, "Levels"), - LocationName.Lvl75: LocationData(0x130239, 75, "Levels"), - LocationName.Lvl76: LocationData(0x13023A, 76, "Levels"), - LocationName.Lvl77: LocationData(0x13023B, 77, "Levels"), - LocationName.Lvl78: LocationData(0x13023C, 78, "Levels"), - LocationName.Lvl79: LocationData(0x13023D, 79, "Levels"), - LocationName.Lvl80: LocationData(0x13023E, 80, "Levels"), - LocationName.Lvl81: LocationData(0x13023F, 81, "Levels"), - LocationName.Lvl82: LocationData(0x130240, 82, "Levels"), - LocationName.Lvl83: LocationData(0x130241, 83, "Levels"), - LocationName.Lvl84: LocationData(0x130242, 84, "Levels"), - LocationName.Lvl85: LocationData(0x130243, 85, "Levels"), - LocationName.Lvl86: LocationData(0x130244, 86, "Levels"), - LocationName.Lvl87: LocationData(0x130245, 87, "Levels"), - LocationName.Lvl88: LocationData(0x130246, 88, "Levels"), - LocationName.Lvl89: LocationData(0x130247, 89, "Levels"), - LocationName.Lvl90: LocationData(0x130248, 90, "Levels"), - LocationName.Lvl91: LocationData(0x130249, 91, "Levels"), - LocationName.Lvl92: LocationData(0x13024A, 92, "Levels"), - LocationName.Lvl93: LocationData(0x13024B, 93, "Levels"), - LocationName.Lvl94: LocationData(0x13024C, 94, "Levels"), - LocationName.Lvl95: LocationData(0x13024D, 95, "Levels"), - LocationName.Lvl96: LocationData(0x13024E, 96, "Levels"), - LocationName.Lvl97: LocationData(0x13024F, 97, "Levels"), - LocationName.Lvl98: LocationData(0x130250, 98, "Levels"), - LocationName.Lvl99: LocationData(0x130251, 99, "Levels"), + LocationName.Lvl2: LocationData(2, "Levels"), + LocationName.Lvl3: LocationData(3, "Levels"), + LocationName.Lvl4: LocationData(4, "Levels"), + LocationName.Lvl5: LocationData(5, "Levels"), + LocationName.Lvl6: LocationData(6, "Levels"), + LocationName.Lvl7: LocationData(7, "Levels"), + LocationName.Lvl8: LocationData(8, "Levels"), + LocationName.Lvl9: LocationData(9, "Levels"), + LocationName.Lvl10: LocationData(10, "Levels"), + LocationName.Lvl11: LocationData(11, "Levels"), + LocationName.Lvl12: LocationData(12, "Levels"), + LocationName.Lvl13: LocationData(13, "Levels"), + LocationName.Lvl14: LocationData(14, "Levels"), + LocationName.Lvl15: LocationData(15, "Levels"), + LocationName.Lvl16: LocationData(16, "Levels"), + LocationName.Lvl17: LocationData(17, "Levels"), + LocationName.Lvl18: LocationData(18, "Levels"), + LocationName.Lvl19: LocationData(19, "Levels"), + LocationName.Lvl20: LocationData(20, "Levels"), + LocationName.Lvl21: LocationData(21, "Levels"), + LocationName.Lvl22: LocationData(22, "Levels"), + LocationName.Lvl23: LocationData(23, "Levels"), + LocationName.Lvl24: LocationData(24, "Levels"), + LocationName.Lvl25: LocationData(25, "Levels"), + LocationName.Lvl26: LocationData(26, "Levels"), + LocationName.Lvl27: LocationData(27, "Levels"), + LocationName.Lvl28: LocationData(28, "Levels"), + LocationName.Lvl29: LocationData(29, "Levels"), + LocationName.Lvl30: LocationData(30, "Levels"), + LocationName.Lvl31: LocationData(31, "Levels"), + LocationName.Lvl32: LocationData(32, "Levels"), + LocationName.Lvl33: LocationData(33, "Levels"), + LocationName.Lvl34: LocationData(34, "Levels"), + LocationName.Lvl35: LocationData(35, "Levels"), + LocationName.Lvl36: LocationData(36, "Levels"), + LocationName.Lvl37: LocationData(37, "Levels"), + LocationName.Lvl38: LocationData(38, "Levels"), + LocationName.Lvl39: LocationData(39, "Levels"), + LocationName.Lvl40: LocationData(40, "Levels"), + LocationName.Lvl41: LocationData(41, "Levels"), + LocationName.Lvl42: LocationData(42, "Levels"), + LocationName.Lvl43: LocationData(43, "Levels"), + LocationName.Lvl44: LocationData(44, "Levels"), + LocationName.Lvl45: LocationData(45, "Levels"), + LocationName.Lvl46: LocationData(46, "Levels"), + LocationName.Lvl47: LocationData(47, "Levels"), + LocationName.Lvl48: LocationData(48, "Levels"), + LocationName.Lvl49: LocationData(49, "Levels"), + LocationName.Lvl50: LocationData(50, "Levels"), + LocationName.Lvl51: LocationData(51, "Levels"), + LocationName.Lvl52: LocationData(52, "Levels"), + LocationName.Lvl53: LocationData(53, "Levels"), + LocationName.Lvl54: LocationData(54, "Levels"), + LocationName.Lvl55: LocationData(55, "Levels"), + LocationName.Lvl56: LocationData(56, "Levels"), + LocationName.Lvl57: LocationData(57, "Levels"), + LocationName.Lvl58: LocationData(58, "Levels"), + LocationName.Lvl59: LocationData(59, "Levels"), + LocationName.Lvl60: LocationData(60, "Levels"), + LocationName.Lvl61: LocationData(61, "Levels"), + LocationName.Lvl62: LocationData(62, "Levels"), + LocationName.Lvl63: LocationData(63, "Levels"), + LocationName.Lvl64: LocationData(64, "Levels"), + LocationName.Lvl65: LocationData(65, "Levels"), + LocationName.Lvl66: LocationData(66, "Levels"), + LocationName.Lvl67: LocationData(67, "Levels"), + LocationName.Lvl68: LocationData(68, "Levels"), + LocationName.Lvl69: LocationData(69, "Levels"), + LocationName.Lvl70: LocationData(70, "Levels"), + LocationName.Lvl71: LocationData(71, "Levels"), + LocationName.Lvl72: LocationData(72, "Levels"), + LocationName.Lvl73: LocationData(73, "Levels"), + LocationName.Lvl74: LocationData(74, "Levels"), + LocationName.Lvl75: LocationData(75, "Levels"), + LocationName.Lvl76: LocationData(76, "Levels"), + LocationName.Lvl77: LocationData(77, "Levels"), + LocationName.Lvl78: LocationData(78, "Levels"), + LocationName.Lvl79: LocationData(79, "Levels"), + LocationName.Lvl80: LocationData(80, "Levels"), + LocationName.Lvl81: LocationData(81, "Levels"), + LocationName.Lvl82: LocationData(82, "Levels"), + LocationName.Lvl83: LocationData(83, "Levels"), + LocationName.Lvl84: LocationData(84, "Levels"), + LocationName.Lvl85: LocationData(85, "Levels"), + LocationName.Lvl86: LocationData(86, "Levels"), + LocationName.Lvl87: LocationData(87, "Levels"), + LocationName.Lvl88: LocationData(88, "Levels"), + LocationName.Lvl89: LocationData(89, "Levels"), + LocationName.Lvl90: LocationData(90, "Levels"), + LocationName.Lvl91: LocationData(91, "Levels"), + LocationName.Lvl92: LocationData(92, "Levels"), + LocationName.Lvl93: LocationData(93, "Levels"), + LocationName.Lvl94: LocationData(94, "Levels"), + LocationName.Lvl95: LocationData(95, "Levels"), + LocationName.Lvl96: LocationData(96, "Levels"), + LocationName.Lvl97: LocationData(97, "Levels"), + LocationName.Lvl98: LocationData(98, "Levels"), + LocationName.Lvl99: LocationData(99, "Levels"), } Form_Checks = { - LocationName.Valorlvl2: LocationData(0x130253, 2, "Forms", 1), - LocationName.Valorlvl3: LocationData(0x130254, 3, "Forms", 1), - LocationName.Valorlvl4: LocationData(0x130255, 4, "Forms", 1), - LocationName.Valorlvl5: LocationData(0x130256, 5, "Forms", 1), - LocationName.Valorlvl6: LocationData(0x130257, 6, "Forms", 1), - LocationName.Valorlvl7: LocationData(0x130258, 7, "Forms", 1), + LocationName.Valorlvl2: LocationData(2, "Forms", 1), + LocationName.Valorlvl3: LocationData(3, "Forms", 1), + LocationName.Valorlvl4: LocationData(4, "Forms", 1), + LocationName.Valorlvl5: LocationData(5, "Forms", 1), + LocationName.Valorlvl6: LocationData(6, "Forms", 1), + LocationName.Valorlvl7: LocationData(7, "Forms", 1), - LocationName.Wisdomlvl2: LocationData(0x13025A, 2, "Forms", 2), - LocationName.Wisdomlvl3: LocationData(0x13025B, 3, "Forms", 2), - LocationName.Wisdomlvl4: LocationData(0x13025C, 4, "Forms", 2), - LocationName.Wisdomlvl5: LocationData(0x13025D, 5, "Forms", 2), - LocationName.Wisdomlvl6: LocationData(0x13025E, 6, "Forms", 2), - LocationName.Wisdomlvl7: LocationData(0x13025F, 7, "Forms", 2), + LocationName.Wisdomlvl2: LocationData(2, "Forms", 2), + LocationName.Wisdomlvl3: LocationData(3, "Forms", 2), + LocationName.Wisdomlvl4: LocationData(4, "Forms", 2), + LocationName.Wisdomlvl5: LocationData(5, "Forms", 2), + LocationName.Wisdomlvl6: LocationData(6, "Forms", 2), + LocationName.Wisdomlvl7: LocationData(7, "Forms", 2), - LocationName.Limitlvl2: LocationData(0x130261, 2, "Forms", 3), - LocationName.Limitlvl3: LocationData(0x130262, 3, "Forms", 3), - LocationName.Limitlvl4: LocationData(0x130263, 4, "Forms", 3), - LocationName.Limitlvl5: LocationData(0x130264, 5, "Forms", 3), - LocationName.Limitlvl6: LocationData(0x130265, 6, "Forms", 3), - LocationName.Limitlvl7: LocationData(0x130266, 7, "Forms", 3), + LocationName.Limitlvl2: LocationData(2, "Forms", 3), + LocationName.Limitlvl3: LocationData(3, "Forms", 3), + LocationName.Limitlvl4: LocationData(4, "Forms", 3), + LocationName.Limitlvl5: LocationData(5, "Forms", 3), + LocationName.Limitlvl6: LocationData(6, "Forms", 3), + LocationName.Limitlvl7: LocationData(7, "Forms", 3), - LocationName.Masterlvl2: LocationData(0x130268, 2, "Forms", 4), - LocationName.Masterlvl3: LocationData(0x130269, 3, "Forms", 4), - LocationName.Masterlvl4: LocationData(0x13026A, 4, "Forms", 4), - LocationName.Masterlvl5: LocationData(0x13026B, 5, "Forms", 4), - LocationName.Masterlvl6: LocationData(0x13026C, 6, "Forms", 4), - LocationName.Masterlvl7: LocationData(0x13026D, 7, "Forms", 4), + LocationName.Masterlvl2: LocationData(2, "Forms", 4), + LocationName.Masterlvl3: LocationData(3, "Forms", 4), + LocationName.Masterlvl4: LocationData(4, "Forms", 4), + LocationName.Masterlvl5: LocationData(5, "Forms", 4), + LocationName.Masterlvl6: LocationData(6, "Forms", 4), + LocationName.Masterlvl7: LocationData(7, "Forms", 4), - LocationName.Finallvl2: LocationData(0x13026F, 2, "Forms", 5), - LocationName.Finallvl3: LocationData(0x130270, 3, "Forms", 5), - LocationName.Finallvl4: LocationData(0x130271, 4, "Forms", 5), - LocationName.Finallvl5: LocationData(0x130272, 5, "Forms", 5), - LocationName.Finallvl6: LocationData(0x130273, 6, "Forms", 5), - LocationName.Finallvl7: LocationData(0x130274, 7, "Forms", 5), + LocationName.Finallvl2: LocationData(2, "Forms", 5), + LocationName.Finallvl3: LocationData(3, "Forms", 5), + LocationName.Finallvl4: LocationData(4, "Forms", 5), + LocationName.Finallvl5: LocationData(5, "Forms", 5), + LocationName.Finallvl6: LocationData(6, "Forms", 5), + LocationName.Finallvl7: LocationData(7, "Forms", 5), +} +Summon_Checks = { + LocationName.Summonlvl2: LocationData(2, "Summons"), + LocationName.Summonlvl3: LocationData(3, "Summons"), + LocationName.Summonlvl4: LocationData(4, "Summons"), + LocationName.Summonlvl5: LocationData(5, "Summons"), + LocationName.Summonlvl6: LocationData(6, "Summons"), + LocationName.Summonlvl7: LocationData(7, "Summons"), } GoA_Checks = { - LocationName.GardenofAssemblageMap: LocationData(0x130275, 585, "Chest"), - LocationName.GoALostIllusion: LocationData(0x130276, 586, "Chest"), - LocationName.ProofofNonexistence: LocationData(0x130277, 590, "Chest"), + LocationName.GardenofAssemblageMap: LocationData(585, "Chest"), + LocationName.GoALostIllusion: LocationData(586, "Chest"), + LocationName.ProofofNonexistence: LocationData(590, "Chest"), } Keyblade_Slots = { - LocationName.FAKESlot: LocationData(0x130278, 116, "Keyblade"), - LocationName.DetectionSaberSlot: LocationData(0x130279, 83, "Keyblade"), - LocationName.EdgeofUltimaSlot: LocationData(0x13027A, 84, "Keyblade"), - LocationName.KingdomKeySlot: LocationData(0x13027B, 80, "Keyblade"), - LocationName.OathkeeperSlot: LocationData(0x13027C, 81, "Keyblade"), - LocationName.OblivionSlot: LocationData(0x13027D, 82, "Keyblade"), - LocationName.StarSeekerSlot: LocationData(0x13027E, 123, "Keyblade"), - LocationName.HiddenDragonSlot: LocationData(0x13027F, 124, "Keyblade"), - LocationName.HerosCrestSlot: LocationData(0x130280, 127, "Keyblade"), - LocationName.MonochromeSlot: LocationData(0x130281, 128, "Keyblade"), - LocationName.FollowtheWindSlot: LocationData(0x130282, 129, "Keyblade"), - LocationName.CircleofLifeSlot: LocationData(0x130283, 130, "Keyblade"), - LocationName.PhotonDebuggerSlot: LocationData(0x130284, 131, "Keyblade"), - LocationName.GullWingSlot: LocationData(0x130285, 132, "Keyblade"), - LocationName.RumblingRoseSlot: LocationData(0x130286, 133, "Keyblade"), - LocationName.GuardianSoulSlot: LocationData(0x130287, 134, "Keyblade"), - LocationName.WishingLampSlot: LocationData(0x130288, 135, "Keyblade"), - LocationName.DecisivePumpkinSlot: LocationData(0x130289, 136, "Keyblade"), - LocationName.SweetMemoriesSlot: LocationData(0x13028A, 138, "Keyblade"), - LocationName.MysteriousAbyssSlot: LocationData(0x13028B, 139, "Keyblade"), - LocationName.SleepingLionSlot: LocationData(0x13028C, 137, "Keyblade"), - LocationName.BondofFlameSlot: LocationData(0x13028D, 141, "Keyblade"), - LocationName.TwoBecomeOneSlot: LocationData(0x13028E, 148, "Keyblade"), - LocationName.FatalCrestSlot: LocationData(0x13028F, 140, "Keyblade"), - LocationName.FenrirSlot: LocationData(0x130290, 142, "Keyblade"), - LocationName.UltimaWeaponSlot: LocationData(0x130291, 143, "Keyblade"), - LocationName.WinnersProofSlot: LocationData(0x130292, 149, "Keyblade"), - LocationName.PurebloodSlot: LocationData(0x1302DB, 85, "Keyblade"), -} -# checks are given when talking to the computer in the GoA -Critical_Checks = { - LocationName.Crit_1: LocationData(0x130293, 1, "Critical"), - LocationName.Crit_2: LocationData(0x130294, 1, "Critical"), - LocationName.Crit_3: LocationData(0x130295, 1, "Critical"), - LocationName.Crit_4: LocationData(0x130296, 1, "Critical"), - LocationName.Crit_5: LocationData(0x130297, 1, "Critical"), - LocationName.Crit_6: LocationData(0x130298, 1, "Critical"), - LocationName.Crit_7: LocationData(0x130299, 1, "Critical"), + LocationName.FAKESlot: LocationData(116, "Keyblade"), + LocationName.DetectionSaberSlot: LocationData(83, "Keyblade"), + LocationName.EdgeofUltimaSlot: LocationData(84, "Keyblade"), + LocationName.KingdomKeySlot: LocationData(80, "Keyblade"), + LocationName.OathkeeperSlot: LocationData(81, "Keyblade"), + LocationName.OblivionSlot: LocationData(82, "Keyblade"), + LocationName.StarSeekerSlot: LocationData(123, "Keyblade"), + LocationName.HiddenDragonSlot: LocationData(124, "Keyblade"), + LocationName.HerosCrestSlot: LocationData(127, "Keyblade"), + LocationName.MonochromeSlot: LocationData(128, "Keyblade"), + LocationName.FollowtheWindSlot: LocationData(129, "Keyblade"), + LocationName.CircleofLifeSlot: LocationData(130, "Keyblade"), + LocationName.PhotonDebuggerSlot: LocationData(131, "Keyblade"), + LocationName.GullWingSlot: LocationData(132, "Keyblade"), + LocationName.RumblingRoseSlot: LocationData(133, "Keyblade"), + LocationName.GuardianSoulSlot: LocationData(134, "Keyblade"), + LocationName.WishingLampSlot: LocationData(135, "Keyblade"), + LocationName.DecisivePumpkinSlot: LocationData(136, "Keyblade"), + LocationName.SweetMemoriesSlot: LocationData(138, "Keyblade"), + LocationName.MysteriousAbyssSlot: LocationData(139, "Keyblade"), + LocationName.SleepingLionSlot: LocationData(137, "Keyblade"), + LocationName.BondofFlameSlot: LocationData(141, "Keyblade"), + LocationName.TwoBecomeOneSlot: LocationData(148, "Keyblade"), + LocationName.FatalCrestSlot: LocationData(140, "Keyblade"), + LocationName.FenrirSlot: LocationData(142, "Keyblade"), + LocationName.UltimaWeaponSlot: LocationData(143, "Keyblade"), + LocationName.WinnersProofSlot: LocationData(149, "Keyblade"), + LocationName.PurebloodSlot: LocationData(85, "Keyblade"), } Donald_Checks = { - LocationName.DonaldScreens: LocationData(0x13029A, 45, "Get Bonus", "Donald", 2), - LocationName.DonaldDemyxHBGetBonus: LocationData(0x13029B, 28, "Get Bonus", "Donald", 2), - LocationName.DonaldDemyxOC: LocationData(0x13029C, 58, "Get Bonus", "Donald", 2), - LocationName.DonaldBoatPete: LocationData(0x13029D, 16, "Double Get Bonus", "Donald", 2), - LocationName.DonaldBoatPeteGetBonus: LocationData(0x13029E, 16, "Second Get Bonus", "Donald", 2), - LocationName.DonaldPrisonKeeper: LocationData(0x13029F, 18, "Get Bonus", "Donald", 2), - LocationName.DonaldScar: LocationData(0x1302A0, 29, "Get Bonus", "Donald", 2), - LocationName.DonaldSolarSailer: LocationData(0x1302A1, 61, "Get Bonus", "Donald", 2), - LocationName.DonaldExperiment: LocationData(0x1302A2, 20, "Get Bonus", "Donald", 2), - LocationName.DonaldBoatFight: LocationData(0x1302A3, 62, "Get Bonus", "Donald", 2), - LocationName.DonaldMansionNobodies: LocationData(0x1302A4, 56, "Get Bonus", "Donald", 2), - LocationName.DonaldThresholder: LocationData(0x1302A5, 2, "Get Bonus", "Donald", 2), - LocationName.DonaldXaldinGetBonus: LocationData(0x1302A6, 4, "Get Bonus", "Donald", 2), - LocationName.DonaladGrimReaper2: LocationData(0x1302A7, 22, "Get Bonus", "Donald", 2), + LocationName.DonaldScreens: LocationData(45, "Get Bonus", "Donald", 2), + LocationName.DonaldDemyxHBGetBonus: LocationData(28, "Get Bonus", "Donald", 2), + LocationName.DonaldDemyxOC: LocationData(58, "Get Bonus", "Donald", 2), + LocationName.DonaldBoatPete: LocationData(16, "Double Get Bonus", "Donald", 2), + LocationName.DonaldBoatPeteGetBonus: LocationData(16, "Second Get Bonus", "Donald", 2), + LocationName.DonaldPrisonKeeper: LocationData(18, "Get Bonus", "Donald", 2), + LocationName.DonaldScar: LocationData(29, "Get Bonus", "Donald", 2), + LocationName.DonaldSolarSailer: LocationData(61, "Get Bonus", "Donald", 2), + LocationName.DonaldExperiment: LocationData(20, "Get Bonus", "Donald", 2), + LocationName.DonaldBoatFight: LocationData(62, "Get Bonus", "Donald", 2), + LocationName.DonaldMansionNobodies: LocationData(56, "Get Bonus", "Donald", 2), + LocationName.DonaldThresholder: LocationData(2, "Get Bonus", "Donald", 2), + LocationName.DonaldXaldinGetBonus: LocationData(4, "Get Bonus", "Donald", 2), + LocationName.DonaladGrimReaper2: LocationData(22, "Get Bonus", "Donald", 2), - LocationName.CometStaff: LocationData(0x1302A8, 90, "Keyblade", "Donald"), - LocationName.HammerStaff: LocationData(0x1302A9, 87, "Keyblade", "Donald"), - LocationName.LordsBroom: LocationData(0x1302AA, 91, "Keyblade", "Donald"), - LocationName.MagesStaff: LocationData(0x1302AB, 86, "Keyblade", "Donald"), - LocationName.MeteorStaff: LocationData(0x1302AC, 89, "Keyblade", "Donald"), - LocationName.NobodyLance: LocationData(0x1302AD, 94, "Keyblade", "Donald"), - LocationName.PreciousMushroom: LocationData(0x1302AE, 154, "Keyblade", "Donald"), - LocationName.PreciousMushroom2: LocationData(0x1302AF, 155, "Keyblade", "Donald"), - LocationName.PremiumMushroom: LocationData(0x1302B0, 156, "Keyblade", "Donald"), - LocationName.RisingDragon: LocationData(0x1302B1, 93, "Keyblade", "Donald"), - LocationName.SaveTheQueen2: LocationData(0x1302B2, 146, "Keyblade", "Donald"), - LocationName.ShamansRelic: LocationData(0x1302B3, 95, "Keyblade", "Donald"), - LocationName.VictoryBell: LocationData(0x1302B4, 88, "Keyblade", "Donald"), - LocationName.WisdomWand: LocationData(0x1302B5, 92, "Keyblade", "Donald"), - LocationName.Centurion2: LocationData(0x1302B6, 151, "Keyblade", "Donald"), - LocationName.DonaldAbuEscort: LocationData(0x1302B7, 42, "Get Bonus", "Donald", 2), - LocationName.DonaldStarting1: LocationData(0x1302B8, 2, "Critical", "Donald"), - LocationName.DonaldStarting2: LocationData(0x1302B9, 2, "Critical", "Donald"), + LocationName.CometStaff: LocationData(90, "Keyblade", "Donald"), + LocationName.HammerStaff: LocationData(87, "Keyblade", "Donald"), + LocationName.LordsBroom: LocationData(91, "Keyblade", "Donald"), + LocationName.MagesStaff: LocationData(86, "Keyblade", "Donald"), + LocationName.MeteorStaff: LocationData(89, "Keyblade", "Donald"), + LocationName.NobodyLance: LocationData(94, "Keyblade", "Donald"), + LocationName.PreciousMushroom: LocationData(154, "Keyblade", "Donald"), + LocationName.PreciousMushroom2: LocationData(155, "Keyblade", "Donald"), + LocationName.PremiumMushroom: LocationData(156, "Keyblade", "Donald"), + LocationName.RisingDragon: LocationData(93, "Keyblade", "Donald"), + LocationName.SaveTheQueen2: LocationData(146, "Keyblade", "Donald"), + LocationName.ShamansRelic: LocationData(95, "Keyblade", "Donald"), + LocationName.VictoryBell: LocationData(88, "Keyblade", "Donald"), + LocationName.WisdomWand: LocationData(92, "Keyblade", "Donald"), + LocationName.Centurion2: LocationData(151, "Keyblade", "Donald"), + LocationName.DonaldAbuEscort: LocationData(42, "Get Bonus", "Donald", 2), + # LocationName.DonaldStarting1: LocationData(2, "Critical", "Donald"), + # LocationName.DonaldStarting2: LocationData(2, "Critical", "Donald"), } Goofy_Checks = { - LocationName.GoofyBarbossa: LocationData(0x1302BA, 21, "Double Get Bonus", "Goofy", 3), - LocationName.GoofyBarbossaGetBonus: LocationData(0x1302BB, 21, "Second Get Bonus", "Goofy", 3), - LocationName.GoofyGrimReaper1: LocationData(0x1302BC, 59, "Get Bonus", "Goofy", 3), - LocationName.GoofyHostileProgram: LocationData(0x1302BD, 31, "Get Bonus", "Goofy", 3), - LocationName.GoofyHyenas1: LocationData(0x1302BE, 49, "Get Bonus", "Goofy", 3), - LocationName.GoofyHyenas2: LocationData(0x1302BF, 50, "Get Bonus", "Goofy", 3), - LocationName.GoofyLock: LocationData(0x1302C0, 40, "Get Bonus", "Goofy", 3), - LocationName.GoofyOogieBoogie: LocationData(0x1302C1, 19, "Get Bonus", "Goofy", 3), - LocationName.GoofyPeteOC: LocationData(0x1302C2, 6, "Get Bonus", "Goofy", 3), - LocationName.GoofyFuturePete: LocationData(0x1302C3, 17, "Get Bonus", "Goofy", 3), - LocationName.GoofyShanYu: LocationData(0x1302C4, 9, "Get Bonus", "Goofy", 3), - LocationName.GoofyStormRider: LocationData(0x1302C5, 10, "Get Bonus", "Goofy", 3), - LocationName.GoofyBeast: LocationData(0x1302C6, 12, "Get Bonus", "Goofy", 3), - LocationName.GoofyInterceptorBarrels: LocationData(0x1302C7, 39, "Get Bonus", "Goofy", 3), - LocationName.GoofyTreasureRoom: LocationData(0x1302C8, 46, "Get Bonus", "Goofy", 3), - LocationName.GoofyZexion: LocationData(0x1302C9, 66, "Get Bonus", "Goofy", 3), + LocationName.GoofyBarbossa: LocationData(21, "Double Get Bonus", "Goofy", 3), + LocationName.GoofyBarbossaGetBonus: LocationData(21, "Second Get Bonus", "Goofy", 3), + LocationName.GoofyGrimReaper1: LocationData(59, "Get Bonus", "Goofy", 3), + LocationName.GoofyHostileProgram: LocationData(31, "Get Bonus", "Goofy", 3), + LocationName.GoofyHyenas1: LocationData(49, "Get Bonus", "Goofy", 3), + LocationName.GoofyHyenas2: LocationData(50, "Get Bonus", "Goofy", 3), + LocationName.GoofyLock: LocationData(40, "Get Bonus", "Goofy", 3), + LocationName.GoofyOogieBoogie: LocationData(19, "Get Bonus", "Goofy", 3), + LocationName.GoofyPeteOC: LocationData(6, "Get Bonus", "Goofy", 3), + LocationName.GoofyFuturePete: LocationData(17, "Get Bonus", "Goofy", 3), + LocationName.GoofyShanYu: LocationData(9, "Get Bonus", "Goofy", 3), + LocationName.GoofyStormRider: LocationData(10, "Get Bonus", "Goofy", 3), + LocationName.GoofyBeast: LocationData(12, "Get Bonus", "Goofy", 3), + LocationName.GoofyInterceptorBarrels: LocationData(39, "Get Bonus", "Goofy", 3), + LocationName.GoofyTreasureRoom: LocationData(46, "Get Bonus", "Goofy", 3), + LocationName.GoofyZexion: LocationData(66, "Get Bonus", "Goofy", 3), + + LocationName.AdamantShield: LocationData(100, "Keyblade", "Goofy"), + LocationName.AkashicRecord: LocationData(107, "Keyblade", "Goofy"), + LocationName.ChainGear: LocationData(101, "Keyblade", "Goofy"), + LocationName.DreamCloud: LocationData(104, "Keyblade", "Goofy"), + LocationName.FallingStar: LocationData(103, "Keyblade", "Goofy"), + LocationName.FrozenPride2: LocationData(158, "Keyblade", "Goofy"), + LocationName.GenjiShield: LocationData(106, "Keyblade", "Goofy"), + LocationName.KnightDefender: LocationData(105, "Keyblade", "Goofy"), + LocationName.KnightsShield: LocationData(99, "Keyblade", "Goofy"), + LocationName.MajesticMushroom: LocationData(161, "Keyblade", "Goofy"), + LocationName.MajesticMushroom2: LocationData(162, "Keyblade", "Goofy"), + LocationName.NobodyGuard: LocationData(108, "Keyblade", "Goofy"), + LocationName.OgreShield: LocationData(102, "Keyblade", "Goofy"), + LocationName.SaveTheKing2: LocationData(147, "Keyblade", "Goofy"), + LocationName.UltimateMushroom: LocationData(163, "Keyblade", "Goofy"), + # LocationName.GoofyStarting1: LocationData(3, "Critical", "Goofy"), + # LocationName.GoofyStarting2: LocationData(3, "Critical", "Goofy"), +} + +Atlantica_Checks = { + LocationName.UnderseaKingdomMap: LocationData(367, "Chest"), + LocationName.MysteriousAbyss: LocationData(287, "Chest"), # needs 2 magnets + LocationName.MusicalBlizzardElement: LocationData(279, "Chest"), # 2 magnets all thunders + LocationName.MusicalOrichalcumPlus: LocationData(538, "Chest"), # 2 magnets all thunders +} + +event_location_to_item = { + LocationName.HostileProgramEventLocation: ItemName.HostileProgramEvent, + LocationName.McpEventLocation: ItemName.McpEvent, + # LocationName.ASLarxeneEventLocation: ItemName.ASLarxeneEvent, + LocationName.DataLarxeneEventLocation: ItemName.DataLarxeneEvent, + LocationName.BarbosaEventLocation: ItemName.BarbosaEvent, + LocationName.GrimReaper1EventLocation: ItemName.GrimReaper1Event, + LocationName.GrimReaper2EventLocation: ItemName.GrimReaper2Event, + LocationName.DataLuxordEventLocation: ItemName.DataLuxordEvent, + LocationName.DataAxelEventLocation: ItemName.DataAxelEvent, + LocationName.CerberusEventLocation: ItemName.CerberusEvent, + LocationName.OlympusPeteEventLocation: ItemName.OlympusPeteEvent, + LocationName.HydraEventLocation: ItemName.HydraEvent, + LocationName.OcPainAndPanicCupEventLocation: ItemName.OcPainAndPanicCupEvent, + LocationName.OcCerberusCupEventLocation: ItemName.OcCerberusCupEvent, + LocationName.HadesEventLocation: ItemName.HadesEvent, + # LocationName.ASZexionEventLocation: ItemName.ASZexionEvent, + LocationName.DataZexionEventLocation: ItemName.DataZexionEvent, + LocationName.Oc2TitanCupEventLocation: ItemName.Oc2TitanCupEvent, + LocationName.Oc2GofCupEventLocation: ItemName.Oc2GofCupEvent, + # LocationName.Oc2CupsEventLocation: ItemName.Oc2CupsEventLocation, + LocationName.HadesCupEventLocations: ItemName.HadesCupEvents, + LocationName.PrisonKeeperEventLocation: ItemName.PrisonKeeperEvent, + LocationName.OogieBoogieEventLocation: ItemName.OogieBoogieEvent, + LocationName.ExperimentEventLocation: ItemName.ExperimentEvent, + # LocationName.ASVexenEventLocation: ItemName.ASVexenEvent, + LocationName.DataVexenEventLocation: ItemName.DataVexenEvent, + LocationName.ShanYuEventLocation: ItemName.ShanYuEvent, + LocationName.AnsemRikuEventLocation: ItemName.AnsemRikuEvent, + LocationName.StormRiderEventLocation: ItemName.StormRiderEvent, + LocationName.DataXigbarEventLocation: ItemName.DataXigbarEvent, + LocationName.RoxasEventLocation: ItemName.RoxasEvent, + LocationName.XigbarEventLocation: ItemName.XigbarEvent, + LocationName.LuxordEventLocation: ItemName.LuxordEvent, + LocationName.SaixEventLocation: ItemName.SaixEvent, + LocationName.XemnasEventLocation: ItemName.XemnasEvent, + LocationName.ArmoredXemnasEventLocation: ItemName.ArmoredXemnasEvent, + LocationName.ArmoredXemnas2EventLocation: ItemName.ArmoredXemnas2Event, + # LocationName.FinalXemnasEventLocation: ItemName.FinalXemnasEvent, + LocationName.DataXemnasEventLocation: ItemName.DataXemnasEvent, + LocationName.ThresholderEventLocation: ItemName.ThresholderEvent, + LocationName.BeastEventLocation: ItemName.BeastEvent, + LocationName.DarkThornEventLocation: ItemName.DarkThornEvent, + LocationName.XaldinEventLocation: ItemName.XaldinEvent, + LocationName.DataXaldinEventLocation: ItemName.DataXaldinEvent, + LocationName.TwinLordsEventLocation: ItemName.TwinLordsEvent, + LocationName.GenieJafarEventLocation: ItemName.GenieJafarEvent, + # LocationName.ASLexaeusEventLocation: ItemName.ASLexaeusEvent, + LocationName.DataLexaeusEventLocation: ItemName.DataLexaeusEvent, + LocationName.ScarEventLocation: ItemName.ScarEvent, + LocationName.GroundShakerEventLocation: ItemName.GroundShakerEvent, + LocationName.DataSaixEventLocation: ItemName.DataSaixEvent, + LocationName.HBDemyxEventLocation: ItemName.HBDemyxEvent, + LocationName.ThousandHeartlessEventLocation: ItemName.ThousandHeartlessEvent, + LocationName.Mushroom13EventLocation: ItemName.Mushroom13Event, + LocationName.SephiEventLocation: ItemName.SephiEvent, + LocationName.DataDemyxEventLocation: ItemName.DataDemyxEvent, + LocationName.CorFirstFightEventLocation: ItemName.CorFirstFightEvent, + LocationName.CorSecondFightEventLocation: ItemName.CorSecondFightEvent, + LocationName.TransportEventLocation: ItemName.TransportEvent, + LocationName.OldPeteEventLocation: ItemName.OldPeteEvent, + LocationName.FuturePeteEventLocation: ItemName.FuturePeteEvent, + # LocationName.ASMarluxiaEventLocation: ItemName.ASMarluxiaEvent, + LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent, + LocationName.TerraEventLocation: ItemName.TerraEvent, + LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent, + LocationName.Axel1EventLocation: ItemName.Axel1Event, + LocationName.Axel2EventLocation: ItemName.Axel2Event, + LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent, +} +all_weapon_slot = { + LocationName.FAKESlot, + LocationName.DetectionSaberSlot, + LocationName.EdgeofUltimaSlot, + LocationName.KingdomKeySlot, + LocationName.OathkeeperSlot, + LocationName.OblivionSlot, + LocationName.StarSeekerSlot, + LocationName.HiddenDragonSlot, + LocationName.HerosCrestSlot, + LocationName.MonochromeSlot, + LocationName.FollowtheWindSlot, + LocationName.CircleofLifeSlot, + LocationName.PhotonDebuggerSlot, + LocationName.GullWingSlot, + LocationName.RumblingRoseSlot, + LocationName.GuardianSoulSlot, + LocationName.WishingLampSlot, + LocationName.DecisivePumpkinSlot, + LocationName.SweetMemoriesSlot, + LocationName.MysteriousAbyssSlot, + LocationName.SleepingLionSlot, + LocationName.BondofFlameSlot, + LocationName.TwoBecomeOneSlot, + LocationName.FatalCrestSlot, + LocationName.FenrirSlot, + LocationName.UltimaWeaponSlot, + LocationName.WinnersProofSlot, + LocationName.PurebloodSlot, + + LocationName.Centurion2, + LocationName.CometStaff, + LocationName.HammerStaff, + LocationName.LordsBroom, + LocationName.MagesStaff, + LocationName.MeteorStaff, + LocationName.NobodyLance, + LocationName.PreciousMushroom, + LocationName.PreciousMushroom2, + LocationName.PremiumMushroom, + LocationName.RisingDragon, + LocationName.SaveTheQueen2, + LocationName.ShamansRelic, + LocationName.VictoryBell, + LocationName.WisdomWand, + + LocationName.AdamantShield, + LocationName.AkashicRecord, + LocationName.ChainGear, + LocationName.DreamCloud, + LocationName.FallingStar, + LocationName.FrozenPride2, + LocationName.GenjiShield, + LocationName.KnightDefender, + LocationName.KnightsShield, + LocationName.MajesticMushroom, + LocationName.MajesticMushroom2, + LocationName.NobodyGuard, + LocationName.OgreShield, + LocationName.SaveTheKing2, + LocationName.UltimateMushroom, } + +all_locations = { + **TWTNW_Checks, + **TT_Checks, + **STT_Checks, + **PL_Checks, + **HB_Checks, + **HT_Checks, + **PR_Checks, + **PR_Checks, + **SP_Checks, + **BC_Checks, + **Oc_Checks, + **HundredAcre_Checks, + **DC_Checks, + **AG_Checks, + **LoD_Checks, + **SoraLevels, + **Form_Checks, + **GoA_Checks, + **Keyblade_Slots, + **Donald_Checks, + **Goofy_Checks, + **Atlantica_Checks, + **Summon_Checks, +} - LocationName.AdamantShield: LocationData(0x1302CA, 100, "Keyblade", "Goofy"), - LocationName.AkashicRecord: LocationData(0x1302CB, 107, "Keyblade", "Goofy"), - LocationName.ChainGear: LocationData(0x1302CC, 101, "Keyblade", "Goofy"), - LocationName.DreamCloud: LocationData(0x1302CD, 104, "Keyblade", "Goofy"), - LocationName.FallingStar: LocationData(0x1302CE, 103, "Keyblade", "Goofy"), - LocationName.FrozenPride2: LocationData(0x1302CF, 158, "Keyblade", "Goofy"), - LocationName.GenjiShield: LocationData(0x1302D0, 106, "Keyblade", "Goofy"), - LocationName.KnightDefender: LocationData(0x1302D1, 105, "Keyblade", "Goofy"), - LocationName.KnightsShield: LocationData(0x1302D2, 99, "Keyblade", "Goofy"), - LocationName.MajesticMushroom: LocationData(0x1302D3, 161, "Keyblade", "Goofy"), - LocationName.MajesticMushroom2: LocationData(0x1302D4, 162, "Keyblade", "Goofy"), - LocationName.NobodyGuard: LocationData(0x1302D5, 108, "Keyblade", "Goofy"), - LocationName.OgreShield: LocationData(0x1302D6, 102, "Keyblade", "Goofy"), - LocationName.SaveTheKing2: LocationData(0x1302D7, 147, "Keyblade", "Goofy"), - LocationName.UltimateMushroom: LocationData(0x1302D8, 163, "Keyblade", "Goofy"), - LocationName.GoofyStarting1: LocationData(0x1302D9, 3, "Critical", "Goofy"), - LocationName.GoofyStarting2: LocationData(0x1302DA, 3, "Critical", "Goofy"), +popups_set = { + LocationName.SweetMemories, + LocationName.SpookyCaveMap, + LocationName.StarryHillCureElement, + LocationName.StarryHillOrichalcumPlus, + LocationName.AgrabahMap, + LocationName.LampCharm, + LocationName.WishingLamp, + LocationName.DarkThornCureElement, + LocationName.RumblingRose, + LocationName.CastleWallsMap, + LocationName.SecretAnsemReport4, + LocationName.DisneyCastleMap, + LocationName.WindowofTimeMap, + LocationName.Monochrome, + LocationName.WisdomForm, + LocationName.LingeringWillProofofConnection, + LocationName.LingeringWillManifestIllusion, + LocationName.OogieBoogieMagnetElement, + LocationName.Present, + LocationName.DecoyPresents, + LocationName.DecisivePumpkin, + LocationName.MarketplaceMap, + LocationName.MerlinsHouseMembershipCard, + LocationName.MerlinsHouseBlizzardElement, + LocationName.BaileySecretAnsemReport7, + LocationName.BaseballCharm, + LocationName.AnsemsStudyMasterForm, + LocationName.AnsemsStudySkillRecipe, + LocationName.AnsemsStudySleepingLion, + LocationName.FFFightsCureElement, + LocationName.ThousandHeartlessSecretAnsemReport1, + LocationName.ThousandHeartlessIceCream, + LocationName.ThousandHeartlessPicture, + LocationName.WinnersProof, + LocationName.ProofofPeace, + LocationName.SephirothFenrir, + LocationName.EncampmentAreaMap, + LocationName.Mission3, + LocationName.VillageCaveAreaMap, + LocationName.HiddenDragon, + LocationName.ColiseumMap, + LocationName.SecretAnsemReport6, + LocationName.OlympusStone, + LocationName.HerosCrest, + LocationName.AuronsStatue, + LocationName.GuardianSoul, + LocationName.ProtectBeltPainandPanicCup, + LocationName.SerenityGemPainandPanicCup, + LocationName.RisingDragonCerberusCup, + LocationName.SerenityCrystalCerberusCup, + LocationName.GenjiShieldTitanCup, + LocationName.SkillfulRingTitanCup, + LocationName.FatalCrestGoddessofFateCup, + LocationName.OrichalcumPlusGoddessofFateCup, + LocationName.HadesCupTrophyParadoxCups, + LocationName.IsladeMuertaMap, + LocationName.FollowtheWind, + LocationName.SeadriftRowCursedMedallion, + LocationName.SeadriftRowShipGraveyardMap, + LocationName.SecretAnsemReport5, + LocationName.CircleofLife, + LocationName.ScarFireElement, + LocationName.TwilightTownMap, + LocationName.MunnyPouchOlette, + LocationName.JunkChampionBelt, + LocationName.JunkMedal, + LocationName.TheStruggleTrophy, + LocationName.NaminesSketches, + LocationName.MansionMap, + LocationName.PhotonDebugger, + LocationName.StationPlazaSecretAnsemReport2, + LocationName.MunnyPouchMickey, + LocationName.CrystalOrb, + LocationName.StarSeeker, + LocationName.ValorForm, + LocationName.SeifersTrophy, + LocationName.Oathkeeper, + LocationName.LimitForm, + LocationName.BeamSecretAnsemReport10, + LocationName.BetwixtandBetweenBondofFlame, + LocationName.TwoBecomeOne, + LocationName.RoxasSecretAnsemReport8, + LocationName.XigbarSecretAnsemReport3, + LocationName.Oblivion, + LocationName.CastleThatNeverWasMap, + LocationName.LuxordSecretAnsemReport9, + LocationName.SaixSecretAnsemReport12, + LocationName.PreXemnas1SecretAnsemReport11, + LocationName.Xemnas1SecretAnsemReport13, + LocationName.XemnasDataPowerBoost, + LocationName.AxelDataMagicBoost, + LocationName.RoxasDataMagicBoost, + LocationName.SaixDataDefenseBoost, + LocationName.DemyxDataAPBoost, + LocationName.LuxordDataAPBoost, + LocationName.VexenDataLostIllusion, + LocationName.LarxeneDataLostIllusion, + LocationName.XaldinDataDefenseBoost, + LocationName.MarluxiaDataLostIllusion, + LocationName.LexaeusDataLostIllusion, + LocationName.XigbarDataDefenseBoost, + LocationName.VexenASRoadtoDiscovery, + LocationName.LarxeneASCloakedThunder, + LocationName.ZexionASBookofShadows, + LocationName.ZexionDataLostIllusion, + LocationName.LexaeusASStrengthBeyondStrength, + LocationName.MarluxiaASEternalBlossom, + LocationName.UnderseaKingdomMap, + LocationName.MysteriousAbyss, + LocationName.MusicalBlizzardElement, + LocationName.MusicalOrichalcumPlus, } exclusion_table = { - "Popups": { - LocationName.SweetMemories, - LocationName.SpookyCaveMap, - LocationName.StarryHillCureElement, - LocationName.StarryHillOrichalcumPlus, - LocationName.AgrabahMap, - LocationName.LampCharm, - LocationName.WishingLamp, - LocationName.DarkThornCureElement, - LocationName.RumblingRose, - LocationName.CastleWallsMap, - LocationName.SecretAnsemReport4, - LocationName.DisneyCastleMap, - LocationName.WindowofTimeMap, - LocationName.Monochrome, - LocationName.WisdomForm, + "SuperBosses": { + LocationName.LingeringWillBonus, LocationName.LingeringWillProofofConnection, LocationName.LingeringWillManifestIllusion, - LocationName.OogieBoogieMagnetElement, - LocationName.Present, - LocationName.DecoyPresents, - LocationName.DecisivePumpkin, - LocationName.MarketplaceMap, - LocationName.MerlinsHouseMembershipCard, - LocationName.MerlinsHouseBlizzardElement, - LocationName.BaileySecretAnsemReport7, - LocationName.BaseballCharm, - LocationName.AnsemsStudyMasterForm, - LocationName.AnsemsStudySkillRecipe, - LocationName.AnsemsStudySleepingLion, - LocationName.FFFightsCureElement, - LocationName.ThousandHeartlessSecretAnsemReport1, - LocationName.ThousandHeartlessIceCream, - LocationName.ThousandHeartlessPicture, - LocationName.WinnersProof, - LocationName.ProofofPeace, + LocationName.SephirothBonus, LocationName.SephirothFenrir, - LocationName.EncampmentAreaMap, - LocationName.Mission3, - LocationName.VillageCaveAreaMap, - LocationName.HiddenDragon, - LocationName.ColiseumMap, - LocationName.SecretAnsemReport6, - LocationName.OlympusStone, - LocationName.HerosCrest, - LocationName.AuronsStatue, - LocationName.GuardianSoul, - LocationName.ProtectBeltPainandPanicCup, - LocationName.SerenityGemPainandPanicCup, - LocationName.RisingDragonCerberusCup, - LocationName.SerenityCrystalCerberusCup, - LocationName.GenjiShieldTitanCup, - LocationName.SkillfulRingTitanCup, - LocationName.FatalCrestGoddessofFateCup, - LocationName.OrichalcumPlusGoddessofFateCup, - LocationName.HadesCupTrophyParadoxCups, - LocationName.IsladeMuertaMap, - LocationName.FollowtheWind, - LocationName.SeadriftRowCursedMedallion, - LocationName.SeadriftRowShipGraveyardMap, - LocationName.SecretAnsemReport5, - LocationName.CircleofLife, - LocationName.ScarFireElement, - LocationName.TwilightTownMap, - LocationName.MunnyPouchOlette, - LocationName.JunkChampionBelt, - LocationName.JunkMedal, - LocationName.TheStruggleTrophy, - LocationName.NaminesSketches, - LocationName.MansionMap, - LocationName.PhotonDebugger, - LocationName.StationPlazaSecretAnsemReport2, - LocationName.MunnyPouchMickey, - LocationName.CrystalOrb, - LocationName.StarSeeker, - LocationName.ValorForm, - LocationName.SeifersTrophy, - LocationName.Oathkeeper, - LocationName.LimitForm, - LocationName.BeamSecretAnsemReport10, - LocationName.BetwixtandBetweenBondofFlame, - LocationName.TwoBecomeOne, - LocationName.RoxasSecretAnsemReport8, - LocationName.XigbarSecretAnsemReport3, - LocationName.Oblivion, - LocationName.CastleThatNeverWasMap, - LocationName.LuxordSecretAnsemReport9, - LocationName.SaixSecretAnsemReport12, - LocationName.PreXemnas1SecretAnsemReport11, - LocationName.Xemnas1SecretAnsemReport13, - LocationName.XemnasDataPowerBoost, - LocationName.AxelDataMagicBoost, - LocationName.RoxasDataMagicBoost, - LocationName.SaixDataDefenseBoost, - LocationName.DemyxDataAPBoost, - LocationName.LuxordDataAPBoost, - LocationName.VexenDataLostIllusion, - LocationName.LarxeneDataLostIllusion, - LocationName.XaldinDataDefenseBoost, - LocationName.MarluxiaDataLostIllusion, - LocationName.LexaeusDataLostIllusion, - LocationName.XigbarDataDefenseBoost, - LocationName.VexenASRoadtoDiscovery, - LocationName.LarxeneASCloakedThunder, - LocationName.ZexionASBookofShadows, - LocationName.ZexionDataLostIllusion, - LocationName.LexaeusASStrengthBeyondStrength, - LocationName.MarluxiaASEternalBlossom - }, - "Datas": { LocationName.XemnasDataPowerBoost, LocationName.AxelDataMagicBoost, LocationName.RoxasDataMagicBoost, @@ -985,13 +1106,7 @@ class LocationData(typing.NamedTuple): LocationName.ZexionDataLostIllusion, LocationName.ZexionBonus, LocationName.ZexionASBookofShadows, - }, - "SuperBosses": { - LocationName.LingeringWillBonus, - LocationName.LingeringWillProofofConnection, - LocationName.LingeringWillManifestIllusion, - LocationName.SephirothBonus, - LocationName.SephirothFenrir, + LocationName.GoofyZexion, }, # 23 checks spread through 50 levels @@ -1148,15 +1263,6 @@ class LocationData(typing.NamedTuple): LocationName.Lvl98, LocationName.Lvl99, }, - "Critical": { - LocationName.Crit_1, - LocationName.Crit_2, - LocationName.Crit_3, - LocationName.Crit_4, - LocationName.Crit_5, - LocationName.Crit_6, - LocationName.Crit_7, - }, "Hitlist": [ LocationName.XemnasDataPowerBoost, LocationName.AxelDataMagicBoost, @@ -1179,9 +1285,11 @@ class LocationData(typing.NamedTuple): LocationName.Limitlvl7, LocationName.Masterlvl7, LocationName.Finallvl7, + LocationName.Summonlvl7, LocationName.TransporttoRemembrance, LocationName.OrichalcumPlusGoddessofFateCup, LocationName.HadesCupTrophyParadoxCups, + LocationName.MusicalOrichalcumPlus, ], "Cups": { LocationName.ProtectBeltPainandPanicCup, @@ -1194,6 +1302,12 @@ class LocationData(typing.NamedTuple): LocationName.OrichalcumPlusGoddessofFateCup, LocationName.HadesCupTrophyParadoxCups, }, + "Atlantica": { + LocationName.MysteriousAbyss, + LocationName.MusicalOrichalcumPlus, + LocationName.MusicalBlizzardElement, + LocationName.UnderseaKingdomMap, + }, "WeaponSlots": { LocationName.FAKESlot: ItemName.ValorForm, LocationName.DetectionSaberSlot: ItemName.MasterForm, @@ -1244,536 +1358,6 @@ class LocationData(typing.NamedTuple): LocationName.Centurion2: ItemName.Centurion2, }, "Chests": { - LocationName.BambooGroveDarkShard, - LocationName.BambooGroveEther, - LocationName.BambooGroveMythrilShard, - LocationName.CheckpointHiPotion, - LocationName.CheckpointMythrilShard, - LocationName.MountainTrailLightningShard, - LocationName.MountainTrailRecoveryRecipe, - LocationName.MountainTrailEther, - LocationName.MountainTrailMythrilShard, - LocationName.VillageCaveAPBoost, - LocationName.VillageCaveDarkShard, - LocationName.RidgeFrostShard, - LocationName.RidgeAPBoost, - LocationName.ThroneRoomTornPages, - LocationName.ThroneRoomPalaceMap, - LocationName.ThroneRoomAPBoost, - LocationName.ThroneRoomQueenRecipe, - LocationName.ThroneRoomAPBoost2, - LocationName.ThroneRoomOgreShield, - LocationName.ThroneRoomMythrilCrystal, - LocationName.ThroneRoomOrichalcum, - LocationName.AgrabahDarkShard, - LocationName.AgrabahMythrilShard, - LocationName.AgrabahHiPotion, - LocationName.AgrabahAPBoost, - LocationName.AgrabahMythrilStone, - LocationName.AgrabahMythrilShard2, - LocationName.AgrabahSerenityShard, - LocationName.BazaarMythrilGem, - LocationName.BazaarPowerShard, - LocationName.BazaarHiPotion, - LocationName.BazaarAPBoost, - LocationName.BazaarMythrilShard, - LocationName.PalaceWallsSkillRing, - LocationName.PalaceWallsMythrilStone, - LocationName.CaveEntrancePowerStone, - LocationName.CaveEntranceMythrilShard, - LocationName.ValleyofStoneMythrilStone, - LocationName.ValleyofStoneAPBoost, - LocationName.ValleyofStoneMythrilShard, - LocationName.ValleyofStoneHiPotion, - LocationName.ChasmofChallengesCaveofWondersMap, - LocationName.ChasmofChallengesAPBoost, - LocationName.TreasureRoomAPBoost, - LocationName.TreasureRoomSerenityGem, - LocationName.RuinedChamberTornPages, - LocationName.RuinedChamberRuinsMap, - LocationName.DCCourtyardMythrilShard, - LocationName.DCCourtyardStarRecipe, - LocationName.DCCourtyardAPBoost, - LocationName.DCCourtyardMythrilStone, - LocationName.DCCourtyardBlazingStone, - LocationName.DCCourtyardBlazingShard, - LocationName.DCCourtyardMythrilShard2, - LocationName.LibraryTornPages, - LocationName.CornerstoneHillMap, - LocationName.CornerstoneHillFrostShard, - LocationName.PierMythrilShard, - LocationName.PierHiPotion, - LocationName.WaterwayMythrilStone, - LocationName.WaterwayAPBoost, - LocationName.WaterwayFrostStone, - LocationName.PoohsHouse100AcreWoodMap, - LocationName.PoohsHouseAPBoost, - LocationName.PoohsHouseMythrilStone, - LocationName.PigletsHouseDefenseBoost, - LocationName.PigletsHouseAPBoost, - LocationName.PigletsHouseMythrilGem, - LocationName.RabbitsHouseDrawRing, - LocationName.RabbitsHouseMythrilCrystal, - LocationName.RabbitsHouseAPBoost, - LocationName.KangasHouseMagicBoost, - LocationName.KangasHouseAPBoost, - LocationName.KangasHouseOrichalcum, - LocationName.SpookyCaveMythrilGem, - LocationName.SpookyCaveAPBoost, - LocationName.SpookyCaveOrichalcum, - LocationName.SpookyCaveGuardRecipe, - LocationName.SpookyCaveMythrilCrystal, - LocationName.SpookyCaveAPBoost2, - LocationName.StarryHillCosmicRing, - LocationName.StarryHillStyleRecipe, - LocationName.RampartNavalMap, - LocationName.RampartMythrilStone, - LocationName.RampartDarkShard, - LocationName.TownDarkStone, - LocationName.TownAPBoost, - LocationName.TownMythrilShard, - LocationName.TownMythrilGem, - LocationName.CaveMouthBrightShard, - LocationName.CaveMouthMythrilShard, - LocationName.PowderStoreAPBoost1, - LocationName.PowderStoreAPBoost2, - LocationName.MoonlightNookMythrilShard, - LocationName.MoonlightNookSerenityGem, - LocationName.MoonlightNookPowerStone, - LocationName.InterceptorsHoldFeatherCharm, - LocationName.SeadriftKeepAPBoost, - LocationName.SeadriftKeepOrichalcum, - LocationName.SeadriftKeepMeteorStaff, - LocationName.SeadriftRowSerenityGem, - LocationName.SeadriftRowKingRecipe, - LocationName.SeadriftRowMythrilCrystal, - LocationName.PassageMythrilShard, - LocationName.PassageMythrilStone, - LocationName.PassageEther, - LocationName.PassageAPBoost, - LocationName.PassageHiPotion, - LocationName.InnerChamberUnderworldMap, - LocationName.InnerChamberMythrilShard, - LocationName.UnderworldEntrancePowerBoost, - LocationName.CavernsEntranceLucidShard, - LocationName.CavernsEntranceAPBoost, - LocationName.CavernsEntranceMythrilShard, - LocationName.TheLostRoadBrightShard, - LocationName.TheLostRoadEther, - LocationName.TheLostRoadMythrilShard, - LocationName.TheLostRoadMythrilStone, - LocationName.AtriumLucidStone, - LocationName.AtriumAPBoost, - LocationName.TheLockCavernsMap, - LocationName.TheLockMythrilShard, - LocationName.TheLockAPBoost, - LocationName.BCCourtyardAPBoost, - LocationName.BCCourtyardHiPotion, - LocationName.BCCourtyardMythrilShard, - LocationName.BellesRoomCastleMap, - LocationName.BellesRoomMegaRecipe, - LocationName.TheEastWingMythrilShard, - LocationName.TheEastWingTent, - LocationName.TheWestHallHiPotion, - LocationName.TheWestHallPowerShard, - LocationName.TheWestHallMythrilShard2, - LocationName.TheWestHallBrightStone, - LocationName.TheWestHallMythrilShard, - LocationName.DungeonBasementMap, - LocationName.DungeonAPBoost, - LocationName.SecretPassageMythrilShard, - LocationName.SecretPassageHiPotion, - LocationName.SecretPassageLucidShard, - LocationName.TheWestHallAPBoostPostDungeon, - LocationName.TheWestWingMythrilShard, - LocationName.TheWestWingTent, - LocationName.TheBeastsRoomBlazingShard, - LocationName.PitCellAreaMap, - LocationName.PitCellMythrilCrystal, - LocationName.CanyonDarkCrystal, - LocationName.CanyonMythrilStone, - LocationName.CanyonMythrilGem, - LocationName.CanyonFrostCrystal, - LocationName.HallwayPowerCrystal, - LocationName.HallwayAPBoost, - LocationName.CommunicationsRoomIOTowerMap, - LocationName.CommunicationsRoomGaiaBelt, - LocationName.CentralComputerCoreAPBoost, - LocationName.CentralComputerCoreOrichalcumPlus, - LocationName.CentralComputerCoreCosmicArts, - LocationName.CentralComputerCoreMap, - LocationName.GraveyardMythrilShard, - LocationName.GraveyardSerenityGem, - LocationName.FinklesteinsLabHalloweenTownMap, - LocationName.TownSquareMythrilStone, - LocationName.TownSquareEnergyShard, - LocationName.HinterlandsLightningShard, - LocationName.HinterlandsMythrilStone, - LocationName.HinterlandsAPBoost, - LocationName.CandyCaneLaneMegaPotion, - LocationName.CandyCaneLaneMythrilGem, - LocationName.CandyCaneLaneLightningStone, - LocationName.CandyCaneLaneMythrilStone, - LocationName.SantasHouseChristmasTownMap, - LocationName.SantasHouseAPBoost, - LocationName.BoroughDriveRecovery, - LocationName.BoroughAPBoost, - LocationName.BoroughHiPotion, - LocationName.BoroughMythrilShard, - LocationName.BoroughDarkShard, - LocationName.PosternCastlePerimeterMap, - LocationName.PosternMythrilGem, - LocationName.PosternAPBoost, - LocationName.CorridorsMythrilStone, - LocationName.CorridorsMythrilCrystal, - LocationName.CorridorsDarkCrystal, - LocationName.CorridorsAPBoost, - LocationName.AnsemsStudyUkuleleCharm, - LocationName.RestorationSiteMoonRecipe, - LocationName.RestorationSiteAPBoost, - LocationName.CoRDepthsAPBoost, - LocationName.CoRDepthsPowerCrystal, - LocationName.CoRDepthsFrostCrystal, - LocationName.CoRDepthsManifestIllusion, - LocationName.CoRDepthsAPBoost2, - LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap, - LocationName.CoRMineshaftLowerLevelAPBoost, - LocationName.CrystalFissureTornPages, - LocationName.CrystalFissureTheGreatMawMap, - LocationName.CrystalFissureEnergyCrystal, - LocationName.CrystalFissureAPBoost, - LocationName.PosternGullWing, - LocationName.HeartlessManufactoryCosmicChain, - LocationName.CoRDepthsUpperLevelRemembranceGem, - LocationName.CoRMiningAreaSerenityGem, - LocationName.CoRMiningAreaAPBoost, - LocationName.CoRMiningAreaSerenityCrystal, - LocationName.CoRMiningAreaManifestIllusion, - LocationName.CoRMiningAreaSerenityGem2, - LocationName.CoRMiningAreaDarkRemembranceMap, - LocationName.CoRMineshaftMidLevelPowerBoost, - LocationName.CoREngineChamberSerenityCrystal, - LocationName.CoREngineChamberRemembranceCrystal, - LocationName.CoREngineChamberAPBoost, - LocationName.CoREngineChamberManifestIllusion, - LocationName.CoRMineshaftUpperLevelMagicBoost, - LocationName.CoRMineshaftUpperLevelAPBoost, - LocationName.GorgeSavannahMap, - LocationName.GorgeDarkGem, - LocationName.GorgeMythrilStone, - LocationName.ElephantGraveyardFrostGem, - LocationName.ElephantGraveyardMythrilStone, - LocationName.ElephantGraveyardBrightStone, - LocationName.ElephantGraveyardAPBoost, - LocationName.ElephantGraveyardMythrilShard, - LocationName.PrideRockMap, - LocationName.PrideRockMythrilStone, - LocationName.PrideRockSerenityCrystal, - LocationName.WildebeestValleyEnergyStone, - LocationName.WildebeestValleyAPBoost, - LocationName.WildebeestValleyMythrilGem, - LocationName.WildebeestValleyMythrilStone, - LocationName.WildebeestValleyLucidGem, - LocationName.WastelandsMythrilShard, - LocationName.WastelandsSerenityGem, - LocationName.WastelandsMythrilStone, - LocationName.JungleSerenityGem, - LocationName.JungleMythrilStone, - LocationName.JungleSerenityCrystal, - LocationName.OasisMap, - LocationName.OasisTornPages, - LocationName.OasisAPBoost, - LocationName.StationofCallingPotion, - LocationName.CentralStationPotion1, - LocationName.STTCentralStationHiPotion, - LocationName.CentralStationPotion2, - LocationName.SunsetTerraceAbilityRing, - LocationName.SunsetTerraceHiPotion, - LocationName.SunsetTerracePotion1, - LocationName.SunsetTerracePotion2, - LocationName.MansionFoyerHiPotion, - LocationName.MansionFoyerPotion1, - LocationName.MansionFoyerPotion2, - LocationName.MansionDiningRoomElvenBandanna, - LocationName.MansionDiningRoomPotion, - LocationName.MansionLibraryHiPotion, - LocationName.MansionBasementCorridorHiPotion, - LocationName.OldMansionPotion, - LocationName.OldMansionMythrilShard, - LocationName.TheWoodsPotion, - LocationName.TheWoodsMythrilShard, - LocationName.TheWoodsHiPotion, - LocationName.TramCommonHiPotion, - LocationName.TramCommonAPBoost, - LocationName.TramCommonTent, - LocationName.TramCommonMythrilShard1, - LocationName.TramCommonPotion1, - LocationName.TramCommonMythrilShard2, - LocationName.TramCommonPotion2, - LocationName.CentralStationTent, - LocationName.TTCentralStationHiPotion, - LocationName.CentralStationMythrilShard, - LocationName.TheTowerPotion, - LocationName.TheTowerHiPotion, - LocationName.TheTowerEther, - LocationName.TowerEntrywayEther, - LocationName.TowerEntrywayMythrilShard, - LocationName.SorcerersLoftTowerMap, - LocationName.TowerWardrobeMythrilStone, - LocationName.UndergroundConcourseMythrilGem, - LocationName.UndergroundConcourseAPBoost, - LocationName.UndergroundConcourseMythrilCrystal, - LocationName.UndergroundConcourseOrichalcum, - LocationName.TunnelwayOrichalcum, - LocationName.TunnelwayMythrilCrystal, - LocationName.SunsetTerraceOrichalcumPlus, - LocationName.SunsetTerraceMythrilShard, - LocationName.SunsetTerraceMythrilCrystal, - LocationName.SunsetTerraceAPBoost, - LocationName.MansionFoyerMythrilCrystal, - LocationName.MansionFoyerMythrilStone, - LocationName.MansionFoyerSerenityCrystal, - LocationName.MansionDiningRoomMythrilCrystal, - LocationName.MansionDiningRoomMythrilStone, - LocationName.MansionLibraryOrichalcum, - LocationName.MansionBasementCorridorUltimateRecipe, - LocationName.FragmentCrossingMythrilStone, - LocationName.FragmentCrossingMythrilCrystal, - LocationName.FragmentCrossingAPBoost, - LocationName.FragmentCrossingOrichalcum, - LocationName.MemorysSkyscaperMythrilCrystal, - LocationName.MemorysSkyscaperAPBoost, - LocationName.MemorysSkyscaperMythrilStone, - LocationName.TheBrinkofDespairDarkCityMap, - LocationName.TheBrinkofDespairOrichalcumPlus, - LocationName.NothingsCallMythrilGem, - LocationName.NothingsCallOrichalcum, - LocationName.TwilightsViewCosmicBelt, - LocationName.NaughtsSkywayMythrilGem, - LocationName.NaughtsSkywayOrichalcum, - LocationName.NaughtsSkywayMythrilCrystal, - LocationName.RuinandCreationsPassageMythrilStone, - LocationName.RuinandCreationsPassageAPBoost, - LocationName.RuinandCreationsPassageMythrilCrystal, - LocationName.RuinandCreationsPassageOrichalcum, - LocationName.GardenofAssemblageMap, - LocationName.GoALostIllusion, - LocationName.ProofofNonexistence, + location for location, data in all_locations.items() if location not in event_location_to_item.keys() and location not in popups_set and location != LocationName.StationofSerenityPotion and data.yml == "Chest" } } - -AllWeaponSlot = { - LocationName.FAKESlot, - LocationName.DetectionSaberSlot, - LocationName.EdgeofUltimaSlot, - LocationName.KingdomKeySlot, - LocationName.OathkeeperSlot, - LocationName.OblivionSlot, - LocationName.StarSeekerSlot, - LocationName.HiddenDragonSlot, - LocationName.HerosCrestSlot, - LocationName.MonochromeSlot, - LocationName.FollowtheWindSlot, - LocationName.CircleofLifeSlot, - LocationName.PhotonDebuggerSlot, - LocationName.GullWingSlot, - LocationName.RumblingRoseSlot, - LocationName.GuardianSoulSlot, - LocationName.WishingLampSlot, - LocationName.DecisivePumpkinSlot, - LocationName.SweetMemoriesSlot, - LocationName.MysteriousAbyssSlot, - LocationName.SleepingLionSlot, - LocationName.BondofFlameSlot, - LocationName.TwoBecomeOneSlot, - LocationName.FatalCrestSlot, - LocationName.FenrirSlot, - LocationName.UltimaWeaponSlot, - LocationName.WinnersProofSlot, - LocationName.PurebloodSlot, - LocationName.Centurion2, - LocationName.CometStaff, - LocationName.HammerStaff, - LocationName.LordsBroom, - LocationName.MagesStaff, - LocationName.MeteorStaff, - LocationName.NobodyLance, - LocationName.PreciousMushroom, - LocationName.PreciousMushroom2, - LocationName.PremiumMushroom, - LocationName.RisingDragon, - LocationName.SaveTheQueen2, - LocationName.ShamansRelic, - LocationName.VictoryBell, - LocationName.WisdomWand, - - LocationName.AdamantShield, - LocationName.AkashicRecord, - LocationName.ChainGear, - LocationName.DreamCloud, - LocationName.FallingStar, - LocationName.FrozenPride2, - LocationName.GenjiShield, - LocationName.KnightDefender, - LocationName.KnightsShield, - LocationName.MajesticMushroom, - LocationName.MajesticMushroom2, - LocationName.NobodyGuard, - LocationName.OgreShield, - LocationName.SaveTheKing2, - LocationName.UltimateMushroom, } -RegionTable = { - "FirstVisits": { - RegionName.LoD_Region, - RegionName.Ag_Region, - RegionName.Dc_Region, - RegionName.Pr_Region, - RegionName.Oc_Region, - RegionName.Bc_Region, - RegionName.Sp_Region, - RegionName.Ht_Region, - RegionName.Hb_Region, - RegionName.Pl_Region, - RegionName.STT_Region, - RegionName.TT_Region, - RegionName.Twtnw_Region, - }, - "SecondVisits": { - RegionName.LoD2_Region, - RegionName.Ag2_Region, - RegionName.Tr_Region, - RegionName.Pr2_Region, - RegionName.Oc2_Region, - RegionName.Bc2_Region, - RegionName.Sp2_Region, - RegionName.Ht2_Region, - RegionName.Hb2_Region, - RegionName.Pl2_Region, - RegionName.STT_Region, - RegionName.Twtnw2_Region, - }, - "ValorRegion": { - RegionName.LoD_Region, - RegionName.Ag_Region, - RegionName.Dc_Region, - RegionName.Pr_Region, - RegionName.Oc_Region, - RegionName.Bc_Region, - RegionName.Sp_Region, - RegionName.Ht_Region, - RegionName.Hb_Region, - RegionName.TT_Region, - RegionName.Twtnw_Region, - }, - "WisdomRegion": { - RegionName.LoD_Region, - RegionName.Ag_Region, - RegionName.Dc_Region, - RegionName.Pr_Region, - RegionName.Oc_Region, - RegionName.Bc_Region, - RegionName.Sp_Region, - RegionName.Ht_Region, - RegionName.Hb_Region, - RegionName.TT_Region, - RegionName.Twtnw_Region, - }, - "LimitRegion": { - RegionName.LoD_Region, - RegionName.Ag_Region, - RegionName.Dc_Region, - RegionName.Pr_Region, - RegionName.Oc_Region, - RegionName.Bc_Region, - RegionName.Sp_Region, - RegionName.Ht_Region, - RegionName.Hb_Region, - RegionName.TT_Region, - RegionName.Twtnw_Region, - RegionName.STT_Region, - }, - "MasterRegion": { - RegionName.LoD_Region, - RegionName.Ag_Region, - RegionName.Dc_Region, - RegionName.Pr_Region, - RegionName.Oc_Region, - RegionName.Bc_Region, - RegionName.Sp_Region, - RegionName.Ht_Region, - RegionName.Hb_Region, - RegionName.TT_Region, - RegionName.Twtnw_Region, - }, # could add lod2 and bc2 as an option since those spawns are rng - "FinalRegion": { - RegionName.TT3_Region, - RegionName.Twtnw_PostRoxas, - RegionName.Twtnw2_Region, - } -} - -all_locations = { - **TWTNW_Checks, - **TWTNW2_Checks, - **TT_Checks, - **TT2_Checks, - **TT3_Checks, - **STT_Checks, - **PL_Checks, - **PL2_Checks, - **CoR_Checks, - **HB_Checks, - **HB2_Checks, - **HT_Checks, - **HT2_Checks, - **PR_Checks, - **PR2_Checks, - **PR_Checks, - **PR2_Checks, - **SP_Checks, - **SP2_Checks, - **BC_Checks, - **BC2_Checks, - **Oc_Checks, - **Oc2_Checks, - **Oc2Cups, - **HundredAcre1_Checks, - **HundredAcre2_Checks, - **HundredAcre3_Checks, - **HundredAcre4_Checks, - **HundredAcre5_Checks, - **HundredAcre6_Checks, - **DC_Checks, - **TR_Checks, - **AG_Checks, - **AG2_Checks, - **LoD_Checks, - **LoD2_Checks, - **SoraLevels, - **Form_Checks, - **GoA_Checks, - **Keyblade_Slots, - **Critical_Checks, - **Donald_Checks, - **Goofy_Checks, -} - -location_table = {} - - -def setup_locations(): - totallocation_table = {**TWTNW_Checks, **TWTNW2_Checks, **TT_Checks, **TT2_Checks, **TT3_Checks, **STT_Checks, - **PL_Checks, **PL2_Checks, **CoR_Checks, **HB_Checks, **HB2_Checks, - **PR_Checks, **PR2_Checks, **PR_Checks, **PR2_Checks, **SP_Checks, **SP2_Checks, **BC_Checks, - **BC2_Checks, **HT_Checks, **HT2_Checks, - **Oc_Checks, **Oc2_Checks, **Oc2Cups, **Critical_Checks, **Donald_Checks, **Goofy_Checks, - **HundredAcre1_Checks, **HundredAcre2_Checks, **HundredAcre3_Checks, **HundredAcre4_Checks, - **HundredAcre5_Checks, **HundredAcre6_Checks, - **DC_Checks, **TR_Checks, **AG_Checks, **AG2_Checks, **LoD_Checks, **LoD2_Checks, - **SoraLevels, - **Form_Checks, **GoA_Checks, **Keyblade_Slots} - return totallocation_table - - -lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in location_table.items() if - data.code} diff --git a/worlds/kh2/Logic.py b/worlds/kh2/Logic.py new file mode 100644 index 000000000000..1f13aa5f029c --- /dev/null +++ b/worlds/kh2/Logic.py @@ -0,0 +1,642 @@ +from .Names import ItemName, RegionName, LocationName + +# this file contains the dicts,lists and sets used for making rules in rules.py +base_tools = [ + ItemName.FinishingPlus, + ItemName.Guard, + ItemName.AerialRecovery +] +gap_closer = [ + ItemName.SlideDash, + ItemName.FlashStep +] +defensive_tool = [ + ItemName.ReflectElement, + ItemName.Guard +] +form_list = [ + ItemName.ValorForm, + ItemName.WisdomForm, + ItemName.LimitForm, + ItemName.MasterForm, + ItemName.FinalForm +] +form_list_without_final = [ + ItemName.ValorForm, + ItemName.WisdomForm, + ItemName.LimitForm, + ItemName.MasterForm +] +ground_finisher = [ + ItemName.GuardBreak, + ItemName.Explosion, + ItemName.FinishingLeap +] +party_limit = [ + ItemName.Fantasia, + ItemName.FlareForce, + ItemName.Teamwork, + ItemName.TornadoFusion +] +donald_limit = [ + ItemName.Fantasia, + ItemName.FlareForce +] +aerial_move = [ + ItemName.AerialDive, + ItemName.AerialSpiral, + ItemName.HorizontalSlash, + ItemName.AerialSweep, + ItemName.AerialFinish +] +level_3_form_loc = [ + LocationName.Valorlvl3, + LocationName.Wisdomlvl3, + LocationName.Limitlvl3, + LocationName.Masterlvl3, + LocationName.Finallvl3 +] +black_magic = [ + ItemName.FireElement, + ItemName.BlizzardElement, + ItemName.ThunderElement +] +magic = [ + ItemName.FireElement, + ItemName.BlizzardElement, + ItemName.ThunderElement, + ItemName.ReflectElement, + ItemName.CureElement, + ItemName.MagnetElement +] +summons = [ + ItemName.ChickenLittle, + ItemName.Stitch, + ItemName.Genie, + ItemName.PeterPan +] +three_proofs = [ + ItemName.ProofofConnection, + ItemName.ProofofPeace, + ItemName.ProofofNonexistence +] + +auto_form_dict = { + ItemName.FinalForm: ItemName.AutoFinal, + ItemName.MasterForm: ItemName.AutoMaster, + ItemName.LimitForm: ItemName.AutoLimit, + ItemName.WisdomForm: ItemName.AutoWisdom, + ItemName.ValorForm: ItemName.AutoValor, +} + +# could use comprehension for getting a list of the region objects but eh I like this more +drive_form_list = [RegionName.Valor, RegionName.Wisdom, RegionName.Limit, RegionName.Master, RegionName.Final, RegionName.Summon] + +easy_data_xigbar_tools = { + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.AerialDive: 1, + ItemName.HorizontalSlash: 1, + ItemName.AirComboPlus: 2, + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, +} +normal_data_xigbar_tools = { + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.HorizontalSlash: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, +} + +easy_data_lex_tools = { + ItemName.Guard: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1 +} +normal_data_lex_tools = { + ItemName.Guard: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 1, +} + +easy_data_marluxia_tools = { + ItemName.Guard: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.AerialRecovery: 1, +} +normal_data_marluxia_tools = { + ItemName.Guard: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 1, + ItemName.AerialRecovery: 1, +} +easy_terra_tools = { + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.Explosion: 1, + ItemName.ComboPlus: 2, + ItemName.FireElement: 3, + ItemName.Fantasia: 1, + ItemName.FlareForce: 1, + ItemName.ReflectElement: 1, + ItemName.Guard: 1, + ItemName.DodgeRoll: 3, + ItemName.AerialDodge: 3, + ItemName.Glide: 3 +} +normal_terra_tools = { + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.Explosion: 1, + ItemName.ComboPlus: 2, + ItemName.Guard: 1, + ItemName.DodgeRoll: 2, + ItemName.AerialDodge: 2, + ItemName.Glide: 2 +} +hard_terra_tools = { + ItemName.Explosion: 1, + ItemName.ComboPlus: 2, + ItemName.DodgeRoll: 2, + ItemName.AerialDodge: 2, + ItemName.Glide: 2, + ItemName.Guard: 1 +} +easy_data_luxord_tools = { + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.AerialDodge: 2, + ItemName.Glide: 2, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, +} +easy_data_zexion = { + ItemName.FireElement: 3, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.Fantasia: 1, + ItemName.FlareForce: 1, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.QuickRun: 3, +} +normal_data_zexion = { + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, + ItemName.QuickRun: 3 +} +hard_data_zexion = { + ItemName.FireElement: 2, + ItemName.ReflectElement: 1, + ItemName.QuickRun: 2, +} +easy_data_xaldin = { + ItemName.FireElement: 3, + ItemName.AirComboPlus: 2, + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.ReflectElement: 3, + ItemName.FlareForce: 1, + ItemName.Fantasia: 1, + ItemName.HighJump: 3, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, + ItemName.MagnetElement: 1, + ItemName.HorizontalSlash: 1, + ItemName.AerialDive: 1, + ItemName.AerialSpiral: 1, + ItemName.BerserkCharge: 1 +} +normal_data_xaldin = { + ItemName.FireElement: 3, + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.ReflectElement: 3, + ItemName.FlareForce: 1, + ItemName.Fantasia: 1, + ItemName.HighJump: 3, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, + ItemName.MagnetElement: 1, + ItemName.HorizontalSlash: 1, + ItemName.AerialDive: 1, + ItemName.AerialSpiral: 1, +} +hard_data_xaldin = { + ItemName.FireElement: 2, + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.HighJump: 2, + ItemName.AerialDodge: 2, + ItemName.Glide: 2, + ItemName.MagnetElement: 1, + ItemName.AerialDive: 1 +} +easy_data_larxene = { + ItemName.FireElement: 3, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.Fantasia: 1, + ItemName.FlareForce: 1, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1 +} +normal_data_larxene = { + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, +} +hard_data_larxene = { + ItemName.FireElement: 2, + ItemName.ReflectElement: 1, + ItemName.Guard: 1, + ItemName.AerialDodge: 2, + ItemName.Glide: 2, +} +easy_data_vexen = { + ItemName.FireElement: 3, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.Fantasia: 1, + ItemName.FlareForce: 1, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.QuickRun: 3, +} +normal_data_vexen = { + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, + ItemName.DodgeRoll: 3, + ItemName.QuickRun: 3, +} +hard_data_vexen = { + ItemName.FireElement: 2, + ItemName.ReflectElement: 1, + ItemName.Guard: 1, + ItemName.AerialDodge: 2, + ItemName.Glide: 2, + ItemName.DodgeRoll: 3, + ItemName.QuickRun: 3, +} +easy_thousand_heartless_rules = { + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.Guard: 1, + ItemName.MagnetElement: 2, +} +normal_thousand_heartless_rules = { + ItemName.LimitForm: 1, + ItemName.Guard: 1, +} +easy_data_demyx = { + ItemName.FormBoost: 1, + ItemName.ReflectElement: 2, + ItemName.FireElement: 3, + ItemName.FlareForce: 1, + ItemName.Guard: 1, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.FinishingPlus: 1, +} +normal_data_demyx = { + ItemName.ReflectElement: 2, + ItemName.FireElement: 3, + ItemName.FlareForce: 1, + ItemName.Guard: 1, + ItemName.FinishingPlus: 1, +} +hard_data_demyx = { + ItemName.ReflectElement: 1, + ItemName.FireElement: 2, + ItemName.FlareForce: 1, + ItemName.Guard: 1, + ItemName.FinishingPlus: 1, +} +easy_sephiroth_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 3, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.FinishingPlus: 1, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, +} +normal_sephiroth_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.FinishingPlus: 1, +} +hard_sephiroth_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 1, + ItemName.DodgeRoll: 2, + ItemName.FinishingPlus: 1, +} + +not_hard_cor_tools_dict = { + ItemName.ReflectElement: 3, + ItemName.Stitch: 1, + ItemName.ChickenLittle: 1, + ItemName.MagnetElement: 2, + ItemName.Explosion: 1, + ItemName.FinishingLeap: 1, + ItemName.ThunderElement: 2, +} +transport_tools_dict = { + ItemName.ReflectElement: 3, + ItemName.Stitch: 1, + ItemName.ChickenLittle: 1, + ItemName.MagnetElement: 2, + ItemName.Explosion: 1, + ItemName.FinishingLeap: 1, + ItemName.ThunderElement: 3, + ItemName.Fantasia: 1, + ItemName.FlareForce: 1, + ItemName.Genie: 1, +} +easy_data_saix = { + ItemName.Guard: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.ThunderElement: 1, + ItemName.BlizzardElement: 1, + ItemName.FlareForce: 1, + ItemName.Fantasia: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1 +} +normal_data_saix = { + ItemName.Guard: 1, + ItemName.ThunderElement: 1, + ItemName.BlizzardElement: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, +} +hard_data_saix = { + ItemName.Guard: 1, + ItemName.BlizzardElement: 1, + ItemName.ReflectElement: 1, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, +} +easy_data_roxas_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 3, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.FinishingPlus: 1, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, +} +normal_data_roxas_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.FinishingPlus: 1, +} +hard_data_roxas_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 1, + ItemName.DodgeRoll: 2, + ItemName.FinishingPlus: 1, +} +easy_data_axel_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 3, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.FinishingPlus: 1, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.BlizzardElement: 3, +} +normal_data_axel_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.FinishingPlus: 1, + ItemName.BlizzardElement: 3, +} +hard_data_axel_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 1, + ItemName.DodgeRoll: 2, + ItemName.FinishingPlus: 1, + ItemName.BlizzardElement: 2, +} +easy_roxas_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.LimitForm: 1, + ItemName.ThunderElement: 1, + ItemName.ReflectElement: 2, + ItemName.GuardBreak: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.FinishingPlus: 1, + ItemName.BlizzardElement: 1 +} +normal_roxas_tools = { + ItemName.ThunderElement: 1, + ItemName.ReflectElement: 2, + ItemName.GuardBreak: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.FinishingPlus: 1, + ItemName.BlizzardElement: 1 +} +easy_xigbar_tools = { + ItemName.HorizontalSlash: 1, + ItemName.FireElement: 2, + ItemName.FinishingPlus: 1, + ItemName.Glide: 2, + ItemName.AerialDodge: 2, + ItemName.QuickRun: 2, + ItemName.ReflectElement: 1, + ItemName.Guard: 1, +} +normal_xigbar_tools = { + ItemName.FireElement: 2, + ItemName.FinishingPlus: 1, + ItemName.Glide: 2, + ItemName.AerialDodge: 2, + ItemName.QuickRun: 2, + ItemName.ReflectElement: 1, + ItemName.Guard: 1 +} +easy_luxord_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.QuickRun: 2, + ItemName.Guard: 1, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.LimitForm: 1, +} +normal_luxord_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.QuickRun: 2, + ItemName.Guard: 1, + ItemName.ReflectElement: 2, +} +easy_saix_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.QuickRun: 2, + ItemName.Guard: 1, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.LimitForm: 1, +} +normal_saix_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.QuickRun: 2, + ItemName.Guard: 1, + ItemName.ReflectElement: 2, +} +easy_xemnas_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.QuickRun: 2, + ItemName.Guard: 1, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.LimitForm: 1, +} +normal_xemnas_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.QuickRun: 2, + ItemName.Guard: 1, + ItemName.ReflectElement: 2, +} +easy_data_xemnas = { + ItemName.ComboMaster: 1, + ItemName.Slapshot: 1, + ItemName.ReflectElement: 3, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.TrinityLimit: 1, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.LimitForm: 1, +} +normal_data_xemnas = { + ItemName.ComboMaster: 1, + ItemName.Slapshot: 1, + ItemName.ReflectElement: 3, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.LimitForm: 1, +} +hard_data_xemnas = { + ItemName.ComboMaster: 1, + ItemName.Slapshot: 1, + ItemName.ReflectElement: 2, + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.LimitForm: 1, +} +final_leveling_access = { + LocationName.MemorysSkyscaperMythrilCrystal, + LocationName.GrimReaper2, + LocationName.Xaldin, + LocationName.StormRider, + LocationName.SunsetTerraceAbilityRing +} + +multi_form_region_access = { + ItemName.CastleKey, + ItemName.BattlefieldsofWar, + ItemName.SwordoftheAncestor, + ItemName.BeastsClaw, + ItemName.BoneFist, + ItemName.SkillandCrossbones, + ItemName.Scimitar, + ItemName.MembershipCard, + ItemName.IceCream, + ItemName.WaytotheDawn, + ItemName.IdentityDisk, +} +limit_form_region_access = { + ItemName.CastleKey, + ItemName.BattlefieldsofWar, + ItemName.SwordoftheAncestor, + ItemName.BeastsClaw, + ItemName.BoneFist, + ItemName.SkillandCrossbones, + ItemName.Scimitar, + ItemName.MembershipCard, + ItemName.IceCream, + ItemName.WaytotheDawn, + ItemName.IdentityDisk, + ItemName.NamineSketches +} diff --git a/worlds/kh2/Names/ItemName.py b/worlds/kh2/Names/ItemName.py index 57cfcbe0606f..d7dbdb0ad30a 100644 --- a/worlds/kh2/Names/ItemName.py +++ b/worlds/kh2/Names/ItemName.py @@ -12,8 +12,7 @@ SecretAnsemsReport11 = "Secret Ansem's Report 11" SecretAnsemsReport12 = "Secret Ansem's Report 12" SecretAnsemsReport13 = "Secret Ansem's Report 13" - -# progression +# proofs, visit unlocks and forms ProofofConnection = "Proof of Connection" ProofofNonexistence = "Proof of Nonexistence" ProofofPeace = "Proof of Peace" @@ -32,51 +31,33 @@ NamineSketches = "Namine Sketches" CastleKey = "Disney Castle Key" TornPages = "Torn Page" -TornPages = "Torn Page" -TornPages = "Torn Page" -TornPages = "Torn Page" -TornPages = "Torn Page" ValorForm = "Valor Form" WisdomForm = "Wisdom Form" LimitForm = "Limit Form" MasterForm = "Master Form" FinalForm = "Final Form" - +AntiForm = "Anti Form" # magic and summons -FireElement = "Fire Element" - -BlizzardElement = "Blizzard Element" - -ThunderElement = "Thunder Element" - -CureElement = "Cure Element" - -MagnetElement = "Magnet Element" - -ReflectElement = "Reflect Element" +FireElement = "Fire Element" +BlizzardElement = "Blizzard Element" +ThunderElement = "Thunder Element" +CureElement = "Cure Element" +MagnetElement = "Magnet Element" +ReflectElement = "Reflect Element" Genie = "Genie" PeterPan = "Peter Pan" Stitch = "Stitch" ChickenLittle = "Chicken Little" -#movement +# movement HighJump = "High Jump" - - QuickRun = "Quick Run" - - AerialDodge = "Aerial Dodge" - - Glide = "Glide" - - DodgeRoll = "Dodge Roll" - -#keyblades +# keyblades Oathkeeper = "Oathkeeper" Oblivion = "Oblivion" StarSeeker = "Star Seeker" @@ -109,7 +90,6 @@ MeteorStaff = "Meteor Staff" CometStaff = "Comet Staff" Centurion2 = "Centurion+" -MeteorStaff = "Meteor Staff" NobodyLance = "Nobody Lance" PreciousMushroom = "Precious Mushroom" PreciousMushroom2 = "Precious Mushroom+" @@ -203,7 +183,7 @@ GrandRibbon = "Grand Ribbon" # usefull and stat incre -MickyMunnyPouch = "Mickey Munny Pouch" +MickeyMunnyPouch = "Mickey Munny Pouch" OletteMunnyPouch = "Olette Munny Pouch" HadesCupTrophy = "Hades Cup Trophy" UnknownDisk = "Unknown Disk" @@ -253,7 +233,6 @@ MagicLock = "Magic Lock-On" LeafBracer = "Leaf Bracer" CombinationBoost = "Combination Boost" -DamageDrive = "Damage Drive" OnceMore = "Once More" SecondChance = "Second Chance" @@ -313,10 +292,6 @@ DonaldFireBoost = "Donald Fire Boost" DonaldBlizzardBoost = "Donald Blizzard Boost" DonaldThunderBoost = "Donald Thunder Boost" -DonaldFireBoost = "Donald Fire Boost" -DonaldBlizzardBoost = "Donald Blizzard Boost" -DonaldThunderBoost = "Donald Thunder Boost" -DonaldMPRage = "Donald MP Rage" DonaldMPHastera = "Donald MP Hastera" DonaldAutoLimit = "Donald Auto Limit" DonaldHyperHealing = "Donald Hyper Healing" @@ -324,14 +299,7 @@ DonaldMPHastega = "Donald MP Hastega" DonaldItemBoost = "Donald Item Boost" DonaldDamageControl = "Donald Damage Control" -DonaldHyperHealing = "Donald Hyper Healing" -DonaldMPRage = "Donald MP Rage" DonaldMPHaste = "Donald MP Haste" -DonaldMPHastera = "Donald MP Hastera" -DonaldMPHastega = "Donald MP Hastega" -DonaldMPHaste = "Donald MP Haste" -DonaldDamageControl = "Donald Damage Control" -DonaldMPHastera = "Donald MP Hastera" DonaldDraw = "Donald Draw" # goofy abili @@ -353,27 +321,18 @@ GoofyAutoChange = "Goofy Auto Change" GoofyHyperHealing = "Goofy Hyper Healing" GoofyAutoHealing = "Goofy Auto Healing" -GoofyDefender = "Goofy Defender" -GoofyHyperHealing = "Goofy Hyper Healing" GoofyMPHaste = "Goofy MP Haste" GoofyMPHastera = "Goofy MP Hastera" -GoofyMPRage = "Goofy MP Rage" GoofyMPHastega = "Goofy MP Hastega" -GoofyItemBoost = "Goofy Item Boost" -GoofyDamageControl = "Goofy Damage Control" -GoofyProtect = "Goofy Protect" -GoofyProtera = "Goofy Protera" -GoofyProtega = "Goofy Protega" -GoofyDamageControl = "Goofy Damage Control" GoofyProtect = "Goofy Protect" GoofyProtera = "Goofy Protera" GoofyProtega = "Goofy Protega" Victory = "Victory" LuckyEmblem = "Lucky Emblem" -Bounty="Bounty" +Bounty = "Bounty" -UniversalKey="Universal Key" +# UniversalKey = "Universal Key" # Keyblade Slots FAKESlot = "FAKE (Slot)" DetectionSaberSlot = "Detection Saber (Slot)" @@ -402,3 +361,73 @@ FenrirSlot = "Fenrir (Slot)" UltimaWeaponSlot = "Ultima Weapon (Slot)" WinnersProofSlot = "Winner's Proof (Slot)" + +# events +HostileProgramEvent = "Hostile Program Event" +McpEvent = "Master Control Program Event" +ASLarxeneEvent = "AS Larxene Event" +DataLarxeneEvent = "Data Larxene Event" +BarbosaEvent = "Barbosa Event" +GrimReaper1Event = "Grim Reaper 1 Event" +GrimReaper2Event = "Grim Reaper 2 Event" +DataLuxordEvent = "Data Luxord Event" +DataAxelEvent = "Data Axel Event" +CerberusEvent = "Cerberus Event" +OlympusPeteEvent = "Olympus Pete Event" +HydraEvent = "Hydra Event" +OcPainAndPanicCupEvent = "Pain and Panic Cup Event" +OcCerberusCupEvent = "Cerberus Cup Event" +HadesEvent = "Hades Event" +ASZexionEvent = "AS Zexion Event" +DataZexionEvent = "Data Zexion Event" +Oc2TitanCupEvent = "Titan Cup Event" +Oc2GofCupEvent = "Goddess of Fate Cup Event" +Oc2CupsEvent = "Olympus Coliseum Cups Event" +HadesCupEvents = "Olympus Coliseum Hade's Paradox Event" +PrisonKeeperEvent = "Prison Keeper Event" +OogieBoogieEvent = "Oogie Boogie Event" +ExperimentEvent = "The Experiment Event" +ASVexenEvent = "AS Vexen Event" +DataVexenEvent = "Data Vexen Event" +ShanYuEvent = "Shan Yu Event" +AnsemRikuEvent = "Ansem Riku Event" +StormRiderEvent = "Storm Rider Event" +DataXigbarEvent = "Data Xigbar Event" +RoxasEvent = "Roxas Event" +XigbarEvent = "Xigbar Event" +LuxordEvent = "Luxord Event" +SaixEvent = "Saix Event" +XemnasEvent = "Xemnas Event" +ArmoredXemnasEvent = "Armored Xemnas Event" +ArmoredXemnas2Event = "Armored Xemnas 2 Event" +FinalXemnasEvent = "Final Xemnas Event" +DataXemnasEvent = "Data Xemnas Event" +ThresholderEvent = "Thresholder Event" +BeastEvent = "Beast Event" +DarkThornEvent = "Dark Thorn Event" +XaldinEvent = "Xaldin Event" +DataXaldinEvent = "Data Xaldin Event" +TwinLordsEvent = "Twin Lords Event" +GenieJafarEvent = "Genie Jafar Event" +ASLexaeusEvent = "AS Lexaeus Event" +DataLexaeusEvent = "Data Lexaeus Event" +ScarEvent = "Scar Event" +GroundShakerEvent = "Groundshaker Event" +DataSaixEvent = "Data Saix Event" +HBDemyxEvent = "Hollow Bastion Demyx Event" +ThousandHeartlessEvent = "Thousand Heartless Event" +Mushroom13Event = "Mushroom 13 Event" +SephiEvent = "Sephiroth Event" +DataDemyxEvent = "Data Demyx Event" +CorFirstFightEvent = "Cavern of Rememberance:Fight 1 Event" +CorSecondFightEvent = "Cavern of Rememberance:Fight 2 Event" +TransportEvent = "Transport to Rememberance Event" +OldPeteEvent = "Old Pete Event" +FuturePeteEvent = "Future Pete Event" +ASMarluxiaEvent = "AS Marluxia Event" +DataMarluxiaEvent = "Data Marluxia Event" +TerraEvent = "Terra Event" +TwilightThornEvent = "Twilight Thorn Event" +Axel1Event = "Axel 1 Event" +Axel2Event = "Axel 2 Event" +DataRoxasEvent = "Data Roxas Event" diff --git a/worlds/kh2/Names/LocationName.py b/worlds/kh2/Names/LocationName.py index 1a6c4d07fbdd..bcaf66455846 100644 --- a/worlds/kh2/Names/LocationName.py +++ b/worlds/kh2/Names/LocationName.py @@ -27,7 +27,7 @@ ThroneRoomMythrilCrystal = "(LoD2) Throne Room Mythril Crystal" ThroneRoomOrichalcum = "(LoD2) Throne Room Orichalcum" StormRider = "(LoD2) Storm Rider Bonus: Sora Slot 1" -XigbarDataDefenseBoost = "Data Xigbar" +XigbarDataDefenseBoost = "(Post LoD2: Summit) Data Xigbar" AgrabahMap = "(AG) Agrabah Map" AgrabahDarkShard = "(AG) Agrabah Dark Shard" @@ -62,9 +62,10 @@ RuinedChamberRuinsMap = "(AG2) Ruined Chamber Ruins Map" GenieJafar = "(AG2) Genie Jafar" WishingLamp = "(AG2) Wishing Lamp" -LexaeusBonus = "Lexaeus Bonus: Sora Slot 1" -LexaeusASStrengthBeyondStrength = "AS Lexaeus" -LexaeusDataLostIllusion = "Data Lexaeus" +LexaeusBonus = "(Post AG2: Peddler's Shop) Lexaeus Bonus: Sora Slot 1" +LexaeusASStrengthBeyondStrength = "(Post AG2: Peddler's Shop) AS Lexaeus" +LexaeusDataLostIllusion = "(Post AG2: Peddler's Shop) Data Lexaeus" + DCCourtyardMythrilShard = "(DC) Courtyard Mythril Shard" DCCourtyardStarRecipe = "(DC) Courtyard Star Recipe" DCCourtyardAPBoost = "(DC) Courtyard AP Boost" @@ -89,12 +90,15 @@ FuturePeteGetBonus = "(TR) Future Pete Bonus: Sora Slot 2" Monochrome = "(TR) Monochrome" WisdomForm = "(TR) Wisdom Form" -MarluxiaGetBonus = "Marluxia Bonus: Sora Slot 1" -MarluxiaASEternalBlossom = "AS Marluxia" -MarluxiaDataLostIllusion = "Data Marluxia" -LingeringWillBonus = "Lingering Will Bonus: Sora Slot 1" -LingeringWillProofofConnection = "Lingering Will Proof of Connection" -LingeringWillManifestIllusion = "Lingering Will Manifest Illusion" + +MarluxiaGetBonus = "(Post TR:Hall of the Cornerstone) Marluxia Bonus: Sora Slot 1" +MarluxiaASEternalBlossom = "(Post TR:Hall of the Cornerstone) AS Marluxia" +MarluxiaDataLostIllusion = "(Post TR:Hall of the Cornerstone) Data Marluxia" + +LingeringWillBonus = "(Post TR:Hall of the Cornerstone) Lingering Will Bonus: Sora Slot 1" +LingeringWillProofofConnection = "(Post TR:Hall of the Cornerstone) Lingering Will Proof of Connection" +LingeringWillManifestIllusion = "(Post TR:Hall of the Cornerstone) Lingering Will Manifest Illusion" + PoohsHouse100AcreWoodMap = "(100Acre) Pooh's House 100 Acre Wood Map" PoohsHouseAPBoost = "(100Acre) Pooh's House AP Boost" PoohsHouseMythrilStone = "(100Acre) Pooh's House Mythril Stone" @@ -119,6 +123,7 @@ StarryHillStyleRecipe = "(100Acre) Starry Hill Style Recipe" StarryHillCureElement = "(100Acre) Starry Hill Cure Element" StarryHillOrichalcumPlus = "(100Acre) Starry Hill Orichalcum+" + PassageMythrilShard = "(OC) Passage Mythril Shard" PassageMythrilStone = "(OC) Passage Mythril Stone" PassageEther = "(OC) Passage Ether" @@ -162,9 +167,9 @@ FatalCrestGoddessofFateCup = "Fatal Crest Goddess of Fate Cup" OrichalcumPlusGoddessofFateCup = "Orichalcum+ Goddess of Fate Cup" HadesCupTrophyParadoxCups = "Hades Cup Trophy Paradox Cups" -ZexionBonus = "Zexion Bonus: Sora Slot 1" -ZexionASBookofShadows = "AS Zexion" -ZexionDataLostIllusion = "Data Zexion" +ZexionBonus = "(Post OC2: Cave of the Dead Inner Chamber) Zexion Bonus: Sora Slot 1" +ZexionASBookofShadows = "(Post OC2: Cave of the Dead Inner Chamber) AS Zexion" +ZexionDataLostIllusion = "(Post OC2: Cave of the Dead Inner Chamber) Data Zexion" BCCourtyardAPBoost = "(BC) Courtyard AP Boost" @@ -198,7 +203,7 @@ Xaldin = "(BC2) Xaldin Bonus: Sora Slot 1" XaldinGetBonus = "(BC2) Xaldin Bonus: Sora Slot 2" SecretAnsemReport4 = "(BC2) Secret Ansem Report 4 (Xaldin)" -XaldinDataDefenseBoost = "Data Xaldin" +XaldinDataDefenseBoost = "(Post BC2: Ballroom) Data Xaldin" @@ -223,9 +228,9 @@ CentralComputerCoreMap = "(SP2) Central Computer Core Map" MCP = "(SP2) MCP Bonus: Sora Slot 1" MCPGetBonus = "(SP2) MCP Bonus: Sora Slot 2" -LarxeneBonus = "Larxene Bonus: Sora Slot 1" -LarxeneASCloakedThunder = "AS Larxene" -LarxeneDataLostIllusion = "Data Larxene" +LarxeneBonus = "(Post SP2: Central Computer Core) Larxene Bonus: Sora Slot 1" +LarxeneASCloakedThunder = "(Post SP2: Central Computer Core) AS Larxene" +LarxeneDataLostIllusion = "(Post SP2: Central Computer Core) Data Larxene" GraveyardMythrilShard = "(HT) Graveyard Mythril Shard" GraveyardSerenityGem = "(HT) Graveyard Serenity Gem" @@ -249,9 +254,9 @@ DecoyPresents = "(HT2) Decoy Presents" Experiment = "(HT2) Experiment Bonus: Sora Slot 1" DecisivePumpkin = "(HT2) Decisive Pumpkin" -VexenBonus = "Vexen Bonus: Sora Slot 1" -VexenASRoadtoDiscovery = "AS Vexen" -VexenDataLostIllusion = "Data Vexen" +VexenBonus = "(Post HT2: Yuletide Hill) Vexen Bonus: Sora Slot 1" +VexenASRoadtoDiscovery = "(Post HT2: Yuletide Hill) AS Vexen" +VexenDataLostIllusion = "(Post HT2: Yuletide Hill) Data Vexen" RampartNavalMap = "(PR) Rampart Naval Map" RampartMythrilStone = "(PR) Rampart Mythril Stone" @@ -286,7 +291,7 @@ GrimReaper2 = "(PR2) Grim Reaper 2 Bonus: Sora Slot 1" SecretAnsemReport6 = "(PR2) Secret Ansem Report 6 (Grim Reaper 2)" -LuxordDataAPBoost = "Data Luxord" +LuxordDataAPBoost = "(Post PR2: Treasure Heap) Data Luxord" MarketplaceMap = "(HB) Marketplace Map" BoroughDriveRecovery = "(HB) Borough Drive Recovery" @@ -329,7 +334,7 @@ SephirothFenrir = "Sephiroth Fenrir" WinnersProof = "(HB2) Winner's Proof" ProofofPeace = "(HB2) Proof of Peace" -DemyxDataAPBoost = "Data Demyx" +DemyxDataAPBoost = "(Post HB2: Restoration Site) Data Demyx" CoRDepthsAPBoost = "(CoR) Depths AP Boost" CoRDepthsPowerCrystal = "(CoR) Depths Power Crystal" @@ -386,7 +391,7 @@ Hyenas2 = "(PL2) Hyenas 2 Bonus: Sora Slot 1" Groundshaker = "(PL2) Groundshaker Bonus: Sora Slot 1" GroundshakerGetBonus = "(PL2) Groundshaker Bonus: Sora Slot 2" -SaixDataDefenseBoost = "Data Saix" +SaixDataDefenseBoost = "(Post PL2: Peak) Data Saix" TwilightTownMap = "(STT) Twilight Town Map" MunnyPouchOlette = "(STT) Munny Pouch Olette" @@ -415,7 +420,7 @@ MansionLibraryHiPotion = "(STT) Mansion Library Hi-Potion" Axel2 = "(STT) Axel 2" MansionBasementCorridorHiPotion = "(STT) Mansion Basement Corridor Hi-Potion" -RoxasDataMagicBoost = "Data Roxas" +RoxasDataMagicBoost = "(Post STT: Mansion Pod Room) Data Roxas" OldMansionPotion = "(TT) Old Mansion Potion" OldMansionMythrilShard = "(TT) Old Mansion Mythril Shard" @@ -468,46 +473,46 @@ MansionBasementCorridorUltimateRecipe = "(TT3) Mansion Basement Corridor Ultimate Recipe" BetwixtandBetween = "(TT3) Betwixt and Between" BetwixtandBetweenBondofFlame = "(TT3) Betwixt and Between Bond of Flame" -AxelDataMagicBoost = "Data Axel" +AxelDataMagicBoost = "(Post TT3: Betwixt and Between) Data Axel" FragmentCrossingMythrilStone = "(TWTNW) Fragment Crossing Mythril Stone" FragmentCrossingMythrilCrystal = "(TWTNW) Fragment Crossing Mythril Crystal" FragmentCrossingAPBoost = "(TWTNW) Fragment Crossing AP Boost" FragmentCrossingOrichalcum = "(TWTNW) Fragment Crossing Orichalcum" -Roxas = "(TWTNW) Roxas Bonus: Sora Slot 1" -RoxasGetBonus = "(TWTNW) Roxas Bonus: Sora Slot 2" -RoxasSecretAnsemReport8 = "(TWTNW) Roxas Secret Ansem Report 8" -TwoBecomeOne = "(TWTNW) Two Become One" -MemorysSkyscaperMythrilCrystal = "(TWTNW) Memory's Skyscaper Mythril Crystal" -MemorysSkyscaperAPBoost = "(TWTNW) Memory's Skyscaper AP Boost" -MemorysSkyscaperMythrilStone = "(TWTNW) Memory's Skyscaper Mythril Stone" -TheBrinkofDespairDarkCityMap = "(TWTNW) The Brink of Despair Dark City Map" -TheBrinkofDespairOrichalcumPlus = "(TWTNW) The Brink of Despair Orichalcum+" -NothingsCallMythrilGem = "(TWTNW) Nothing's Call Mythril Gem" -NothingsCallOrichalcum = "(TWTNW) Nothing's Call Orichalcum" -TwilightsViewCosmicBelt = "(TWTNW) Twilight's View Cosmic Belt" -XigbarBonus = "(TWTNW) Xigbar Bonus: Sora Slot 1" -XigbarSecretAnsemReport3 = "(TWTNW) Xigbar Secret Ansem Report 3" -NaughtsSkywayMythrilGem = "(TWTNW) Naught's Skyway Mythril Gem" -NaughtsSkywayOrichalcum = "(TWTNW) Naught's Skyway Orichalcum" -NaughtsSkywayMythrilCrystal = "(TWTNW) Naught's Skyway Mythril Crystal" -Oblivion = "(TWTNW) Oblivion" -CastleThatNeverWasMap = "(TWTNW) Castle That Never Was Map" -Luxord = "(TWTNW) Luxord" -LuxordGetBonus = "(TWTNW) Luxord Bonus: Sora Slot 1" -LuxordSecretAnsemReport9 = "(TWTNW) Luxord Secret Ansem Report 9" -SaixBonus = "(TWTNW) Saix Bonus: Sora Slot 1" -SaixSecretAnsemReport12 = "(TWTNW) Saix Secret Ansem Report 12" -PreXemnas1SecretAnsemReport11 = "(TWTNW) Secret Ansem Report 11 (Pre-Xemnas 1)" -RuinandCreationsPassageMythrilStone = "(TWTNW) Ruin and Creation's Passage Mythril Stone" -RuinandCreationsPassageAPBoost = "(TWTNW) Ruin and Creation's Passage AP Boost" -RuinandCreationsPassageMythrilCrystal = "(TWTNW) Ruin and Creation's Passage Mythril Crystal" -RuinandCreationsPassageOrichalcum = "(TWTNW) Ruin and Creation's Passage Orichalcum" -Xemnas1 = "(TWTNW) Xemnas 1 Bonus: Sora Slot 1" -Xemnas1GetBonus = "(TWTNW) Xemnas 1 Bonus: Sora Slot 2" -Xemnas1SecretAnsemReport13 = "(TWTNW) Xemnas 1 Secret Ansem Report 13" +Roxas = "(TWTNW2) Roxas Bonus: Sora Slot 1" +RoxasGetBonus = "(TWTNW2) Roxas Bonus: Sora Slot 2" +RoxasSecretAnsemReport8 = "(TWTNW2) Roxas Secret Ansem Report 8" +TwoBecomeOne = "(TWTNW2) Two Become One" +MemorysSkyscaperMythrilCrystal = "(TWTNW2) Memory's Skyscaper Mythril Crystal" +MemorysSkyscaperAPBoost = "(TWTNW2) Memory's Skyscaper AP Boost" +MemorysSkyscaperMythrilStone = "(TWTNW2) Memory's Skyscaper Mythril Stone" +TheBrinkofDespairDarkCityMap = "(TWTNW2) The Brink of Despair Dark City Map" +TheBrinkofDespairOrichalcumPlus = "(TWTNW2) The Brink of Despair Orichalcum+" +NothingsCallMythrilGem = "(TWTNW2) Nothing's Call Mythril Gem" +NothingsCallOrichalcum = "(TWTNW2) Nothing's Call Orichalcum" +TwilightsViewCosmicBelt = "(TWTNW2) Twilight's View Cosmic Belt" +XigbarBonus = "(TWTNW2) Xigbar Bonus: Sora Slot 1" +XigbarSecretAnsemReport3 = "(TWTNW2) Xigbar Secret Ansem Report 3" +NaughtsSkywayMythrilGem = "(TWTNW2) Naught's Skyway Mythril Gem" +NaughtsSkywayOrichalcum = "(TWTNW2) Naught's Skyway Orichalcum" +NaughtsSkywayMythrilCrystal = "(TWTNW2) Naught's Skyway Mythril Crystal" +Oblivion = "(TWTNW2) Oblivion" +CastleThatNeverWasMap = "(TWTNW2) Castle That Never Was Map" +Luxord = "(TWTNW2) Luxord Bonus: Sora Slot 2" +LuxordGetBonus = "(TWTNW2) Luxord Bonus: Sora Slot 1" +LuxordSecretAnsemReport9 = "(TWTNW2) Luxord Secret Ansem Report 9" +SaixBonus = "(TWTNW2) Saix Bonus: Sora Slot 1" +SaixSecretAnsemReport12 = "(TWTNW2) Saix Secret Ansem Report 12" +PreXemnas1SecretAnsemReport11 = "(TWTNW3) Secret Ansem Report 11 (Pre-Xemnas 1)" +RuinandCreationsPassageMythrilStone = "(TWTNW3) Ruin and Creation's Passage Mythril Stone" +RuinandCreationsPassageAPBoost = "(TWTNW3) Ruin and Creation's Passage AP Boost" +RuinandCreationsPassageMythrilCrystal = "(TWTNW3) Ruin and Creation's Passage Mythril Crystal" +RuinandCreationsPassageOrichalcum = "(TWTNW3) Ruin and Creation's Passage Orichalcum" +Xemnas1 = "(TWTNW3) Xemnas 1 Bonus: Sora Slot 1" +Xemnas1GetBonus = "(TWTNW3) Xemnas 1 Bonus: Sora Slot 2" +Xemnas1SecretAnsemReport13 = "(TWTNW3) Xemnas 1 Secret Ansem Report 13" FinalXemnas = "Final Xemnas" -XemnasDataPowerBoost = "Data Xemnas" +XemnasDataPowerBoost = "(Post TWTNW3: The Altar of Naught) Data Xemnas" Lvl1 ="Level 01" Lvl2 ="Level 02" Lvl3 ="Level 03" @@ -605,7 +610,7 @@ Lvl95 ="Level 95" Lvl96 ="Level 96" Lvl97 ="Level 97" -Lvl98 ="Level 98" +Lvl98 ="Level 98" Lvl99 ="Level 99" Valorlvl1 ="Valor level 1" Valorlvl2 ="Valor level 2" @@ -643,13 +648,28 @@ Finallvl6 ="Final level 6" Finallvl7 ="Final level 7" +Summonlvl2="Summon level 2" +Summonlvl3="Summon level 3" +Summonlvl4="Summon level 4" +Summonlvl5="Summon level 5" +Summonlvl6="Summon level 6" +Summonlvl7="Summon level 7" + + GardenofAssemblageMap ="Garden of Assemblage Map" GoALostIllusion ="GoA Lost Illusion" ProofofNonexistence ="Proof of Nonexistence Location" -test= "test" - +UnderseaKingdomMap ="(AT) Undersea Kingdom Map" +MysteriousAbyss ="(AT) Mysterious Abyss" +MusicalBlizzardElement ="(AT) Musical Blizzard Element" +MusicalOrichalcumPlus ="(AT) Musical Orichalcum+" +DonaldStarting1 ="Donald Starting Item 1" +DonaldStarting2 ="Donald Starting Item 2" +GoofyStarting1 ="Goofy Starting Item 1" +GoofyStarting2 ="Goofy Starting Item 2" +# TODO: remove in 4.3 Crit_1 ="Critical Starting Ability 1" Crit_2 ="Critical Starting Ability 2" Crit_3 ="Critical Starting Ability 3" @@ -657,14 +677,9 @@ Crit_5 ="Critical Starting Ability 5" Crit_6 ="Critical Starting Ability 6" Crit_7 ="Critical Starting Ability 7" -DonaldStarting1 ="Donald Starting Item 1" -DonaldStarting2 ="Donald Starting Item 2" -GoofyStarting1 ="Goofy Starting Item 1" -GoofyStarting2 ="Goofy Starting Item 2" - DonaldScreens ="(SP) Screens Bonus: Donald Slot 1" -DonaldDemyxHBGetBonus ="(HB) Demyx Bonus: Donald Slot 1" +DonaldDemyxHBGetBonus ="(HB2) Demyx Bonus: Donald Slot 1" DonaldDemyxOC ="(OC) Demyx Bonus: Donald Slot 1" DonaldBoatPete ="(TR) Boat Pete Bonus: Donald Slot 1" DonaldBoatPeteGetBonus ="(TR) Boat Pete Bonus: Donald Slot 2" @@ -694,7 +709,7 @@ GoofyBeast ="(BC) Beast Bonus: Goofy Slot 1" GoofyInterceptorBarrels ="(PR) Interceptor Barrels Bonus: Goofy Slot 1" GoofyTreasureRoom ="(AG) Treasure Room Heartless Bonus: Goofy Slot 1" -GoofyZexion ="Zexion Bonus: Goofy Slot 1" +GoofyZexion ="(Post OC2: Cave of the Dead Inner Chamber) Zexion Bonus: Goofy Slot 1" AdamantShield ="Adamant Shield Slot" @@ -760,4 +775,86 @@ WinnersProofSlot ="Winner's Proof Slot" PurebloodSlot ="Pureblood Slot" -#Final_Region ="Final Form" +Mushroom13_1 = "(Post TWTNW3: Memory's Skyscraper) Mushroom XIII No. 1" +Mushroom13_2 = "(Post HT2: Christmas Tree Plaza) Mushroom XIII No. 2" +Mushroom13_3 = "(Post BC2: Bridge) Mushroom XIII No. 3" +Mushroom13_4 = "(Post LOD2: Palace Gates) Mushroom XIII No. 4" +Mushroom13_5 = "(Post AG2: Treasure Room) Mushroom XIII No. 5" +Mushroom13_6 = "(Post OC2: Atrium) Mushroom XIII No. 6" +Mushroom13_7 = "(Post TT3: Tunnel way) Mushroom XIII No. 7" +Mushroom13_8 = "(Post TT3: Tower) Mushroom XIII No. 8" +Mushroom13_9 = "(Post HB2: Castle Gates) Mushroom XIII No. 9" +Mushroom13_10 = "(Post PR2: Moonlight Nook) Mushroom XIII No. 10" +Mushroom13_11 = "(Post TR: Waterway) Mushroom XIII No. 11" +Mushroom13_12 = "(Post TT3: Old Mansion) Mushroom XIII No. 12" + + +HostileProgramEventLocation = "Hostile Program Event Location" +McpEventLocation = "Master Control Program Event Location" +ASLarxeneEventLocation = "AS Larxene Event Location" +DataLarxeneEventLocation = "Data Larxene Event Location" +BarbosaEventLocation = "Barbosa Event Location" +GrimReaper1EventLocation = "Grim Reaper 1 Event Location" +GrimReaper2EventLocation = "Grim Reaper 2 Event Location" +DataLuxordEventLocation = "Data Luxord Event Location" +DataAxelEventLocation = "Data Axel Event Location" +CerberusEventLocation = "Cerberus Event Location" +OlympusPeteEventLocation = "Olympus Pete Event Location" +HydraEventLocation = "Hydra Event Location" +OcPainAndPanicCupEventLocation = "Pain and Panic Cup Event Location" +OcCerberusCupEventLocation = "Cerberus Cup Event Location" +HadesEventLocation = "Hades Event Location" +ASZexionEventLocation = "AS Zexion Event Location" +DataZexionEventLocation = "Data Zexion Event Location" +Oc2TitanCupEventLocation = "Titan Cup Event Location" +Oc2GofCupEventLocation = "Goddess of Fate Cup Event Location" +Oc2CupsEventLocation = "Olympus Coliseum Cups Event Location" +HadesCupEventLocations = "Olympus Coliseum Hade's Paradox Event Location" +PrisonKeeperEventLocation = "Prison Keeper Event Location" +OogieBoogieEventLocation = "Oogie Boogie Event Location" +ExperimentEventLocation = "The Experiment Event Location" +ASVexenEventLocation = "AS Vexen Event Location" +DataVexenEventLocation = "Data Vexen Event Location" +ShanYuEventLocation = "Shan Yu Event Location" +AnsemRikuEventLocation = "Ansem Riku Event Location" +StormRiderEventLocation = "Storm Rider Event Location" +DataXigbarEventLocation = "Data Xigbar Event Location" +RoxasEventLocation = "Roxas Event Location" +XigbarEventLocation = "Xigbar Event Location" +LuxordEventLocation = "Luxord Event Location" +SaixEventLocation = "Saix Event Location" +XemnasEventLocation = "Xemnas Event Location" +ArmoredXemnasEventLocation = "Armored Xemnas Event Location" +ArmoredXemnas2EventLocation = "Armored Xemnas 2 Event Location" +FinalXemnasEventLocation = "Final Xemnas Event Location" +DataXemnasEventLocation = "Data Xemnas Event Location" +ThresholderEventLocation = "Thresholder Event Location" +BeastEventLocation = "Beast Event Location" +DarkThornEventLocation = "Dark Thorn Event Location" +XaldinEventLocation = "Xaldin Event Location" +DataXaldinEventLocation = "Data Xaldin Event Location" +TwinLordsEventLocation = "Twin Lords Event Location" +GenieJafarEventLocation = "Genie Jafar Event Location" +ASLexaeusEventLocation = "AS Lexaeus Event Location" +DataLexaeusEventLocation = "Data Lexaeus Event Location" +ScarEventLocation = "Scar Event Location" +GroundShakerEventLocation = "Groundshaker Event Location" +DataSaixEventLocation = "Data Saix Event Location" +HBDemyxEventLocation = "Hollow Bastion Demyx Event Location" +ThousandHeartlessEventLocation = "Thousand Heartless Event Location" +Mushroom13EventLocation = "Mushroom 13 Event Location" +SephiEventLocation = "Sephiroth Event Location" +DataDemyxEventLocation = "Data Demyx Event Location" +CorFirstFightEventLocation = "Cavern of Rememberance:Fight 1 Event Location" +CorSecondFightEventLocation = "Cavern of Rememberance:Fight 2 Event Location" +TransportEventLocation = "Transport to Rememberance Event Location" +OldPeteEventLocation = "Old Pete Event Location" +FuturePeteEventLocation = "Future Pete Event Location" +ASMarluxiaEventLocation = "AS Marluxia Event Location" +DataMarluxiaEventLocation = "Data Marluxia Event Location" +TerraEventLocation = "Terra Event Location" +TwilightThornEventLocation = "Twilight Thorn Event Location" +Axel1EventLocation = "Axel 1 Event Location" +Axel2EventLocation = "Axel 2 Event Location" +DataRoxasEventLocation = "Data Roxas Event Location" + diff --git a/worlds/kh2/Names/RegionName.py b/worlds/kh2/Names/RegionName.py index d07b5d3de367..63ba6acdb878 100644 --- a/worlds/kh2/Names/RegionName.py +++ b/worlds/kh2/Names/RegionName.py @@ -1,90 +1,156 @@ -LoD_Region ="Land of Dragons" -LoD2_Region ="Land of Dragons 2" - -Ag_Region ="Agrabah" -Ag2_Region ="Agrabah 2" - -Dc_Region ="Disney Castle" -Tr_Region ="Timeless River" - -HundredAcre1_Region ="Pooh's House" -HundredAcre2_Region ="Piglet's House" -HundredAcre3_Region ="Rabbit's House" -HundredAcre4_Region ="Roo's House" -HundredAcre5_Region ="Spookey Cave" -HundredAcre6_Region ="Starry Hill" - -Pr_Region ="Port Royal" -Pr2_Region ="Port Royal 2" -Gr2_Region ="Grim Reaper 2" - -Oc_Region ="Olympus Coliseum" -Oc2_Region ="Olympus Coliseum 2" -Oc2_pain_and_panic_Region ="Pain and Panic Cup" -Oc2_titan_Region ="Titan Cup" -Oc2_cerberus_Region ="Cerberus Cup" -Oc2_gof_Region ="Goddest of Fate Cup" -Oc2Cups_Region ="Olympus Coliseum Cups" -HadesCups_Region ="Olympus Coliseum Hade's Paradox" - -Bc_Region ="Beast's Castle" -Bc2_Region ="Beast's Castle 2" -Xaldin_Region ="Xaldin" - -Sp_Region ="Space Paranoids" -Sp2_Region ="Space Paranoids 2" -Mcp_Region ="Master Control Program" - -Ht_Region ="Holloween Town" -Ht2_Region ="Holloween Town 2" - -Hb_Region ="Hollow Bastion" -Hb2_Region ="Hollow Bastion 2" -ThousandHeartless_Region ="Thousand Hearless" -Mushroom13_Region ="Mushroom 13" -CoR_Region ="Cavern of Rememberance" -Transport_Region ="Transport to Rememberance" - -Pl_Region ="Pride Lands" -Pl2_Region ="Pride Lands 2" - -STT_Region ="Simulated Twilight Town" - -TT_Region ="Twlight Town" -TT2_Region ="Twlight Town 2" -TT3_Region ="Twlight Town 3" - -Twtnw_Region ="The World That Never Was (First Visit)" -Twtnw_PostRoxas ="The World That Never Was (Post Roxas)" -Twtnw_PostXigbar ="The World That Never Was (Post Xigbar)" -Twtnw2_Region ="The World That Never Was (Second Visit)" #before riku transformation - -SoraLevels_Region ="Sora's Levels" -GoA_Region ="Garden Of Assemblage" -Keyblade_Region ="Keyblade Slots" - -Valor_Region ="Valor Form" -Wisdom_Region ="Wisdom Form" -Limit_Region ="Limit Form" -Master_Region ="Master Form" -Final_Region ="Final Form" - -Terra_Region ="Lingering Will" -Sephi_Region ="Sephiroth" -Marluxia_Region ="Marluxia" -Larxene_Region ="Larxene" -Vexen_Region ="Vexen" -Lexaeus_Region ="Lexaeus" -Zexion_Region ="Zexion" - -LevelsVS1 ="Levels Region (1 Visit Locking Item)" -LevelsVS3 ="Levels Region (3 Visit Locking Items)" -LevelsVS6 ="Levels Region (6 Visit Locking Items)" -LevelsVS9 ="Levels Region (9 Visit Locking Items)" -LevelsVS12 ="Levels Region (12 Visit Locking Items)" -LevelsVS15 ="Levels Region (15 Visit Locking Items)" -LevelsVS18 ="Levels Region (18 Visit Locking Items)" -LevelsVS21 ="Levels Region (21 Visit Locking Items)" -LevelsVS24 ="Levels Region (24 Visit Locking Items)" -LevelsVS26 ="Levels Region (26 Visit Locking Items)" - +Ha1 = "Pooh's House" +Ha2 = "Piglet's House" +Ha3 = "Rabbit's House" +Ha4 = "Roo's House" +Ha5 = "Spooky Cave" +Ha6 = "Starry Hill" + +SoraLevels = "Sora's Levels" +GoA = "Garden Of Assemblage" +Keyblade = "Weapon Slots" + +Valor = "Valor Form" +Wisdom = "Wisdom Form" +Limit = "Limit Form" +Master = "Master Form" +Final = "Final Form" +Summon = "Summons" +# sp +Sp = "Space Paranoids" +HostileProgram = "Hostile Program" +Sp2 = "Space Paranoids 2" +Mcp = "Master Control Program" +ASLarxene = "AS Larxene" +DataLarxene = "Data Larxene" + +# pr +Pr = "Port Royal" +Barbosa = "Barbosa" +Pr2 = "Port Royal 2" +GrimReaper1 = "Grim Reaper 1" +GrimReaper2 = "Grim Reaper 2" +DataLuxord = "Data Luxord" + +# tt +Tt = "Twilight Town" +Tt2 = "Twilight Town 2" +Tt3 = "Twilight Town 3" +DataAxel = "Data Axel" + +# oc +Oc = "Olympus Coliseum" +Cerberus = "Cerberus" +OlympusPete = "Olympus Pete" +Hydra = "Hydra" +OcPainAndPanicCup = "Pain and Panic Cup" +OcCerberusCup = "Cerberus Cup" +Oc2 = "Olympus Coliseum 2" +Hades = "Hades" +ASZexion = "AS Zexion" +DataZexion = "Data Zexion" +Oc2TitanCup = "Titan Cup" +Oc2GofCup = "Goddess of Fate Cup" +Oc2Cups = "Olympus Coliseum Cups" +HadesCups = "Olympus Coliseum Hade's Paradox" + +# ht +Ht = "Holloween Town" +PrisonKeeper = "Prison Keeper" +OogieBoogie = "Oogie Boogie" +Ht2 = "Holloween Town 2" +Experiment = "The Experiment" +ASVexen = "AS Vexen" +DataVexen = "Data Vexen" + +# lod +LoD = "Land of Dragons" +ShanYu = "Shan Yu" +LoD2 = "Land of Dragons 2" +AnsemRiku = "Ansem Riku" +StormRider = "Storm Rider" +DataXigbar = "Data Xigbar" + +# twtnw +Twtnw = "The World That Never Was (Pre Roxas)" +Roxas = "Roxas" +Xigbar = "Xigbar" +Luxord = "Luxord" +Saix = "Saix" +Twtnw2 = "The World That Never Was (Second Visit)" # Post riku transformation +Xemnas = "Xemnas" +ArmoredXemnas = "Armored Xemnas" +ArmoredXemnas2 = "Armored Xemnas 2" +FinalXemnas = "Final Xemnas" +DataXemnas = "Data Xemnas" + +# bc +Bc = "Beast's Castle" +Thresholder = "Thresholder" +Beast = "Beast" +DarkThorn = "Dark Thorn" +Bc2 = "Beast's Castle 2" +Xaldin = "Xaldin" +DataXaldin = "Data Xaldin" + +# ag +Ag = "Agrabah" +TwinLords = "Twin Lords" +Ag2 = "Agrabah 2" +GenieJafar = "Genie Jafar" +ASLexaeus = "AS Lexaeus" +DataLexaeus = "Data Lexaeus" + +# pl +Pl = "Pride Lands" +Scar = "Scar" +Pl2 = "Pride Lands 2" +GroundShaker = "Groundshaker" +DataSaix = "Data Saix" + +# hb +Hb = "Hollow Bastion" +Hb2 = "Hollow Bastion 2" +HBDemyx = "Hollow Bastion Demyx" +ThousandHeartless = "Thousand Heartless" +Mushroom13 = "Mushroom 13" +Sephi = "Sephiroth" +DataDemyx = "Data Demyx" + +# CoR +CoR = "Cavern of Rememberance" +CorFirstFight = "Cavern of Rememberance:Fight 1" +CorSecondFight = "Cavern of Rememberance:Fight 2" +Transport = "Transport to Rememberance" + +# dc +Dc = "Disney Castle" +Tr = "Timeless River" +OldPete = "Old Pete" +FuturePete = "Future Pete" +ASMarluxia = "AS Marluxia" +DataMarluxia = "Data Marluxia" +Terra = "Terra" + +# stt +Stt = "Simulated Twilight Town" +TwilightThorn = "Twilight Thorn" +Axel1 = "Axel 1" +Axel2 = "Axel 2" +DataRoxas = "Data Roxas" + +AtlanticaSongOne = "Atlantica First Song" +AtlanticaSongTwo = "Atlantica Second Song" +AtlanticaSongThree = "Atlantica Third Song" +AtlanticaSongFour = "Atlantica Fourth Song" + + +LevelsVS1 = "Levels Region (1 Visit Locking Item)" +LevelsVS3 = "Levels Region (3 Visit Locking Items)" +LevelsVS6 = "Levels Region (6 Visit Locking Items)" +LevelsVS9 = "Levels Region (9 Visit Locking Items)" +LevelsVS12 = "Levels Region (12 Visit Locking Items)" +LevelsVS15 = "Levels Region (15 Visit Locking Items)" +LevelsVS18 = "Levels Region (18 Visit Locking Items)" +LevelsVS21 = "Levels Region (21 Visit Locking Items)" +LevelsVS24 = "Levels Region (24 Visit Locking Items)" +LevelsVS26 = "Levels Region (26 Visit Locking Items)" diff --git a/worlds/kh2/OpenKH.py b/worlds/kh2/OpenKH.py index c3334dbb9949..6b0418c9976b 100644 --- a/worlds/kh2/OpenKH.py +++ b/worlds/kh2/OpenKH.py @@ -5,7 +5,7 @@ import Utils import zipfile -from .Items import item_dictionary_table, CheckDupingItems +from .Items import item_dictionary_table from .Locations import all_locations, SoraLevels, exclusion_table from .XPValues import lvlStats, formExp, soraExp from worlds.Files import APContainer @@ -15,7 +15,7 @@ class KH2Container(APContainer): game: str = 'Kingdom Hearts 2' def __init__(self, patch_data: dict, base_path: str, output_directory: str, - player=None, player_name: str = "", server: str = ""): + player=None, player_name: str = "", server: str = ""): self.patch_data = patch_data self.file_path = base_path container_path = os.path.join(output_directory, base_path + ".zip") @@ -24,12 +24,6 @@ def __init__(self, patch_data: dict, base_path: str, output_directory: str, def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: for filename, yml in self.patch_data.items(): opened_zipfile.writestr(filename, yml) - for root, dirs, files in os.walk(os.path.join(os.path.dirname(__file__), "mod_template")): - for file in files: - opened_zipfile.write(os.path.join(root, file), - os.path.relpath(os.path.join(root, file), - os.path.join(os.path.dirname(__file__), "mod_template"))) - # opened_zipfile.writestr(self.zpf_path, self.patch_data) super().write_contents(opened_zipfile) @@ -59,13 +53,6 @@ def increaseStat(i): formexp = None formName = None levelsetting = list() - slotDataDuping = set() - for values in CheckDupingItems.values(): - if isinstance(values, set): - slotDataDuping = slotDataDuping.union(values) - else: - for inner_values in values.values(): - slotDataDuping = slotDataDuping.union(inner_values) if self.multiworld.Keyblade_Minimum[self.player].value > self.multiworld.Keyblade_Maximum[self.player].value: logging.info( @@ -89,14 +76,19 @@ def increaseStat(i): levelsetting.extend(exclusion_table["Level99Sanity"]) mod_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.get_file_safe_player_name(self.player)}" - + all_valid_locations = {location for location, data in all_locations.items()} for location in self.multiworld.get_filled_locations(self.player): - - data = all_locations[location.name] - if location.item.player == self.player: - itemcode = item_dictionary_table[location.item.name].kh2id + if location.name in all_valid_locations: + data = all_locations[location.name] else: - itemcode = 90 # castle map + continue + if location.item: + if location.item.player == self.player: + itemcode = item_dictionary_table[location.item.name].kh2id + else: + itemcode = 90 # castle map + else: + itemcode = 90 if data.yml == "Chest": self.formattedTrsr[data.locid] = {"ItemId": itemcode} @@ -129,8 +121,8 @@ def increaseStat(i): elif data.yml == "Keyblade": self.formattedItem["Stats"].append({ "Id": data.locid, - "Attack": self.multiworld.per_slot_randoms[self.player].randint(keyblademin, keyblademax), - "Magic": self.multiworld.per_slot_randoms[self.player].randint(keyblademin, keyblademax), + "Attack": self.random.randint(keyblademin, keyblademax), + "Magic": self.random.randint(keyblademin, keyblademax), "Defense": 0, "Ability": itemcode, "AbilityPoints": 0, @@ -154,7 +146,8 @@ def increaseStat(i): 2: self.multiworld.Wisdom_Form_EXP[self.player].value, 3: self.multiworld.Limit_Form_EXP[self.player].value, 4: self.multiworld.Master_Form_EXP[self.player].value, - 5: self.multiworld.Final_Form_EXP[self.player].value} + 5: self.multiworld.Final_Form_EXP[self.player].value + } formexp = formDictExp[data.charName] formName = formDict[data.charName] self.formattedFmlv[formName] = [] @@ -174,7 +167,7 @@ def increaseStat(i): "GrowthAbilityLevel": 0, }) - # Summons have no checks on them so done fully locally + # Summons have no actual locations so done down here. self.formattedFmlv["Summon"] = [] for x in range(1, 7): self.formattedFmlv["Summon"].append({ @@ -185,17 +178,18 @@ def increaseStat(i): "GrowthAbilityLevel": 0, }) # levels done down here because of optional settings that can take locations out of the pool. - self.i = 1 + self.i = 2 for location in SoraLevels: - increaseStat(self.multiworld.per_slot_randoms[self.player].randint(0, 3)) + increaseStat(self.random.randint(0, 3)) if location in levelsetting: data = self.multiworld.get_location(location, self.player) - if data.item.player == self.player: - itemcode = item_dictionary_table[data.item.name].kh2id - else: - itemcode = 90 # castle map + if data.item: + if data.item.player == self.player: + itemcode = item_dictionary_table[data.item.name].kh2id + else: + itemcode = 90 # castle map else: - increaseStat(self.multiworld.per_slot_randoms[self.player].randint(0, 3)) + increaseStat(self.random.randint(0, 3)) itemcode = 0 self.formattedLvup["Sora"][self.i] = { "Exp": int(soraExp[self.i] / self.multiworld.Sora_Level_EXP[self.player].value), @@ -229,6 +223,193 @@ def increaseStat(i): "GeneralResistance": 100, "Unknown": 0 }) + self.formattedLvup["Sora"][1] = { + "Exp": int(soraExp[1] / self.multiworld.Sora_Level_EXP[self.player].value), + "Strength": 2, + "Magic": 6, + "Defense": 2, + "Ap": 0, + "SwordAbility": 0, + "ShieldAbility": 0, + "StaffAbility": 0, + "Padding": 0, + "Character": "Sora", + "Level": 1 + } + self.mod_yml = { + "assets": [ + { + 'method': 'binarc', + 'name': '00battle.bin', + 'source': [ + { + 'method': 'listpatch', + 'name': 'fmlv', + 'source': [ + { + 'name': 'FmlvList.yml', + 'type': 'fmlv' + } + ], + 'type': 'List' + }, + { + 'method': 'listpatch', + 'name': 'lvup', + 'source': [ + { + 'name': 'LvupList.yml', + 'type': 'lvup' + } + ], + 'type': 'List' + }, + { + 'method': 'listpatch', + 'name': 'bons', + 'source': [ + { + 'name': 'BonsList.yml', + 'type': 'bons' + } + ], + 'type': 'List' + } + ] + }, + { + 'method': 'binarc', + 'name': '03system.bin', + 'source': [ + { + 'method': 'listpatch', + 'name': 'trsr', + 'source': [ + { + 'name': 'TrsrList.yml', + 'type': 'trsr' + } + ], + 'type': 'List' + }, + { + 'method': 'listpatch', + 'name': 'item', + 'source': [ + { + 'name': 'ItemList.yml', + 'type': 'item' + } + ], + 'type': 'List' + } + ] + }, + { + 'name': 'msg/us/po.bar', + 'multi': [ + { + 'name': 'msg/fr/po.bar' + }, + { + 'name': 'msg/gr/po.bar' + }, + { + 'name': 'msg/it/po.bar' + }, + { + 'name': 'msg/sp/po.bar' + } + ], + 'method': 'binarc', + 'source': [ + { + 'name': 'po', + 'type': 'list', + 'method': 'kh2msg', + 'source': [ + { + 'name': 'po.yml', + 'language': 'en' + } + ] + } + ] + }, + { + 'name': 'msg/us/sys.bar', + 'multi': [ + { + 'name': 'msg/fr/sys.bar' + }, + { + 'name': 'msg/gr/sys.bar' + }, + { + 'name': 'msg/it/sys.bar' + }, + { + 'name': 'msg/sp/sys.bar' + } + ], + 'method': 'binarc', + 'source': [ + { + 'name': 'sys', + 'type': 'list', + 'method': 'kh2msg', + 'source': [ + { + 'name': 'sys.yml', + 'language': 'en' + } + ] + } + ] + }, + ], + 'title': 'Randomizer Seed' + } + + goal_to_text = { + 0: "Three Proofs", + 1: "Lucky Emblem", + 2: "Hitlist", + 3: "Lucky Emblem and Hitlist", + } + lucky_emblem_text = { + 0: "Your Goal is not Lucky Emblem. It is Hitlist or Three Proofs.", + 1: f"Lucky Emblem Required: {self.multiworld.LuckyEmblemsRequired[self.player]} out of {self.multiworld.LuckyEmblemsAmount[self.player]}", + 2: "Your Goal is not Lucky Emblem. It is Hitlist or Three Proofs.", + 3: f"Lucky Emblem Required: {self.multiworld.LuckyEmblemsRequired[self.player]} out of {self.multiworld.LuckyEmblemsAmount[self.player]}" + } + hitlist_text = { + 0: "Your Goal is not Hitlist. It is Lucky Emblem or Three Proofs", + 1: "Your Goal is not Hitlist. It is Lucky Emblem or Three Proofs", + 2: f"Bounties Required: {self.multiworld.BountyRequired[self.player]} out of {self.multiworld.BountyAmount[self.player]}", + 3: f"Bounties Required: {self.multiworld.BountyRequired[self.player]} out of {self.multiworld.BountyAmount[self.player]}", + } + + self.pooh_text = [ + { + 'id': 18326, + 'en': f"Your goal is {goal_to_text[self.multiworld.Goal[self.player].value]}" + }, + { + 'id': 18327, + 'en': lucky_emblem_text[self.multiworld.Goal[self.player].value] + }, + { + 'id': 18328, + 'en': hitlist_text[self.multiworld.Goal[self.player].value] + } + ] + self.level_depth_text = [ + { + 'id': 0x3BF1, + 'en': f"Your Level Depth is {self.multiworld.LevelDepth[self.player].current_option_name}" + } + ] mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__) openkhmod = { @@ -237,8 +418,11 @@ def increaseStat(i): "BonsList.yml": yaml.dump(self.formattedBons, line_break="\n"), "ItemList.yml": yaml.dump(self.formattedItem, line_break="\n"), "FmlvList.yml": yaml.dump(self.formattedFmlv, line_break="\n"), + "mod.yml": yaml.dump(self.mod_yml, line_break="\n"), + "po.yml": yaml.dump(self.pooh_text, line_break="\n"), + "sys.yml": yaml.dump(self.level_depth_text, line_break="\n"), } mod = KH2Container(openkhmod, mod_dir, output_directory, self.player, - self.multiworld.get_file_safe_player_name(self.player)) + self.multiworld.get_file_safe_player_name(self.player)) mod.write() diff --git a/worlds/kh2/Options.py b/worlds/kh2/Options.py index 7a6f106aa9b8..7ba7c0082d17 100644 --- a/worlds/kh2/Options.py +++ b/worlds/kh2/Options.py @@ -1,7 +1,8 @@ -from Options import Choice, Option, Range, Toggle, OptionSet -import typing +from dataclasses import dataclass -from worlds.kh2 import SupportAbility_Table, ActionAbility_Table +from Options import Choice, Range, Toggle, ItemDict, PerGameCommonOptions, StartInventoryPool + +from worlds.kh2 import default_itempool_option class SoraEXP(Range): @@ -107,23 +108,61 @@ class Visitlocking(Choice): First and Second Visit Locking: One item for First Visit Two For Second Visit""" display_name = "Visit locking" option_no_visit_locking = 0 # starts with 25 visit locking - option_second_visit_locking = 1 # starts with 13 (no icecream/picture) + option_second_visit_locking = 1 # starts with 12 visit locking option_first_and_second_visit_locking = 2 # starts with nothing default = 2 +class FightLogic(Choice): + """ + The level of logic to use when determining what fights in each KH2 world are beatable. + + Easy: For Players not very comfortable doing things without a lot of tools. + + Normal: For Players somewhat comfortable doing fights with some of the tools. + + Hard: For Players comfortable doing fights with almost no tools. + """ + display_name = "Fight Logic" + option_easy = 0 + option_normal = 1 + option_hard = 2 + default = 1 + + +class FinalFormLogic(Choice): + """Determines forcing final form logic + + No Light and Darkness: Light and Darkness is not in logic. + Light And Darkness: Final Forcing with light and darkness is in logic. + Just a Form: All that requires final forcing is another form. + """ + display_name = "Final Form Logic" + option_no_light_and_darkness = 0 + option_light_and_darkness = 1 + option_just_a_form = 2 + default = 1 + + +class AutoFormLogic(Toggle): + """ Have Auto Forms levels in logic. + """ + display_name = "Auto Form Logic" + default = False + + class RandomVisitLockingItem(Range): """Start with random amount of visit locking items.""" display_name = "Random Visit Locking Item" range_start = 0 range_end = 25 - default = 3 + default = 0 class SuperBosses(Toggle): - """Terra, Sephiroth and Data Fights Toggle.""" + """Terra Sephiroth and Data Fights Toggle.""" display_name = "Super Bosses" - default = False + default = True class Cups(Choice): @@ -135,7 +174,7 @@ class Cups(Choice): option_no_cups = 0 option_cups = 1 option_cups_and_hades_paradox = 2 - default = 1 + default = 0 class LevelDepth(Choice): @@ -157,67 +196,71 @@ class LevelDepth(Choice): default = 0 -class PromiseCharm(Toggle): - """Add Promise Charm to the Pool""" - display_name = "Promise Charm" - default = False +class DonaldGoofyStatsanity(Toggle): + """Toggles if on Donald and Goofy's Get Bonus locations can be any item""" + display_name = "Donald & Goofy Statsanity" + default = True -class KeybladeAbilities(Choice): - """ - Action: Action Abilities in the Keyblade Slot Pool. +class AtlanticaToggle(Toggle): + """Atlantica Toggle""" + display_name = "Atlantica Toggle" + default = False - Support: Support Abilities in the Keyblade Slot Pool. - Both: Action and Support Abilities in the Keyblade Slot Pool.""" - display_name = "Keyblade Abilities" - option_support = 0 - option_action = 1 - option_both = 2 - default = 0 +class PromiseCharm(Toggle): + """Add Promise Charm to the pool""" + display_name = "Promise Charm" + default = False -class BlacklistKeyblade(OptionSet): - """Black List these Abilities on Keyblades""" - display_name = "Blacklist Keyblade Abilities" - valid_keys = set(SupportAbility_Table.keys()).union(ActionAbility_Table.keys()) +class AntiForm(Toggle): + """Add Anti Form to the pool""" + display_name = "Anti Form" + default = False class Goal(Choice): """Win Condition - Three Proofs: Get a Gold Crown on Sora's Head. + Three Proofs: Find the 3 Proofs to unlock the final door. + + Lucky Emblem Hunt: Find required amount of Lucky Emblems. - Lucky Emblem Hunt: Find Required Amount of Lucky Emblems . + Hitlist (Bounty Hunt): Find required amount of Bounties. - Hitlist (Bounty Hunt): Find Required Amount of Bounties""" + Lucky Emblem and Hitlist: Find the required amount of Lucky Emblems and Bounties.""" display_name = "Goal" option_three_proofs = 0 option_lucky_emblem_hunt = 1 option_hitlist = 2 - default = 0 + option_hitlist_and_lucky_emblem = 3 + default = 1 class FinalXemnas(Toggle): """Kill Final Xemnas to Beat the Game. - This is in addition to your Goal. I.E. get three proofs+kill final Xemnas""" + + This is in addition to your Goal. + + I.E. get three proofs+kill final Xemnas""" display_name = "Final Xemnas" default = True class LuckyEmblemsRequired(Range): - """Number of Lucky Emblems to collect to Win/Unlock Final Xemnas Door. + """Number of Lucky Emblems to collect to Win/Unlock Final Xemnas' Door. - If Goal is not Lucky Emblem Hunt this does nothing.""" + If Goal is not Lucky Emblem Hunt or Lucky Emblem and Hitlist this does nothing.""" display_name = "Lucky Emblems Required" range_start = 1 range_end = 60 - default = 30 + default = 35 class LuckyEmblemsAmount(Range): """Number of Lucky Emblems that are in the pool. - If Goal is not Lucky Emblem Hunt this does nothing.""" + If Goal is not Lucky Emblem Hunt or Lucky Emblem and Hitlist this does nothing.""" display_name = "Lucky Emblems Available" range_start = 1 range_end = 60 @@ -227,48 +270,103 @@ class LuckyEmblemsAmount(Range): class BountyRequired(Range): """Number of Bounties to collect to Win/Unlock Final Xemnas Door. - If Goal is not Hitlist this does nothing.""" + If Goal is not Hitlist or Lucky Emblem and Hitlist this does nothing.""" display_name = "Bounties Required" range_start = 1 - range_end = 24 + range_end = 26 default = 7 class BountyAmount(Range): """Number of Bounties that are in the pool. - If Goal is not Hitlist this does nothing.""" + If Goal is not Hitlist or Lucky Emblem and Hitlist this does nothing.""" display_name = "Bounties Available" range_start = 1 - range_end = 24 - default = 13 - - -KH2_Options: typing.Dict[str, type(Option)] = { - "LevelDepth": LevelDepth, - "Sora_Level_EXP": SoraEXP, - "Valor_Form_EXP": ValorEXP, - "Wisdom_Form_EXP": WisdomEXP, - "Limit_Form_EXP": LimitEXP, - "Master_Form_EXP": MasterEXP, - "Final_Form_EXP": FinalEXP, - "Summon_EXP": SummonEXP, - "Schmovement": Schmovement, - "RandomGrowth": RandomGrowth, - "Promise_Charm": PromiseCharm, - "Goal": Goal, - "FinalXemnas": FinalXemnas, - "LuckyEmblemsAmount": LuckyEmblemsAmount, - "LuckyEmblemsRequired": LuckyEmblemsRequired, - "BountyAmount": BountyAmount, - "BountyRequired": BountyRequired, - "Keyblade_Minimum": KeybladeMin, - "Keyblade_Maximum": KeybladeMax, - "Visitlocking": Visitlocking, - "RandomVisitLockingItem": RandomVisitLockingItem, - "SuperBosses": SuperBosses, - "KeybladeAbilities": KeybladeAbilities, - "BlacklistKeyblade": BlacklistKeyblade, - "Cups": Cups, - -} + range_end = 26 + default = 10 + + +class BountyStartHint(Toggle): + """Start with Bounties Hinted""" + display_name = "Start with Bounties Hinted" + default = False + + +class WeaponSlotStartHint(Toggle): + """Start with Weapon Slots' Hinted""" + display_name = "Start with Weapon Slots Hinted" + default = False + + +class CorSkipToggle(Toggle): + """Toggle for Cor skip. + + Tools depend on which difficulty was chosen on Fight Difficulty. + + Toggle does not negate fight logic but is an alternative. + + Final Chest is also can be put into logic with this skip. + """ + display_name = "CoR Skip Toggle." + default = False + + +class CustomItemPoolQuantity(ItemDict): + """Add more of an item into the itempool. Note: You cannot take out items from the pool.""" + display_name = "Custom Item Pool" + verify_item_name = True + default = default_itempool_option + + +class FillerItemsLocal(Toggle): + """Make all dynamic filler classified items local. Recommended when playing with games with fewer locations than kh2""" + display_name = "Local Filler Items" + default = True + + +class SummonLevelLocationToggle(Toggle): + """Toggle Summon levels to have locations.""" + display_name = "Summon Level Locations" + default = False + + +# shamelessly stolen from the messanger +@dataclass +class KingdomHearts2Options(PerGameCommonOptions): + start_inventory: StartInventoryPool + LevelDepth: LevelDepth + Sora_Level_EXP: SoraEXP + Valor_Form_EXP: ValorEXP + Wisdom_Form_EXP: WisdomEXP + Limit_Form_EXP: LimitEXP + Master_Form_EXP: MasterEXP + Final_Form_EXP: FinalEXP + Summon_EXP: SummonEXP + Schmovement: Schmovement + RandomGrowth: RandomGrowth + AntiForm: AntiForm + Promise_Charm: PromiseCharm + Goal: Goal + FinalXemnas: FinalXemnas + LuckyEmblemsAmount: LuckyEmblemsAmount + LuckyEmblemsRequired: LuckyEmblemsRequired + BountyAmount: BountyAmount + BountyRequired: BountyRequired + BountyStartingHintToggle: BountyStartHint + Keyblade_Minimum: KeybladeMin + Keyblade_Maximum: KeybladeMax + WeaponSlotStartHint: WeaponSlotStartHint + FightLogic: FightLogic + FinalFormLogic: FinalFormLogic + AutoFormLogic: AutoFormLogic + DonaldGoofyStatsanity: DonaldGoofyStatsanity + FillerItemsLocal: FillerItemsLocal + Visitlocking: Visitlocking + RandomVisitLockingItem: RandomVisitLockingItem + SuperBosses: SuperBosses + Cups: Cups + SummonLevelLocationToggle: SummonLevelLocationToggle + AtlanticaToggle: AtlanticaToggle + CorSkipToggle: CorSkipToggle + CustomItemPoolQuantity: CustomItemPoolQuantity diff --git a/worlds/kh2/Regions.py b/worlds/kh2/Regions.py index 36fc0c046b5c..aceab97f37ce 100644 --- a/worlds/kh2/Regions.py +++ b/worlds/kh2/Regions.py @@ -1,35 +1,22 @@ import typing -from BaseClasses import MultiWorld, Region, Entrance +from BaseClasses import MultiWorld, Region -from .Locations import KH2Location, RegionTable -from .Names import LocationName, ItemName, RegionName +from .Locations import KH2Location, event_location_to_item +from . import LocationName, RegionName, Events_Table - -def create_regions(world, player: int, active_locations): - menu_region = create_region(world, player, active_locations, 'Menu', None) - - goa_region_locations = [ - LocationName.Crit_1, - LocationName.Crit_2, - LocationName.Crit_3, - LocationName.Crit_4, - LocationName.Crit_5, - LocationName.Crit_6, - LocationName.Crit_7, +KH2REGIONS: typing.Dict[str, typing.List[str]] = { + "Menu": [], + RegionName.GoA: [ LocationName.GardenofAssemblageMap, LocationName.GoALostIllusion, LocationName.ProofofNonexistence, - LocationName.DonaldStarting1, - LocationName.DonaldStarting2, - LocationName.GoofyStarting1, - LocationName.GoofyStarting2, - ] - - goa_region = create_region(world, player, active_locations, RegionName.GoA_Region, - goa_region_locations) - - lod_Region_locations = [ + # LocationName.DonaldStarting1, + # LocationName.DonaldStarting2, + # LocationName.GoofyStarting1, + # LocationName.GoofyStarting2 + ], + RegionName.LoD: [ LocationName.BambooGroveDarkShard, LocationName.BambooGroveEther, LocationName.BambooGroveMythrilShard, @@ -47,14 +34,16 @@ def create_regions(world, player: int, active_locations): LocationName.VillageCaveBonus, LocationName.RidgeFrostShard, LocationName.RidgeAPBoost, + ], + RegionName.ShanYu: [ LocationName.ShanYu, LocationName.ShanYuGetBonus, LocationName.HiddenDragon, LocationName.GoofyShanYu, - ] - lod_Region = create_region(world, player, active_locations, RegionName.LoD_Region, - lod_Region_locations) - lod2_Region_locations = [ + LocationName.ShanYuEventLocation + ], + RegionName.LoD2: [], + RegionName.AnsemRiku: [ LocationName.ThroneRoomTornPages, LocationName.ThroneRoomPalaceMap, LocationName.ThroneRoomAPBoost, @@ -63,13 +52,18 @@ def create_regions(world, player: int, active_locations): LocationName.ThroneRoomOgreShield, LocationName.ThroneRoomMythrilCrystal, LocationName.ThroneRoomOrichalcum, + LocationName.AnsemRikuEventLocation, + ], + RegionName.StormRider: [ LocationName.StormRider, - LocationName.XigbarDataDefenseBoost, LocationName.GoofyStormRider, - ] - lod2_Region = create_region(world, player, active_locations, RegionName.LoD2_Region, - lod2_Region_locations) - ag_region_locations = [ + LocationName.StormRiderEventLocation + ], + RegionName.DataXigbar: [ + LocationName.XigbarDataDefenseBoost, + LocationName.DataXigbarEventLocation + ], + RegionName.Ag: [ LocationName.AgrabahMap, LocationName.AgrabahDarkShard, LocationName.AgrabahMythrilShard, @@ -97,30 +91,30 @@ def create_regions(world, player: int, active_locations): LocationName.TreasureRoom, LocationName.TreasureRoomAPBoost, LocationName.TreasureRoomSerenityGem, + LocationName.GoofyTreasureRoom, + LocationName.DonaldAbuEscort + ], + RegionName.TwinLords: [ LocationName.ElementalLords, LocationName.LampCharm, - LocationName.GoofyTreasureRoom, - LocationName.DonaldAbuEscort, - ] - ag_region = create_region(world, player, active_locations, RegionName.Ag_Region, - ag_region_locations) - ag2_region_locations = [ + LocationName.TwinLordsEventLocation + ], + RegionName.Ag2: [ LocationName.RuinedChamberTornPages, LocationName.RuinedChamberRuinsMap, + ], + RegionName.GenieJafar: [ LocationName.GenieJafar, LocationName.WishingLamp, - ] - ag2_region = create_region(world, player, active_locations, RegionName.Ag2_Region, - ag2_region_locations) - lexaeus_region_locations = [ + LocationName.GenieJafarEventLocation, + ], + RegionName.DataLexaeus: [ LocationName.LexaeusBonus, LocationName.LexaeusASStrengthBeyondStrength, LocationName.LexaeusDataLostIllusion, - ] - lexaeus_region = create_region(world, player, active_locations, RegionName.Lexaeus_Region, - lexaeus_region_locations) - - dc_region_locations = [ + LocationName.DataLexaeusEventLocation + ], + RegionName.Dc: [ LocationName.DCCourtyardMythrilShard, LocationName.DCCourtyardStarRecipe, LocationName.DCCourtyardAPBoost, @@ -131,74 +125,65 @@ def create_regions(world, player: int, active_locations): LocationName.LibraryTornPages, LocationName.DisneyCastleMap, LocationName.MinnieEscort, - LocationName.MinnieEscortGetBonus, - ] - dc_region = create_region(world, player, active_locations, RegionName.Dc_Region, - dc_region_locations) - tr_region_locations = [ + LocationName.MinnieEscortGetBonus + ], + RegionName.Tr: [ LocationName.CornerstoneHillMap, LocationName.CornerstoneHillFrostShard, LocationName.PierMythrilShard, LocationName.PierHiPotion, + ], + RegionName.OldPete: [ LocationName.WaterwayMythrilStone, LocationName.WaterwayAPBoost, LocationName.WaterwayFrostStone, LocationName.WindowofTimeMap, LocationName.BoatPete, + LocationName.DonaldBoatPete, + LocationName.DonaldBoatPeteGetBonus, + LocationName.OldPeteEventLocation, + ], + RegionName.FuturePete: [ LocationName.FuturePete, LocationName.FuturePeteGetBonus, LocationName.Monochrome, LocationName.WisdomForm, - LocationName.DonaldBoatPete, - LocationName.DonaldBoatPeteGetBonus, LocationName.GoofyFuturePete, - ] - tr_region = create_region(world, player, active_locations, RegionName.Tr_Region, - tr_region_locations) - marluxia_region_locations = [ + LocationName.FuturePeteEventLocation + ], + RegionName.DataMarluxia: [ LocationName.MarluxiaGetBonus, LocationName.MarluxiaASEternalBlossom, LocationName.MarluxiaDataLostIllusion, - ] - marluxia_region = create_region(world, player, active_locations, RegionName.Marluxia_Region, - marluxia_region_locations) - terra_region_locations = [ + LocationName.DataMarluxiaEventLocation + ], + RegionName.Terra: [ LocationName.LingeringWillBonus, LocationName.LingeringWillProofofConnection, LocationName.LingeringWillManifestIllusion, - ] - terra_region = create_region(world, player, active_locations, RegionName.Terra_Region, - terra_region_locations) - - hundred_acre1_region_locations = [ + LocationName.TerraEventLocation + ], + RegionName.Ha1: [ LocationName.PoohsHouse100AcreWoodMap, LocationName.PoohsHouseAPBoost, - LocationName.PoohsHouseMythrilStone, - ] - hundred_acre1_region = create_region(world, player, active_locations, RegionName.HundredAcre1_Region, - hundred_acre1_region_locations) - hundred_acre2_region_locations = [ + LocationName.PoohsHouseMythrilStone + ], + RegionName.Ha2: [ LocationName.PigletsHouseDefenseBoost, LocationName.PigletsHouseAPBoost, - LocationName.PigletsHouseMythrilGem, - ] - hundred_acre2_region = create_region(world, player, active_locations, RegionName.HundredAcre2_Region, - hundred_acre2_region_locations) - hundred_acre3_region_locations = [ + LocationName.PigletsHouseMythrilGem + ], + RegionName.Ha3: [ LocationName.RabbitsHouseDrawRing, LocationName.RabbitsHouseMythrilCrystal, LocationName.RabbitsHouseAPBoost, - ] - hundred_acre3_region = create_region(world, player, active_locations, RegionName.HundredAcre3_Region, - hundred_acre3_region_locations) - hundred_acre4_region_locations = [ + ], + RegionName.Ha4: [ LocationName.KangasHouseMagicBoost, LocationName.KangasHouseAPBoost, LocationName.KangasHouseOrichalcum, - ] - hundred_acre4_region = create_region(world, player, active_locations, RegionName.HundredAcre4_Region, - hundred_acre4_region_locations) - hundred_acre5_region_locations = [ + ], + RegionName.Ha5: [ LocationName.SpookyCaveMythrilGem, LocationName.SpookyCaveAPBoost, LocationName.SpookyCaveOrichalcum, @@ -206,19 +191,15 @@ def create_regions(world, player: int, active_locations): LocationName.SpookyCaveMythrilCrystal, LocationName.SpookyCaveAPBoost2, LocationName.SweetMemories, - LocationName.SpookyCaveMap, - ] - hundred_acre5_region = create_region(world, player, active_locations, RegionName.HundredAcre5_Region, - hundred_acre5_region_locations) - hundred_acre6_region_locations = [ + LocationName.SpookyCaveMap + ], + RegionName.Ha6: [ LocationName.StarryHillCosmicRing, LocationName.StarryHillStyleRecipe, LocationName.StarryHillCureElement, - LocationName.StarryHillOrichalcumPlus, - ] - hundred_acre6_region = create_region(world, player, active_locations, RegionName.HundredAcre6_Region, - hundred_acre6_region_locations) - pr_region_locations = [ + LocationName.StarryHillOrichalcumPlus + ], + RegionName.Pr: [ LocationName.RampartNavalMap, LocationName.RampartMythrilStone, LocationName.RampartDarkShard, @@ -236,17 +217,20 @@ def create_regions(world, player: int, active_locations): LocationName.MoonlightNookMythrilShard, LocationName.MoonlightNookSerenityGem, LocationName.MoonlightNookPowerStone, + LocationName.DonaldBoatFight, + LocationName.GoofyInterceptorBarrels, + + ], + RegionName.Barbosa: [ LocationName.Barbossa, LocationName.BarbossaGetBonus, LocationName.FollowtheWind, - LocationName.DonaldBoatFight, LocationName.GoofyBarbossa, LocationName.GoofyBarbossaGetBonus, - LocationName.GoofyInterceptorBarrels, - ] - pr_region = create_region(world, player, active_locations, RegionName.Pr_Region, - pr_region_locations) - pr2_region_locations = [ + LocationName.BarbosaEventLocation, + ], + RegionName.Pr2: [], + RegionName.GrimReaper1: [ LocationName.GrimReaper1, LocationName.InterceptorsHoldFeatherCharm, LocationName.SeadriftKeepAPBoost, @@ -258,19 +242,19 @@ def create_regions(world, player: int, active_locations): LocationName.SeadriftRowCursedMedallion, LocationName.SeadriftRowShipGraveyardMap, LocationName.GoofyGrimReaper1, - - ] - pr2_region = create_region(world, player, active_locations, RegionName.Pr2_Region, - pr2_region_locations) - gr2_region_locations = [ + LocationName.GrimReaper1EventLocation, + ], + RegionName.GrimReaper2: [ LocationName.DonaladGrimReaper2, LocationName.GrimReaper2, LocationName.SecretAnsemReport6, + LocationName.GrimReaper2EventLocation, + ], + RegionName.DataLuxord: [ LocationName.LuxordDataAPBoost, - ] - gr2_region = create_region(world, player, active_locations, RegionName.Gr2_Region, - gr2_region_locations) - oc_region_locations = [ + LocationName.DataLuxordEventLocation + ], + RegionName.Oc: [ LocationName.PassageMythrilShard, LocationName.PassageMythrilStone, LocationName.PassageEther, @@ -278,6 +262,8 @@ def create_regions(world, player: int, active_locations): LocationName.PassageHiPotion, LocationName.InnerChamberUnderworldMap, LocationName.InnerChamberMythrilShard, + ], + RegionName.Cerberus: [ LocationName.Cerberus, LocationName.ColiseumMap, LocationName.Urns, @@ -297,56 +283,61 @@ def create_regions(world, player: int, active_locations): LocationName.TheLockCavernsMap, LocationName.TheLockMythrilShard, LocationName.TheLockAPBoost, + LocationName.CerberusEventLocation + ], + RegionName.OlympusPete: [ LocationName.PeteOC, + LocationName.DonaldDemyxOC, + LocationName.GoofyPeteOC, + LocationName.OlympusPeteEventLocation + ], + RegionName.Hydra: [ LocationName.Hydra, LocationName.HydraGetBonus, LocationName.HerosCrest, - LocationName.DonaldDemyxOC, - LocationName.GoofyPeteOC, - ] - oc_region = create_region(world, player, active_locations, RegionName.Oc_Region, - oc_region_locations) - oc2_region_locations = [ + LocationName.HydraEventLocation + ], + RegionName.Oc2: [ LocationName.AuronsStatue, + ], + RegionName.Hades: [ LocationName.Hades, LocationName.HadesGetBonus, LocationName.GuardianSoul, - - ] - oc2_region = create_region(world, player, active_locations, RegionName.Oc2_Region, - oc2_region_locations) - oc2_pain_and_panic_locations = [ + LocationName.HadesEventLocation + ], + RegionName.OcPainAndPanicCup: [ LocationName.ProtectBeltPainandPanicCup, LocationName.SerenityGemPainandPanicCup, - ] - oc2_titan_locations = [ - LocationName.GenjiShieldTitanCup, - LocationName.SkillfulRingTitanCup, - ] - oc2_cerberus_locations = [ + LocationName.OcPainAndPanicCupEventLocation + ], + RegionName.OcCerberusCup: [ LocationName.RisingDragonCerberusCup, LocationName.SerenityCrystalCerberusCup, - ] - oc2_gof_cup_locations = [ + LocationName.OcCerberusCupEventLocation + ], + RegionName.Oc2TitanCup: [ + LocationName.GenjiShieldTitanCup, + LocationName.SkillfulRingTitanCup, + LocationName.Oc2TitanCupEventLocation + ], + RegionName.Oc2GofCup: [ LocationName.FatalCrestGoddessofFateCup, LocationName.OrichalcumPlusGoddessofFateCup, + LocationName.Oc2GofCupEventLocation, + ], + RegionName.HadesCups: [ LocationName.HadesCupTrophyParadoxCups, - ] - zexion_region_locations = [ + LocationName.HadesCupEventLocations + ], + RegionName.DataZexion: [ LocationName.ZexionBonus, LocationName.ZexionASBookofShadows, LocationName.ZexionDataLostIllusion, LocationName.GoofyZexion, - ] - oc2_pain_and_panic_cup = create_region(world, player, active_locations, RegionName.Oc2_pain_and_panic_Region, - oc2_pain_and_panic_locations) - oc2_titan_cup = create_region(world, player, active_locations, RegionName.Oc2_titan_Region, oc2_titan_locations) - oc2_cerberus_cup = create_region(world, player, active_locations, RegionName.Oc2_cerberus_Region, - oc2_cerberus_locations) - oc2_gof_cup = create_region(world, player, active_locations, RegionName.Oc2_gof_Region, oc2_gof_cup_locations) - zexion_region = create_region(world, player, active_locations, RegionName.Zexion_Region, zexion_region_locations) - - bc_region_locations = [ + LocationName.DataZexionEventLocation + ], + RegionName.Bc: [ LocationName.BCCourtyardAPBoost, LocationName.BCCourtyardHiPotion, LocationName.BCCourtyardMythrilShard, @@ -359,6 +350,8 @@ def create_regions(world, player: int, active_locations): LocationName.TheWestHallMythrilShard2, LocationName.TheWestHallBrightStone, LocationName.TheWestHallMythrilShard, + ], + RegionName.Thresholder: [ LocationName.Thresholder, LocationName.DungeonBasementMap, LocationName.DungeonAPBoost, @@ -368,33 +361,37 @@ def create_regions(world, player: int, active_locations): LocationName.TheWestHallAPBoostPostDungeon, LocationName.TheWestWingMythrilShard, LocationName.TheWestWingTent, + LocationName.DonaldThresholder, + LocationName.ThresholderEventLocation + ], + RegionName.Beast: [ LocationName.Beast, LocationName.TheBeastsRoomBlazingShard, + LocationName.GoofyBeast, + LocationName.BeastEventLocation + ], + RegionName.DarkThorn: [ LocationName.DarkThorn, LocationName.DarkThornGetBonus, LocationName.DarkThornCureElement, - LocationName.DonaldThresholder, - LocationName.GoofyBeast, - ] - bc_region = create_region(world, player, active_locations, RegionName.Bc_Region, - bc_region_locations) - bc2_region_locations = [ + LocationName.DarkThornEventLocation, + ], + RegionName.Bc2: [ LocationName.RumblingRose, - LocationName.CastleWallsMap, - - ] - bc2_region = create_region(world, player, active_locations, RegionName.Bc2_Region, - bc2_region_locations) - xaldin_region_locations = [ + LocationName.CastleWallsMap + ], + RegionName.Xaldin: [ LocationName.Xaldin, LocationName.XaldinGetBonus, LocationName.DonaldXaldinGetBonus, LocationName.SecretAnsemReport4, + LocationName.XaldinEventLocation + ], + RegionName.DataXaldin: [ LocationName.XaldinDataDefenseBoost, - ] - xaldin_region = create_region(world, player, active_locations, RegionName.Xaldin_Region, - xaldin_region_locations) - sp_region_locations = [ + LocationName.DataXaldinEventLocation + ], + RegionName.Sp: [ LocationName.PitCellAreaMap, LocationName.PitCellMythrilCrystal, LocationName.CanyonDarkCrystal, @@ -406,41 +403,35 @@ def create_regions(world, player: int, active_locations): LocationName.HallwayAPBoost, LocationName.CommunicationsRoomIOTowerMap, LocationName.CommunicationsRoomGaiaBelt, + LocationName.DonaldScreens, + ], + RegionName.HostileProgram: [ LocationName.HostileProgram, LocationName.HostileProgramGetBonus, LocationName.PhotonDebugger, - LocationName.DonaldScreens, LocationName.GoofyHostileProgram, - - ] - sp_region = create_region(world, player, active_locations, RegionName.Sp_Region, - sp_region_locations) - sp2_region_locations = [ + LocationName.HostileProgramEventLocation + ], + RegionName.Sp2: [ LocationName.SolarSailer, LocationName.CentralComputerCoreAPBoost, LocationName.CentralComputerCoreOrichalcumPlus, LocationName.CentralComputerCoreCosmicArts, LocationName.CentralComputerCoreMap, - - LocationName.DonaldSolarSailer, - ] - - sp2_region = create_region(world, player, active_locations, RegionName.Sp2_Region, - sp2_region_locations) - mcp_region_locations = [ + LocationName.DonaldSolarSailer + ], + RegionName.Mcp: [ LocationName.MCP, LocationName.MCPGetBonus, - ] - mcp_region = create_region(world, player, active_locations, RegionName.Mcp_Region, - mcp_region_locations) - larxene_region_locations = [ + LocationName.McpEventLocation + ], + RegionName.DataLarxene: [ LocationName.LarxeneBonus, LocationName.LarxeneASCloakedThunder, LocationName.LarxeneDataLostIllusion, - ] - larxene_region = create_region(world, player, active_locations, RegionName.Larxene_Region, - larxene_region_locations) - ht_region_locations = [ + LocationName.DataLarxeneEventLocation + ], + RegionName.Ht: [ LocationName.GraveyardMythrilShard, LocationName.GraveyardSerenityGem, LocationName.FinklesteinsLabHalloweenTownMap, @@ -455,34 +446,37 @@ def create_regions(world, player: int, active_locations): LocationName.CandyCaneLaneMythrilStone, LocationName.SantasHouseChristmasTownMap, LocationName.SantasHouseAPBoost, + ], + RegionName.PrisonKeeper: [ LocationName.PrisonKeeper, + LocationName.DonaldPrisonKeeper, + LocationName.PrisonKeeperEventLocation, + ], + RegionName.OogieBoogie: [ LocationName.OogieBoogie, LocationName.OogieBoogieMagnetElement, - LocationName.DonaldPrisonKeeper, LocationName.GoofyOogieBoogie, - ] - ht_region = create_region(world, player, active_locations, RegionName.Ht_Region, - ht_region_locations) - ht2_region_locations = [ + LocationName.OogieBoogieEventLocation + ], + RegionName.Ht2: [ LocationName.Lock, LocationName.Present, LocationName.DecoyPresents, + LocationName.GoofyLock + ], + RegionName.Experiment: [ LocationName.Experiment, LocationName.DecisivePumpkin, - LocationName.DonaldExperiment, - LocationName.GoofyLock, - ] - ht2_region = create_region(world, player, active_locations, RegionName.Ht2_Region, - ht2_region_locations) - vexen_region_locations = [ + LocationName.ExperimentEventLocation, + ], + RegionName.DataVexen: [ LocationName.VexenBonus, LocationName.VexenASRoadtoDiscovery, LocationName.VexenDataLostIllusion, - ] - vexen_region = create_region(world, player, active_locations, RegionName.Vexen_Region, - vexen_region_locations) - hb_region_locations = [ + LocationName.DataVexenEventLocation + ], + RegionName.Hb: [ LocationName.MarketplaceMap, LocationName.BoroughDriveRecovery, LocationName.BoroughAPBoost, @@ -493,11 +487,9 @@ def create_regions(world, player: int, active_locations): LocationName.MerlinsHouseBlizzardElement, LocationName.Bailey, LocationName.BaileySecretAnsemReport7, - LocationName.BaseballCharm, - ] - hb_region = create_region(world, player, active_locations, RegionName.Hb_Region, - hb_region_locations) - hb2_region_locations = [ + LocationName.BaseballCharm + ], + RegionName.Hb2: [ LocationName.PosternCastlePerimeterMap, LocationName.PosternMythrilGem, LocationName.PosternAPBoost, @@ -511,18 +503,9 @@ def create_regions(world, player: int, active_locations): LocationName.AnsemsStudyUkuleleCharm, LocationName.RestorationSiteMoonRecipe, LocationName.RestorationSiteAPBoost, - LocationName.CoRDepthsAPBoost, - LocationName.CoRDepthsPowerCrystal, - LocationName.CoRDepthsFrostCrystal, - LocationName.CoRDepthsManifestIllusion, - LocationName.CoRDepthsAPBoost2, - LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap, - LocationName.CoRMineshaftLowerLevelAPBoost, + ], + RegionName.HBDemyx: [ LocationName.DonaldDemyxHBGetBonus, - ] - hb2_region = create_region(world, player, active_locations, RegionName.Hb2_Region, - hb2_region_locations) - onek_region_locations = [ LocationName.DemyxHB, LocationName.DemyxHBGetBonus, LocationName.FFFightsCureElement, @@ -530,30 +513,41 @@ def create_regions(world, player: int, active_locations): LocationName.CrystalFissureTheGreatMawMap, LocationName.CrystalFissureEnergyCrystal, LocationName.CrystalFissureAPBoost, + LocationName.HBDemyxEventLocation, + ], + RegionName.ThousandHeartless: [ LocationName.ThousandHeartless, LocationName.ThousandHeartlessSecretAnsemReport1, LocationName.ThousandHeartlessIceCream, LocationName.ThousandHeartlessPicture, LocationName.PosternGullWing, LocationName.HeartlessManufactoryCosmicChain, + LocationName.ThousandHeartlessEventLocation, + ], + RegionName.DataDemyx: [ LocationName.DemyxDataAPBoost, - ] - onek_region = create_region(world, player, active_locations, RegionName.ThousandHeartless_Region, - onek_region_locations) - mushroom_region_locations = [ + LocationName.DataDemyxEventLocation, + ], + RegionName.Mushroom13: [ LocationName.WinnersProof, LocationName.ProofofPeace, - ] - mushroom_region = create_region(world, player, active_locations, RegionName.Mushroom13_Region, - mushroom_region_locations) - sephi_region_locations = [ + LocationName.Mushroom13EventLocation, + ], + RegionName.Sephi: [ LocationName.SephirothBonus, LocationName.SephirothFenrir, - ] - sephi_region = create_region(world, player, active_locations, RegionName.Sephi_Region, - sephi_region_locations) - - cor_region_locations = [ + LocationName.SephiEventLocation + ], + RegionName.CoR: [ + LocationName.CoRDepthsAPBoost, + LocationName.CoRDepthsPowerCrystal, + LocationName.CoRDepthsFrostCrystal, + LocationName.CoRDepthsManifestIllusion, + LocationName.CoRDepthsAPBoost2, + LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap, + LocationName.CoRMineshaftLowerLevelAPBoost, + ], + RegionName.CorFirstFight: [ LocationName.CoRDepthsUpperLevelRemembranceGem, LocationName.CoRMiningAreaSerenityGem, LocationName.CoRMiningAreaAPBoost, @@ -561,22 +555,23 @@ def create_regions(world, player: int, active_locations): LocationName.CoRMiningAreaManifestIllusion, LocationName.CoRMiningAreaSerenityGem2, LocationName.CoRMiningAreaDarkRemembranceMap, + LocationName.CorFirstFightEventLocation, + ], + RegionName.CorSecondFight: [ LocationName.CoRMineshaftMidLevelPowerBoost, LocationName.CoREngineChamberSerenityCrystal, LocationName.CoREngineChamberRemembranceCrystal, LocationName.CoREngineChamberAPBoost, LocationName.CoREngineChamberManifestIllusion, LocationName.CoRMineshaftUpperLevelMagicBoost, - ] - cor_region = create_region(world, player, active_locations, RegionName.CoR_Region, - cor_region_locations) - transport_region_locations = [ - LocationName.CoRMineshaftUpperLevelAPBoost, + LocationName.CorSecondFightEventLocation, + ], + RegionName.Transport: [ + LocationName.CoRMineshaftUpperLevelAPBoost, # last chest LocationName.TransporttoRemembrance, - ] - transport_region = create_region(world, player, active_locations, RegionName.Transport_Region, - transport_region_locations) - pl_region_locations = [ + LocationName.TransportEventLocation, + ], + RegionName.Pl: [ LocationName.GorgeSavannahMap, LocationName.GorgeDarkGem, LocationName.GorgeMythrilStone, @@ -604,31 +599,40 @@ def create_regions(world, player: int, active_locations): LocationName.OasisAPBoost, LocationName.CircleofLife, LocationName.Hyenas1, + + LocationName.GoofyHyenas1 + ], + RegionName.Scar: [ LocationName.Scar, LocationName.ScarFireElement, LocationName.DonaldScar, - LocationName.GoofyHyenas1, - - ] - pl_region = create_region(world, player, active_locations, RegionName.Pl_Region, - pl_region_locations) - pl2_region_locations = [ + LocationName.ScarEventLocation, + ], + RegionName.Pl2: [ LocationName.Hyenas2, + LocationName.GoofyHyenas2 + ], + RegionName.GroundShaker: [ LocationName.Groundshaker, LocationName.GroundshakerGetBonus, + LocationName.GroundShakerEventLocation, + ], + RegionName.DataSaix: [ LocationName.SaixDataDefenseBoost, - LocationName.GoofyHyenas2, - ] - pl2_region = create_region(world, player, active_locations, RegionName.Pl2_Region, - pl2_region_locations) - - stt_region_locations = [ + LocationName.DataSaixEventLocation + ], + RegionName.Stt: [ LocationName.TwilightTownMap, LocationName.MunnyPouchOlette, LocationName.StationDusks, LocationName.StationofSerenityPotion, LocationName.StationofCallingPotion, + ], + RegionName.TwilightThorn: [ LocationName.TwilightThorn, + LocationName.TwilightThornEventLocation + ], + RegionName.Axel1: [ LocationName.Axel1, LocationName.JunkChampionBelt, LocationName.JunkMedal, @@ -648,14 +652,18 @@ def create_regions(world, player: int, active_locations): LocationName.NaminesSketches, LocationName.MansionMap, LocationName.MansionLibraryHiPotion, + LocationName.Axel1EventLocation + ], + RegionName.Axel2: [ LocationName.Axel2, LocationName.MansionBasementCorridorHiPotion, + LocationName.Axel2EventLocation + ], + RegionName.DataRoxas: [ LocationName.RoxasDataMagicBoost, - ] - stt_region = create_region(world, player, active_locations, RegionName.STT_Region, - stt_region_locations) - - tt_region_locations = [ + LocationName.DataRoxasEventLocation + ], + RegionName.Tt: [ LocationName.OldMansionPotion, LocationName.OldMansionMythrilShard, LocationName.TheWoodsPotion, @@ -682,18 +690,14 @@ def create_regions(world, player: int, active_locations): LocationName.SorcerersLoftTowerMap, LocationName.TowerWardrobeMythrilStone, LocationName.StarSeeker, - LocationName.ValorForm, - ] - tt_region = create_region(world, player, active_locations, RegionName.TT_Region, - tt_region_locations) - tt2_region_locations = [ + LocationName.ValorForm + ], + RegionName.Tt2: [ LocationName.SeifersTrophy, LocationName.Oathkeeper, - LocationName.LimitForm, - ] - tt2_region = create_region(world, player, active_locations, RegionName.TT2_Region, - tt2_region_locations) - tt3_region_locations = [ + LocationName.LimitForm + ], + RegionName.Tt3: [ LocationName.UndergroundConcourseMythrilGem, LocationName.UndergroundConcourseAPBoost, LocationName.UndergroundConcourseMythrilCrystal, @@ -715,22 +719,19 @@ def create_regions(world, player: int, active_locations): LocationName.MansionBasementCorridorUltimateRecipe, LocationName.BetwixtandBetween, LocationName.BetwixtandBetweenBondofFlame, + LocationName.DonaldMansionNobodies + ], + RegionName.DataAxel: [ LocationName.AxelDataMagicBoost, - LocationName.DonaldMansionNobodies, - ] - tt3_region = create_region(world, player, active_locations, RegionName.TT3_Region, - tt3_region_locations) - - twtnw_region_locations = [ + LocationName.DataAxelEventLocation, + ], + RegionName.Twtnw: [ LocationName.FragmentCrossingMythrilStone, LocationName.FragmentCrossingMythrilCrystal, LocationName.FragmentCrossingAPBoost, - LocationName.FragmentCrossingOrichalcum, - ] - - twtnw_region = create_region(world, player, active_locations, RegionName.Twtnw_Region, - twtnw_region_locations) - twtnw_postroxas_region_locations = [ + LocationName.FragmentCrossingOrichalcum + ], + RegionName.Roxas: [ LocationName.Roxas, LocationName.RoxasGetBonus, LocationName.RoxasSecretAnsemReport8, @@ -743,11 +744,9 @@ def create_regions(world, player: int, active_locations): LocationName.NothingsCallMythrilGem, LocationName.NothingsCallOrichalcum, LocationName.TwilightsViewCosmicBelt, - - ] - twtnw_postroxas_region = create_region(world, player, active_locations, RegionName.Twtnw_PostRoxas, - twtnw_postroxas_region_locations) - twtnw_postxigbar_region_locations = [ + LocationName.RoxasEventLocation + ], + RegionName.Xigbar: [ LocationName.XigbarBonus, LocationName.XigbarSecretAnsemReport3, LocationName.NaughtsSkywayMythrilGem, @@ -755,80 +754,100 @@ def create_regions(world, player: int, active_locations): LocationName.NaughtsSkywayMythrilCrystal, LocationName.Oblivion, LocationName.CastleThatNeverWasMap, + LocationName.XigbarEventLocation, + ], + RegionName.Luxord: [ LocationName.Luxord, LocationName.LuxordGetBonus, LocationName.LuxordSecretAnsemReport9, - ] - twtnw_postxigbar_region = create_region(world, player, active_locations, RegionName.Twtnw_PostXigbar, - twtnw_postxigbar_region_locations) - twtnw2_region_locations = [ + LocationName.LuxordEventLocation, + ], + RegionName.Saix: [ LocationName.SaixBonus, LocationName.SaixSecretAnsemReport12, + LocationName.SaixEventLocation, + ], + RegionName.Twtnw2: [ LocationName.PreXemnas1SecretAnsemReport11, LocationName.RuinandCreationsPassageMythrilStone, LocationName.RuinandCreationsPassageAPBoost, LocationName.RuinandCreationsPassageMythrilCrystal, - LocationName.RuinandCreationsPassageOrichalcum, + LocationName.RuinandCreationsPassageOrichalcum + ], + RegionName.Xemnas: [ LocationName.Xemnas1, LocationName.Xemnas1GetBonus, LocationName.Xemnas1SecretAnsemReport13, - LocationName.FinalXemnas, + LocationName.XemnasEventLocation + + ], + RegionName.ArmoredXemnas: [ + LocationName.ArmoredXemnasEventLocation + ], + RegionName.ArmoredXemnas2: [ + LocationName.ArmoredXemnas2EventLocation + ], + RegionName.FinalXemnas: [ + LocationName.FinalXemnas + ], + RegionName.DataXemnas: [ LocationName.XemnasDataPowerBoost, - ] - twtnw2_region = create_region(world, player, active_locations, RegionName.Twtnw2_Region, - twtnw2_region_locations) + LocationName.DataXemnasEventLocation + ], + RegionName.AtlanticaSongOne: [ + LocationName.UnderseaKingdomMap + ], + RegionName.AtlanticaSongTwo: [ - valor_region_locations = [ + ], + RegionName.AtlanticaSongThree: [ + LocationName.MysteriousAbyss + ], + RegionName.AtlanticaSongFour: [ + LocationName.MusicalBlizzardElement, + LocationName.MusicalOrichalcumPlus + ], + RegionName.Valor: [ LocationName.Valorlvl2, LocationName.Valorlvl3, LocationName.Valorlvl4, LocationName.Valorlvl5, LocationName.Valorlvl6, - LocationName.Valorlvl7, - ] - valor_region = create_region(world, player, active_locations, RegionName.Valor_Region, - valor_region_locations) - wisdom_region_locations = [ + LocationName.Valorlvl7 + ], + RegionName.Wisdom: [ LocationName.Wisdomlvl2, LocationName.Wisdomlvl3, LocationName.Wisdomlvl4, LocationName.Wisdomlvl5, LocationName.Wisdomlvl6, - LocationName.Wisdomlvl7, - ] - wisdom_region = create_region(world, player, active_locations, RegionName.Wisdom_Region, - wisdom_region_locations) - limit_region_locations = [ + LocationName.Wisdomlvl7 + ], + RegionName.Limit: [ LocationName.Limitlvl2, LocationName.Limitlvl3, LocationName.Limitlvl4, LocationName.Limitlvl5, LocationName.Limitlvl6, - LocationName.Limitlvl7, - ] - limit_region = create_region(world, player, active_locations, RegionName.Limit_Region, - limit_region_locations) - master_region_locations = [ + LocationName.Limitlvl7 + ], + RegionName.Master: [ LocationName.Masterlvl2, LocationName.Masterlvl3, LocationName.Masterlvl4, LocationName.Masterlvl5, LocationName.Masterlvl6, - LocationName.Masterlvl7, - ] - master_region = create_region(world, player, active_locations, RegionName.Master_Region, - master_region_locations) - final_region_locations = [ + LocationName.Masterlvl7 + ], + RegionName.Final: [ LocationName.Finallvl2, LocationName.Finallvl3, LocationName.Finallvl4, LocationName.Finallvl5, LocationName.Finallvl6, - LocationName.Finallvl7, - ] - final_region = create_region(world, player, active_locations, RegionName.Final_Region, - final_region_locations) - keyblade_region_locations = [ + LocationName.Finallvl7 + ], + RegionName.Keyblade: [ LocationName.FAKESlot, LocationName.DetectionSaberSlot, LocationName.EdgeofUltimaSlot, @@ -887,356 +906,256 @@ def create_regions(world, player: int, active_locations): LocationName.NobodyGuard, LocationName.OgreShield, LocationName.SaveTheKing2, - LocationName.UltimateMushroom, - ] - keyblade_region = create_region(world, player, active_locations, RegionName.Keyblade_Region, - keyblade_region_locations) + LocationName.UltimateMushroom + ], +} +level_region_list = [ + RegionName.LevelsVS1, + RegionName.LevelsVS3, + RegionName.LevelsVS6, + RegionName.LevelsVS9, + RegionName.LevelsVS12, + RegionName.LevelsVS15, + RegionName.LevelsVS18, + RegionName.LevelsVS21, + RegionName.LevelsVS24, + RegionName.LevelsVS26, +] + - world.regions += [ - lod_Region, - lod2_Region, - ag_region, - ag2_region, - lexaeus_region, - dc_region, - tr_region, - terra_region, - marluxia_region, - hundred_acre1_region, - hundred_acre2_region, - hundred_acre3_region, - hundred_acre4_region, - hundred_acre5_region, - hundred_acre6_region, - pr_region, - pr2_region, - gr2_region, - oc_region, - oc2_region, - oc2_pain_and_panic_cup, - oc2_titan_cup, - oc2_cerberus_cup, - oc2_gof_cup, - zexion_region, - bc_region, - bc2_region, - xaldin_region, - sp_region, - sp2_region, - mcp_region, - larxene_region, - ht_region, - ht2_region, - vexen_region, - hb_region, - hb2_region, - onek_region, - mushroom_region, - sephi_region, - cor_region, - transport_region, - pl_region, - pl2_region, - stt_region, - tt_region, - tt2_region, - tt3_region, - twtnw_region, - twtnw_postroxas_region, - twtnw_postxigbar_region, - twtnw2_region, - goa_region, - menu_region, - valor_region, - wisdom_region, - limit_region, - master_region, - final_region, - keyblade_region, - ] +def create_regions(self): # Level region depends on level depth. # for every 5 levels there should be +3 visit locking - levelVL1 = [] - levelVL3 = [] - levelVL6 = [] - levelVL9 = [] - levelVL12 = [] - levelVL15 = [] - levelVL18 = [] - levelVL21 = [] - levelVL24 = [] - levelVL26 = [] # level 50 - if world.LevelDepth[player] == "level_50": - levelVL1 = [LocationName.Lvl2, LocationName.Lvl4, LocationName.Lvl7, LocationName.Lvl9, LocationName.Lvl10] - levelVL3 = [LocationName.Lvl12, LocationName.Lvl14, LocationName.Lvl15, LocationName.Lvl17, - LocationName.Lvl20, ] - levelVL6 = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28, LocationName.Lvl30] - levelVL9 = [LocationName.Lvl32, LocationName.Lvl34, LocationName.Lvl36, LocationName.Lvl39, LocationName.Lvl41] - levelVL12 = [LocationName.Lvl44, LocationName.Lvl46, LocationName.Lvl48] - levelVL15 = [LocationName.Lvl50] + multiworld = self.multiworld + player = self.player + active_locations = self.location_name_to_id + + for level_region_name in level_region_list: + KH2REGIONS[level_region_name] = [] + if multiworld.LevelDepth[player] == "level_50": + KH2REGIONS[RegionName.LevelsVS1] = [LocationName.Lvl2, LocationName.Lvl4, LocationName.Lvl7, LocationName.Lvl9, + LocationName.Lvl10] + KH2REGIONS[RegionName.LevelsVS3] = [LocationName.Lvl12, LocationName.Lvl14, LocationName.Lvl15, + LocationName.Lvl17, + LocationName.Lvl20] + KH2REGIONS[RegionName.LevelsVS6] = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28, + LocationName.Lvl30] + KH2REGIONS[RegionName.LevelsVS9] = [LocationName.Lvl32, LocationName.Lvl34, LocationName.Lvl36, + LocationName.Lvl39, LocationName.Lvl41] + KH2REGIONS[RegionName.LevelsVS12] = [LocationName.Lvl44, LocationName.Lvl46, LocationName.Lvl48] + KH2REGIONS[RegionName.LevelsVS15] = [LocationName.Lvl50] + # level 99 - elif world.LevelDepth[player] == "level_99": - levelVL1 = [LocationName.Lvl7, LocationName.Lvl9, ] - levelVL3 = [LocationName.Lvl12, LocationName.Lvl15, LocationName.Lvl17, LocationName.Lvl20] - levelVL6 = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28] - levelVL9 = [LocationName.Lvl31, LocationName.Lvl33, LocationName.Lvl36, LocationName.Lvl39] - levelVL12 = [LocationName.Lvl41, LocationName.Lvl44, LocationName.Lvl47, LocationName.Lvl49] - levelVL15 = [LocationName.Lvl53, LocationName.Lvl59] - levelVL18 = [LocationName.Lvl65] - levelVL21 = [LocationName.Lvl73] - levelVL24 = [LocationName.Lvl85] - levelVL26 = [LocationName.Lvl99] + elif multiworld.LevelDepth[player] == "level_99": + KH2REGIONS[RegionName.LevelsVS1] = [LocationName.Lvl7, LocationName.Lvl9] + KH2REGIONS[RegionName.LevelsVS3] = [LocationName.Lvl12, LocationName.Lvl15, LocationName.Lvl17, + LocationName.Lvl20] + KH2REGIONS[RegionName.LevelsVS6] = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28] + KH2REGIONS[RegionName.LevelsVS9] = [LocationName.Lvl31, LocationName.Lvl33, LocationName.Lvl36, + LocationName.Lvl39] + KH2REGIONS[RegionName.LevelsVS12] = [LocationName.Lvl41, LocationName.Lvl44, LocationName.Lvl47, + LocationName.Lvl49] + KH2REGIONS[RegionName.LevelsVS15] = [LocationName.Lvl53, LocationName.Lvl59] + KH2REGIONS[RegionName.LevelsVS18] = [LocationName.Lvl65] + KH2REGIONS[RegionName.LevelsVS21] = [LocationName.Lvl73] + KH2REGIONS[RegionName.LevelsVS24] = [LocationName.Lvl85] + KH2REGIONS[RegionName.LevelsVS26] = [LocationName.Lvl99] # level sanity # has to be [] instead of {} for in - elif world.LevelDepth[player] in ["level_50_sanity", "level_99_sanity"]: - levelVL1 = [LocationName.Lvl2, LocationName.Lvl3, LocationName.Lvl4, LocationName.Lvl5, LocationName.Lvl6, - LocationName.Lvl7, LocationName.Lvl8, LocationName.Lvl9, LocationName.Lvl10] - levelVL3 = [LocationName.Lvl11, LocationName.Lvl12, LocationName.Lvl13, LocationName.Lvl14, LocationName.Lvl15, - LocationName.Lvl16, LocationName.Lvl17, LocationName.Lvl18, LocationName.Lvl19, LocationName.Lvl20] - levelVL6 = [LocationName.Lvl21, LocationName.Lvl22, LocationName.Lvl23, LocationName.Lvl24, LocationName.Lvl25, - LocationName.Lvl26, LocationName.Lvl27, LocationName.Lvl28, LocationName.Lvl29, LocationName.Lvl30] - levelVL9 = [LocationName.Lvl31, LocationName.Lvl32, LocationName.Lvl33, LocationName.Lvl34, LocationName.Lvl35, - LocationName.Lvl36, LocationName.Lvl37, LocationName.Lvl38, LocationName.Lvl39, LocationName.Lvl40] - levelVL12 = [LocationName.Lvl41, LocationName.Lvl42, LocationName.Lvl43, LocationName.Lvl44, LocationName.Lvl45, - LocationName.Lvl46, LocationName.Lvl47, LocationName.Lvl48, LocationName.Lvl49, LocationName.Lvl50] + elif multiworld.LevelDepth[player] in ["level_50_sanity", "level_99_sanity"]: + KH2REGIONS[RegionName.LevelsVS1] = [LocationName.Lvl2, LocationName.Lvl3, LocationName.Lvl4, LocationName.Lvl5, + LocationName.Lvl6, + LocationName.Lvl7, LocationName.Lvl8, LocationName.Lvl9, LocationName.Lvl10] + KH2REGIONS[RegionName.LevelsVS3] = [LocationName.Lvl11, LocationName.Lvl12, LocationName.Lvl13, + LocationName.Lvl14, LocationName.Lvl15, + LocationName.Lvl16, LocationName.Lvl17, LocationName.Lvl18, + LocationName.Lvl19, LocationName.Lvl20] + KH2REGIONS[RegionName.LevelsVS6] = [LocationName.Lvl21, LocationName.Lvl22, LocationName.Lvl23, + LocationName.Lvl24, LocationName.Lvl25, + LocationName.Lvl26, LocationName.Lvl27, LocationName.Lvl28, + LocationName.Lvl29, LocationName.Lvl30] + KH2REGIONS[RegionName.LevelsVS9] = [LocationName.Lvl31, LocationName.Lvl32, LocationName.Lvl33, + LocationName.Lvl34, LocationName.Lvl35, + LocationName.Lvl36, LocationName.Lvl37, LocationName.Lvl38, + LocationName.Lvl39, LocationName.Lvl40] + KH2REGIONS[RegionName.LevelsVS12] = [LocationName.Lvl41, LocationName.Lvl42, LocationName.Lvl43, + LocationName.Lvl44, LocationName.Lvl45, + LocationName.Lvl46, LocationName.Lvl47, LocationName.Lvl48, + LocationName.Lvl49, LocationName.Lvl50] # level 99 sanity - if world.LevelDepth[player] == "level_99_sanity": - levelVL15 = [LocationName.Lvl51, LocationName.Lvl52, LocationName.Lvl53, LocationName.Lvl54, - LocationName.Lvl55, LocationName.Lvl56, LocationName.Lvl57, LocationName.Lvl58, - LocationName.Lvl59, LocationName.Lvl60] - levelVL18 = [LocationName.Lvl61, LocationName.Lvl62, LocationName.Lvl63, LocationName.Lvl64, - LocationName.Lvl65, LocationName.Lvl66, LocationName.Lvl67, LocationName.Lvl68, - LocationName.Lvl69, LocationName.Lvl70] - levelVL21 = [LocationName.Lvl71, LocationName.Lvl72, LocationName.Lvl73, LocationName.Lvl74, - LocationName.Lvl75, LocationName.Lvl76, LocationName.Lvl77, LocationName.Lvl78, - LocationName.Lvl79, LocationName.Lvl80] - levelVL24 = [LocationName.Lvl81, LocationName.Lvl82, LocationName.Lvl83, LocationName.Lvl84, - LocationName.Lvl85, LocationName.Lvl86, LocationName.Lvl87, LocationName.Lvl88, - LocationName.Lvl89, LocationName.Lvl90] - levelVL26 = [LocationName.Lvl91, LocationName.Lvl92, LocationName.Lvl93, LocationName.Lvl94, - LocationName.Lvl95, LocationName.Lvl96, LocationName.Lvl97, LocationName.Lvl98, - LocationName.Lvl99] - - level_regionVL1 = create_region(world, player, active_locations, RegionName.LevelsVS1, - levelVL1) - level_regionVL3 = create_region(world, player, active_locations, RegionName.LevelsVS3, - levelVL3) - level_regionVL6 = create_region(world, player, active_locations, RegionName.LevelsVS6, - levelVL6) - level_regionVL9 = create_region(world, player, active_locations, RegionName.LevelsVS9, - levelVL9) - level_regionVL12 = create_region(world, player, active_locations, RegionName.LevelsVS12, - levelVL12) - level_regionVL15 = create_region(world, player, active_locations, RegionName.LevelsVS15, - levelVL15) - level_regionVL18 = create_region(world, player, active_locations, RegionName.LevelsVS18, - levelVL18) - level_regionVL21 = create_region(world, player, active_locations, RegionName.LevelsVS21, - levelVL21) - level_regionVL24 = create_region(world, player, active_locations, RegionName.LevelsVS24, - levelVL24) - level_regionVL26 = create_region(world, player, active_locations, RegionName.LevelsVS26, - levelVL26) - world.regions += [level_regionVL1, level_regionVL3, level_regionVL6, level_regionVL9, level_regionVL12, - level_regionVL15, level_regionVL18, level_regionVL21, level_regionVL24, level_regionVL26] + if multiworld.LevelDepth[player] == "level_99_sanity": + KH2REGIONS[RegionName.LevelsVS15] = [LocationName.Lvl51, LocationName.Lvl52, LocationName.Lvl53, + LocationName.Lvl54, + LocationName.Lvl55, LocationName.Lvl56, LocationName.Lvl57, + LocationName.Lvl58, + LocationName.Lvl59, LocationName.Lvl60] + KH2REGIONS[RegionName.LevelsVS18] = [LocationName.Lvl61, LocationName.Lvl62, LocationName.Lvl63, + LocationName.Lvl64, + LocationName.Lvl65, LocationName.Lvl66, LocationName.Lvl67, + LocationName.Lvl68, + LocationName.Lvl69, LocationName.Lvl70] + KH2REGIONS[RegionName.LevelsVS21] = [LocationName.Lvl71, LocationName.Lvl72, LocationName.Lvl73, + LocationName.Lvl74, + LocationName.Lvl75, LocationName.Lvl76, LocationName.Lvl77, + LocationName.Lvl78, + LocationName.Lvl79, LocationName.Lvl80] + KH2REGIONS[RegionName.LevelsVS24] = [LocationName.Lvl81, LocationName.Lvl82, LocationName.Lvl83, + LocationName.Lvl84, + LocationName.Lvl85, LocationName.Lvl86, LocationName.Lvl87, + LocationName.Lvl88, + LocationName.Lvl89, LocationName.Lvl90] + KH2REGIONS[RegionName.LevelsVS26] = [LocationName.Lvl91, LocationName.Lvl92, LocationName.Lvl93, + LocationName.Lvl94, + LocationName.Lvl95, LocationName.Lvl96, LocationName.Lvl97, + LocationName.Lvl98, LocationName.Lvl99] + KH2REGIONS[RegionName.Summon] = [] + if multiworld.SummonLevelLocationToggle[player]: + KH2REGIONS[RegionName.Summon] = [LocationName.Summonlvl2, + LocationName.Summonlvl3, + LocationName.Summonlvl4, + LocationName.Summonlvl5, + LocationName.Summonlvl6, + LocationName.Summonlvl7] + multiworld.regions += [create_region(multiworld, player, active_locations, region, locations) for region, locations in + KH2REGIONS.items()] + # fill the event locations with events + multiworld.worlds[player].item_name_to_id.update({event_name: None for event_name in Events_Table}) + for location, item in event_location_to_item.items(): + multiworld.get_location(location, player).place_locked_item( + multiworld.worlds[player].create_item(item)) -def connect_regions(world: MultiWorld, player: int): +def connect_regions(self): + multiworld = self.multiworld + player = self.player # connecting every first visit to the GoA + KH2RegionConnections: typing.Dict[str, typing.Set[str]] = { + "Menu": {RegionName.GoA}, + RegionName.GoA: {RegionName.Sp, RegionName.Pr, RegionName.Tt, RegionName.Oc, RegionName.Ht, + RegionName.LoD, + RegionName.Twtnw, RegionName.Bc, RegionName.Ag, RegionName.Pl, RegionName.Hb, + RegionName.Dc, RegionName.Stt, + RegionName.Ha1, RegionName.Keyblade, RegionName.LevelsVS1, + RegionName.Valor, RegionName.Wisdom, RegionName.Limit, RegionName.Master, + RegionName.Final, RegionName.Summon, RegionName.AtlanticaSongOne}, + RegionName.LoD: {RegionName.ShanYu}, + RegionName.ShanYu: {RegionName.LoD2}, + RegionName.LoD2: {RegionName.AnsemRiku}, + RegionName.AnsemRiku: {RegionName.StormRider}, + RegionName.StormRider: {RegionName.DataXigbar}, + RegionName.Ag: {RegionName.TwinLords}, + RegionName.TwinLords: {RegionName.Ag2}, + RegionName.Ag2: {RegionName.GenieJafar}, + RegionName.GenieJafar: {RegionName.DataLexaeus}, + RegionName.Dc: {RegionName.Tr}, + RegionName.Tr: {RegionName.OldPete}, + RegionName.OldPete: {RegionName.FuturePete}, + RegionName.FuturePete: {RegionName.Terra, RegionName.DataMarluxia}, + RegionName.Ha1: {RegionName.Ha2}, + RegionName.Ha2: {RegionName.Ha3}, + RegionName.Ha3: {RegionName.Ha4}, + RegionName.Ha4: {RegionName.Ha5}, + RegionName.Ha5: {RegionName.Ha6}, + RegionName.Pr: {RegionName.Barbosa}, + RegionName.Barbosa: {RegionName.Pr2}, + RegionName.Pr2: {RegionName.GrimReaper1}, + RegionName.GrimReaper1: {RegionName.GrimReaper2}, + RegionName.GrimReaper2: {RegionName.DataLuxord}, + RegionName.Oc: {RegionName.Cerberus}, + RegionName.Cerberus: {RegionName.OlympusPete}, + RegionName.OlympusPete: {RegionName.Hydra}, + RegionName.Hydra: {RegionName.OcPainAndPanicCup, RegionName.OcCerberusCup, RegionName.Oc2}, + RegionName.Oc2: {RegionName.Hades}, + RegionName.Hades: {RegionName.Oc2TitanCup, RegionName.Oc2GofCup, RegionName.DataZexion}, + RegionName.Oc2GofCup: {RegionName.HadesCups}, + RegionName.Bc: {RegionName.Thresholder}, + RegionName.Thresholder: {RegionName.Beast}, + RegionName.Beast: {RegionName.DarkThorn}, + RegionName.DarkThorn: {RegionName.Bc2}, + RegionName.Bc2: {RegionName.Xaldin}, + RegionName.Xaldin: {RegionName.DataXaldin}, + RegionName.Sp: {RegionName.HostileProgram}, + RegionName.HostileProgram: {RegionName.Sp2}, + RegionName.Sp2: {RegionName.Mcp}, + RegionName.Mcp: {RegionName.DataLarxene}, + RegionName.Ht: {RegionName.PrisonKeeper}, + RegionName.PrisonKeeper: {RegionName.OogieBoogie}, + RegionName.OogieBoogie: {RegionName.Ht2}, + RegionName.Ht2: {RegionName.Experiment}, + RegionName.Experiment: {RegionName.DataVexen}, + RegionName.Hb: {RegionName.Hb2}, + RegionName.Hb2: {RegionName.CoR, RegionName.HBDemyx}, + RegionName.HBDemyx: {RegionName.ThousandHeartless}, + RegionName.ThousandHeartless: {RegionName.Mushroom13, RegionName.DataDemyx, RegionName.Sephi}, + RegionName.CoR: {RegionName.CorFirstFight}, + RegionName.CorFirstFight: {RegionName.CorSecondFight}, + RegionName.CorSecondFight: {RegionName.Transport}, + RegionName.Pl: {RegionName.Scar}, + RegionName.Scar: {RegionName.Pl2}, + RegionName.Pl2: {RegionName.GroundShaker}, + RegionName.GroundShaker: {RegionName.DataSaix}, + RegionName.Stt: {RegionName.TwilightThorn}, + RegionName.TwilightThorn: {RegionName.Axel1}, + RegionName.Axel1: {RegionName.Axel2}, + RegionName.Axel2: {RegionName.DataRoxas}, + RegionName.Tt: {RegionName.Tt2}, + RegionName.Tt2: {RegionName.Tt3}, + RegionName.Tt3: {RegionName.DataAxel}, + RegionName.Twtnw: {RegionName.Roxas}, + RegionName.Roxas: {RegionName.Xigbar}, + RegionName.Xigbar: {RegionName.Luxord}, + RegionName.Luxord: {RegionName.Saix}, + RegionName.Saix: {RegionName.Twtnw2}, + RegionName.Twtnw2: {RegionName.Xemnas}, + RegionName.Xemnas: {RegionName.ArmoredXemnas, RegionName.DataXemnas}, + RegionName.ArmoredXemnas: {RegionName.ArmoredXemnas2}, + RegionName.ArmoredXemnas2: {RegionName.FinalXemnas}, + RegionName.LevelsVS1: {RegionName.LevelsVS3}, + RegionName.LevelsVS3: {RegionName.LevelsVS6}, + RegionName.LevelsVS6: {RegionName.LevelsVS9}, + RegionName.LevelsVS9: {RegionName.LevelsVS12}, + RegionName.LevelsVS12: {RegionName.LevelsVS15}, + RegionName.LevelsVS15: {RegionName.LevelsVS18}, + RegionName.LevelsVS18: {RegionName.LevelsVS21}, + RegionName.LevelsVS21: {RegionName.LevelsVS24}, + RegionName.LevelsVS24: {RegionName.LevelsVS26}, + RegionName.AtlanticaSongOne: {RegionName.AtlanticaSongTwo}, + RegionName.AtlanticaSongTwo: {RegionName.AtlanticaSongThree}, + RegionName.AtlanticaSongThree: {RegionName.AtlanticaSongFour}, + } - names: typing.Dict[str, int] = {} - - connect(world, player, names, "Menu", RegionName.Keyblade_Region) - connect(world, player, names, "Menu", RegionName.GoA_Region) - - connect(world, player, names, RegionName.GoA_Region, RegionName.LoD_Region, - lambda state: state.kh_lod_unlocked(player, 1)) - connect(world, player, names, RegionName.LoD_Region, RegionName.LoD2_Region, - lambda state: state.kh_lod_unlocked(player, 2)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Oc_Region, - lambda state: state.kh_oc_unlocked(player, 1)) - connect(world, player, names, RegionName.Oc_Region, RegionName.Oc2_Region, - lambda state: state.kh_oc_unlocked(player, 2)) - connect(world, player, names, RegionName.Oc2_Region, RegionName.Zexion_Region, - lambda state: state.kh_datazexion(player)) - - connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_pain_and_panic_Region, - lambda state: state.kh_painandpanic(player)) - connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_cerberus_Region, - lambda state: state.kh_cerberuscup(player)) - connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_titan_Region, - lambda state: state.kh_titan(player)) - connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_gof_Region, - lambda state: state.kh_gof(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Ag_Region, - lambda state: state.kh_ag_unlocked(player, 1)) - connect(world, player, names, RegionName.Ag_Region, RegionName.Ag2_Region, - lambda state: state.kh_ag_unlocked(player, 2) - and (state.has(ItemName.FireElement, player) - and state.has(ItemName.BlizzardElement, player) - and state.has(ItemName.ThunderElement, player))) - connect(world, player, names, RegionName.Ag2_Region, RegionName.Lexaeus_Region, - lambda state: state.kh_datalexaeus(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Dc_Region, - lambda state: state.kh_dc_unlocked(player, 1)) - connect(world, player, names, RegionName.Dc_Region, RegionName.Tr_Region, - lambda state: state.kh_dc_unlocked(player, 2)) - connect(world, player, names, RegionName.Tr_Region, RegionName.Marluxia_Region, - lambda state: state.kh_datamarluxia(player)) - connect(world, player, names, RegionName.Tr_Region, RegionName.Terra_Region, lambda state: state.kh_terra(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Pr_Region, - lambda state: state.kh_pr_unlocked(player, 1)) - connect(world, player, names, RegionName.Pr_Region, RegionName.Pr2_Region, - lambda state: state.kh_pr_unlocked(player, 2)) - connect(world, player, names, RegionName.Pr2_Region, RegionName.Gr2_Region, - lambda state: state.kh_gr2(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Bc_Region, - lambda state: state.kh_bc_unlocked(player, 1)) - connect(world, player, names, RegionName.Bc_Region, RegionName.Bc2_Region, - lambda state: state.kh_bc_unlocked(player, 2)) - connect(world, player, names, RegionName.Bc2_Region, RegionName.Xaldin_Region, - lambda state: state.kh_xaldin(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Sp_Region, - lambda state: state.kh_sp_unlocked(player, 1)) - connect(world, player, names, RegionName.Sp_Region, RegionName.Sp2_Region, - lambda state: state.kh_sp_unlocked(player, 2)) - connect(world, player, names, RegionName.Sp2_Region, RegionName.Mcp_Region, - lambda state: state.kh_mcp(player)) - connect(world, player, names, RegionName.Mcp_Region, RegionName.Larxene_Region, - lambda state: state.kh_datalarxene(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Ht_Region, - lambda state: state.kh_ht_unlocked(player, 1)) - connect(world, player, names, RegionName.Ht_Region, RegionName.Ht2_Region, - lambda state: state.kh_ht_unlocked(player, 2)) - connect(world, player, names, RegionName.Ht2_Region, RegionName.Vexen_Region, - lambda state: state.kh_datavexen(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Hb_Region, - lambda state: state.kh_hb_unlocked(player, 1)) - connect(world, player, names, RegionName.Hb_Region, RegionName.Hb2_Region, - lambda state: state.kh_hb_unlocked(player, 2)) - connect(world, player, names, RegionName.Hb2_Region, RegionName.ThousandHeartless_Region, - lambda state: state.kh_onek(player)) - connect(world, player, names, RegionName.ThousandHeartless_Region, RegionName.Mushroom13_Region, - lambda state: state.has(ItemName.ProofofPeace, player)) - connect(world, player, names, RegionName.ThousandHeartless_Region, RegionName.Sephi_Region, - lambda state: state.kh_sephi(player)) - - connect(world, player, names, RegionName.Hb2_Region, RegionName.CoR_Region, lambda state: state.kh_cor(player)) - connect(world, player, names, RegionName.CoR_Region, RegionName.Transport_Region, lambda state: - state.has(ItemName.HighJump, player, 3) - and state.has(ItemName.AerialDodge, player, 3) - and state.has(ItemName.Glide, player, 3)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Pl_Region, - lambda state: state.kh_pl_unlocked(player, 1)) - connect(world, player, names, RegionName.Pl_Region, RegionName.Pl2_Region, - lambda state: state.kh_pl_unlocked(player, 2) and ( - state.has(ItemName.BerserkCharge, player) or state.kh_reflect(player))) - - connect(world, player, names, RegionName.GoA_Region, RegionName.STT_Region, - lambda state: state.kh_stt_unlocked(player, 1)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.TT_Region, - lambda state: state.kh_tt_unlocked(player, 1)) - connect(world, player, names, RegionName.TT_Region, RegionName.TT2_Region, - lambda state: state.kh_tt_unlocked(player, 2)) - connect(world, player, names, RegionName.TT2_Region, RegionName.TT3_Region, - lambda state: state.kh_tt_unlocked(player, 3)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Twtnw_Region, - lambda state: state.kh_twtnw_unlocked(player, 0)) - connect(world, player, names, RegionName.Twtnw_Region, RegionName.Twtnw_PostRoxas, - lambda state: state.kh_roxastools(player)) - connect(world, player, names, RegionName.Twtnw_PostRoxas, RegionName.Twtnw_PostXigbar, - lambda state: state.kh_basetools(player) and (state.kh_donaldlimit(player) or ( - state.has(ItemName.FinalForm, player) and state.has(ItemName.FireElement, player)))) - connect(world, player, names, RegionName.Twtnw_PostRoxas, RegionName.Twtnw2_Region, - lambda state: state.kh_twtnw_unlocked(player, 1)) - - hundredacrevisits = {RegionName.HundredAcre1_Region: 0, RegionName.HundredAcre2_Region: 1, - RegionName.HundredAcre3_Region: 2, - RegionName.HundredAcre4_Region: 3, RegionName.HundredAcre5_Region: 4, - RegionName.HundredAcre6_Region: 5} - for visit, tornpage in hundredacrevisits.items(): - connect(world, player, names, RegionName.GoA_Region, visit, - lambda state: (state.has(ItemName.TornPages, player, tornpage))) - - connect(world, player, names, RegionName.GoA_Region, RegionName.LevelsVS1, - lambda state: state.kh_visit_locking_amount(player, 1)) - connect(world, player, names, RegionName.LevelsVS1, RegionName.LevelsVS3, - lambda state: state.kh_visit_locking_amount(player, 3)) - connect(world, player, names, RegionName.LevelsVS3, RegionName.LevelsVS6, - lambda state: state.kh_visit_locking_amount(player, 6)) - connect(world, player, names, RegionName.LevelsVS6, RegionName.LevelsVS9, - lambda state: state.kh_visit_locking_amount(player, 9)) - connect(world, player, names, RegionName.LevelsVS9, RegionName.LevelsVS12, - lambda state: state.kh_visit_locking_amount(player, 12)) - connect(world, player, names, RegionName.LevelsVS12, RegionName.LevelsVS15, - lambda state: state.kh_visit_locking_amount(player, 15)) - connect(world, player, names, RegionName.LevelsVS15, RegionName.LevelsVS18, - lambda state: state.kh_visit_locking_amount(player, 18)) - connect(world, player, names, RegionName.LevelsVS18, RegionName.LevelsVS21, - lambda state: state.kh_visit_locking_amount(player, 21)) - connect(world, player, names, RegionName.LevelsVS21, RegionName.LevelsVS24, - lambda state: state.kh_visit_locking_amount(player, 24)) - connect(world, player, names, RegionName.LevelsVS24, RegionName.LevelsVS26, - lambda state: state.kh_visit_locking_amount(player, 25)) # 25 because of goa twtnw bugs with visit locking. - - for region in RegionTable["ValorRegion"]: - connect(world, player, names, region, RegionName.Valor_Region, - lambda state: state.has(ItemName.ValorForm, player)) - for region in RegionTable["WisdomRegion"]: - connect(world, player, names, region, RegionName.Wisdom_Region, - lambda state: state.has(ItemName.WisdomForm, player)) - for region in RegionTable["LimitRegion"]: - connect(world, player, names, region, RegionName.Limit_Region, - lambda state: state.has(ItemName.LimitForm, player)) - for region in RegionTable["MasterRegion"]: - connect(world, player, names, region, RegionName.Master_Region, - lambda state: state.has(ItemName.MasterForm, player) and state.has(ItemName.DriveConverter, player)) - for region in RegionTable["FinalRegion"]: - connect(world, player, names, region, RegionName.Final_Region, - lambda state: state.has(ItemName.FinalForm, player)) - - -# shamelessly stolen from the sa2b -def connect(world: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str, - rule: typing.Optional[typing.Callable] = None): - source_region = world.get_region(source, player) - target_region = world.get_region(target, player) - - if target not in used_names: - used_names[target] = 1 - name = target - else: - used_names[target] += 1 - name = target + (' ' * used_names[target]) - - connection = Entrance(player, name, source_region) - - if rule: - connection.access_rule = rule + for source, target in KH2RegionConnections.items(): + source_region = multiworld.get_region(source, player) + source_region.add_exits(target) - source_region.exits.append(connection) - connection.connect(target_region) +# cave fight:fire/guard +# hades escape logic:fire,blizzard,slide dash, base tools +# windows:chicken little.fire element,base tools +# chasm of challenges:reflect, blizzard, trinity limit,chicken little +# living bones: magnet +# some things for barbosa(PR), chicken little +# hyneas(magnet,reflect) +# tt2: reflect,chicken,form, guard,aerial recovery,finising plus, +# corridors,dancers:chicken little or stitch +demyx tools +# 1k: guard,once more,limit form, +# snipers +before: stitch, magnet, finishing leap, base tools, reflect +# dragoons:stitch, magnet, base tools, reflect +# oc2 tournament thing: stitch, magnet, base tools, reflera +# lock,shock and barrel: reflect, base tools +# carpet section: magnera, reflect, base tools, +# sp2: reflera, stitch, basse tools, reflera, thundara, fantasia/duck flare,once more. +# tt3: stitch/chicken little, magnera,reflera,base tools,finishing leap,limit form +# cor -def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None): - ret = Region(name, player, world) +def create_region(multiworld, player: int, active_locations, name: str, locations=None): + ret = Region(name, player, multiworld) if locations: - for location in locations: - loc_id = active_locations.get(location, 0) - if loc_id: - location = KH2Location(player, location, loc_id.code, ret) - ret.locations.append(location) + loc_to_id = {loc: active_locations.get(loc, 0) for loc in locations if active_locations.get(loc, None)} + ret.add_locations(loc_to_id, KH2Location) + loc_to_event = {loc: active_locations.get(loc, None) for loc in locations if + not active_locations.get(loc, None)} + ret.add_locations(loc_to_event, KH2Location) return ret diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index b86ae4a2db4f..18375231a5a6 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -1,96 +1,1163 @@ +from typing import Dict, Callable, TYPE_CHECKING -from BaseClasses import MultiWorld - -from .Items import exclusionItem_table -from .Locations import STT_Checks, exclusion_table -from .Names import LocationName, ItemName -from ..generic.Rules import add_rule, forbid_items, set_rule - - -def set_rules(world: MultiWorld, player: int): - - add_rule(world.get_location(LocationName.RoxasDataMagicBoost, player), - lambda state: state.kh_dataroxas(player)) - add_rule(world.get_location(LocationName.DemyxDataAPBoost, player), - lambda state: state.kh_datademyx(player)) - add_rule(world.get_location(LocationName.SaixDataDefenseBoost, player), - lambda state: state.kh_datasaix(player)) - add_rule(world.get_location(LocationName.XaldinDataDefenseBoost, player), - lambda state: state.kh_dataxaldin(player)) - add_rule(world.get_location(LocationName.XemnasDataPowerBoost, player), - lambda state: state.kh_dataxemnas(player)) - add_rule(world.get_location(LocationName.XigbarDataDefenseBoost, player), - lambda state: state.kh_dataxigbar(player)) - add_rule(world.get_location(LocationName.VexenDataLostIllusion, player), - lambda state: state.kh_dataaxel(player)) - add_rule(world.get_location(LocationName.LuxordDataAPBoost, player), - lambda state: state.kh_dataluxord(player)) - - for slot, weapon in exclusion_table["WeaponSlots"].items(): - add_rule(world.get_location(slot, player), lambda state: state.has(weapon, player)) - formLogicTable = { - ItemName.ValorForm: [LocationName.Valorlvl4, - LocationName.Valorlvl5, - LocationName.Valorlvl6, - LocationName.Valorlvl7], - ItemName.WisdomForm: [LocationName.Wisdomlvl4, - LocationName.Wisdomlvl5, - LocationName.Wisdomlvl6, - LocationName.Wisdomlvl7], - ItemName.LimitForm: [LocationName.Limitlvl4, - LocationName.Limitlvl5, - LocationName.Limitlvl6, - LocationName.Limitlvl7], - ItemName.MasterForm: [LocationName.Masterlvl4, - LocationName.Masterlvl5, - LocationName.Masterlvl6, - LocationName.Masterlvl7], - ItemName.FinalForm: [LocationName.Finallvl4, - LocationName.Finallvl5, - LocationName.Finallvl6, - LocationName.Finallvl7] - } - - for form in formLogicTable: - for i in range(4): - location = world.get_location(formLogicTable[form][i], player) - set_rule(location, lambda state, i=i + 1, form=form: state.kh_amount_of_forms(player, i, form)) - - if world.Goal[player] == "three_proofs": - add_rule(world.get_location(LocationName.FinalXemnas, player), - lambda state: state.kh_three_proof_unlocked(player)) - if world.FinalXemnas[player]: - world.completion_condition[player] = lambda state: state.kh_victory(player) - else: - world.completion_condition[player] = lambda state: state.kh_three_proof_unlocked(player) - # lucky emblem hunt - elif world.Goal[player] == "lucky_emblem_hunt": - add_rule(world.get_location(LocationName.FinalXemnas, player), - lambda state: state.kh_lucky_emblem_unlocked(player, world.LuckyEmblemsRequired[player].value)) - if world.FinalXemnas[player]: - world.completion_condition[player] = lambda state: state.kh_victory(player) - else: - world.completion_condition[player] = lambda state: state.kh_lucky_emblem_unlocked(player, world.LuckyEmblemsRequired[player].value) - # hitlist if == 2 - else: - add_rule(world.get_location(LocationName.FinalXemnas, player), - lambda state: state.kh_hitlist(player, world.BountyRequired[player].value)) - if world.FinalXemnas[player]: - world.completion_condition[player] = lambda state: state.kh_victory(player) +from BaseClasses import CollectionState +from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table +from .Locations import exclusion_table, popups_set, Goofy_Checks, Donald_Checks +from .Names import LocationName, ItemName, RegionName +from worlds.generic.Rules import add_rule, forbid_items, add_item_rule +from .Logic import * + +# I don't know what is going on here, but it works. +if TYPE_CHECKING: + from . import KH2World +else: + KH2World = object + + +# Shamelessly Stolen from Messanger + + +class KH2Rules: + player: int + world: KH2World + # World Rules: Rules for the visit locks + # Location Rules: Deterministic of player settings. + # Form Rules: Rules for Drive Forms and Summon levels. These Are Locations + # Fight Rules: Rules for fights. These are regions in the worlds. + world_rules: Dict[str, Callable[[CollectionState], bool]] + location_rules: Dict[str, Callable[[CollectionState], bool]] + + fight_rules: Dict[str, Callable[[CollectionState], bool]] + + def __init__(self, world: KH2World) -> None: + self.player = world.player + self.world = world + self.multiworld = world.multiworld + + def lod_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.SwordoftheAncestor, self.player, Amount) + + def oc_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.BattlefieldsofWar, self.player, Amount) + + def twtnw_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.WaytotheDawn, self.player, Amount) + + def ht_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.BoneFist, self.player, Amount) + + def tt_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.IceCream, self.player, Amount) + + def pr_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.SkillandCrossbones, self.player, Amount) + + def sp_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.IdentityDisk, self.player, Amount) + + def stt_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.NamineSketches, self.player, Amount) + + def dc_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.CastleKey, self.player, Amount) # Using Dummy 13 for this + + def hb_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.MembershipCard, self.player, Amount) + + def pl_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.ProudFang, self.player, Amount) + + def ag_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.Scimitar, self.player, Amount) + + def bc_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.BeastsClaw, self.player, Amount) + + def at_three_unlocked(self, state: CollectionState) -> bool: + return state.has(ItemName.MagnetElement, self.player, 2) + + def at_four_unlocked(self, state: CollectionState) -> bool: + return state.has(ItemName.ThunderElement, self.player, 3) + + def hundred_acre_unlocked(self, state: CollectionState, amount) -> bool: + return state.has(ItemName.TornPages, self.player, amount) + + def level_locking_unlock(self, state: CollectionState, amount): + return amount <= sum([state.count(item_name, self.player) for item_name in visit_locking_dict["2VisitLocking"]]) + + def summon_levels_unlocked(self, state: CollectionState, amount) -> bool: + return amount <= sum([state.count(item_name, self.player) for item_name in summons]) + + def kh2_list_count_sum(self, item_name_set: list, state: CollectionState) -> int: + """ + Returns the sum of state.count() for each item in the list. + """ + return sum( + [state.count(item_name, self.player) for item_name in item_name_set] + ) + + def kh2_list_any_sum(self, list_of_item_name_list: list, state: CollectionState) -> int: + """ + Returns sum that increments by 1 if state.has_any + """ + return sum( + [1 for item_list in list_of_item_name_list if + state.has_any(set(item_list), self.player)] + ) + + def kh2_dict_count(self, item_name_to_count: dict, state: CollectionState) -> bool: + """ + simplifies count to a dictionary. + """ + return all( + [state.count(item_name, self.player) >= item_amount for item_name, item_amount in + item_name_to_count.items()] + ) + + def kh2_dict_one_count(self, item_name_to_count: dict, state: CollectionState) -> int: + """ + simplifies count to a dictionary. + """ + return sum( + [1 for item_name, item_amount in + item_name_to_count.items() if state.count(item_name, self.player) >= item_amount] + ) + + def kh2_can_reach_any(self, loc_set: list, state: CollectionState): + """ + Can reach any locations in the set. + """ + return any( + [self.kh2_can_reach(location, state) for location in + loc_set] + ) + + def kh2_can_reach_all(self, loc_list: list, state: CollectionState): + """ + Can reach all locations in the set. + """ + return all( + [self.kh2_can_reach(location, state) for location in loc_list] + ) + + def kh2_can_reach(self, loc: str, state: CollectionState): + """ + Returns bool instead of collection state. + """ + return state.can_reach(self.multiworld.get_location(loc, self.player), "location", self.player) + + def kh2_has_all(self, items: list, state: CollectionState): + """If state has at least one of all.""" + return state.has_all(set(items), self.player) + + def kh2_has_any(self, items: list, state: CollectionState): + return state.has_any(set(items), self.player) + + def form_list_unlock(self, state: CollectionState, parent_form_list, level_required, fight_logic=False) -> bool: + form_access = {parent_form_list} + if self.multiworld.AutoFormLogic[self.player] and state.has(ItemName.SecondChance, self.player) and not fight_logic: + if parent_form_list == ItemName.MasterForm: + if state.has(ItemName.DriveConverter, self.player): + form_access.add(auto_form_dict[parent_form_list]) + else: + form_access.add(auto_form_dict[parent_form_list]) + return state.has_any(form_access, self.player) \ + and self.get_form_level_requirement(state, level_required) + + def get_form_level_requirement(self, state, amount): + forms_available = 0 + form_list = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm, + ItemName.FinalForm] + if self.world.multiworld.FinalFormLogic[self.player] != "no_light_and_darkness": + if self.world.multiworld.FinalFormLogic[self.player] == "light_and_darkness": + if state.has(ItemName.LightDarkness, self.player) and state.has_any(set(form_list), self.player): + forms_available += 1 + form_list.remove(ItemName.FinalForm) + else: # self.multiworld.FinalFormLogic=="just a form" + form_list.remove(ItemName.FinalForm) + if state.has_any(form_list, self.player): + forms_available += 1 + forms_available += sum([1 for form in form_list if state.has(form, self.player)]) + return forms_available >= amount + + +class KH2WorldRules(KH2Rules): + def __init__(self, kh2world: KH2World) -> None: + # These Rules are Always in effect + super().__init__(kh2world) + self.region_rules = { + RegionName.LoD: lambda state: self.lod_unlocked(state, 1), + RegionName.LoD2: lambda state: self.lod_unlocked(state, 2), + + RegionName.Oc: lambda state: self.oc_unlocked(state, 1), + RegionName.Oc2: lambda state: self.oc_unlocked(state, 2), + + RegionName.Twtnw2: lambda state: self.twtnw_unlocked(state, 2), + # These will be swapped and First Visit lock for twtnw is in development. + # RegionName.Twtnw1: lambda state: self.lod_unlocked(state, 2), + + RegionName.Ht: lambda state: self.ht_unlocked(state, 1), + RegionName.Ht2: lambda state: self.ht_unlocked(state, 2), + + RegionName.Tt: lambda state: self.tt_unlocked(state, 1), + RegionName.Tt2: lambda state: self.tt_unlocked(state, 2), + RegionName.Tt3: lambda state: self.tt_unlocked(state, 3), + + RegionName.Pr: lambda state: self.pr_unlocked(state, 1), + RegionName.Pr2: lambda state: self.pr_unlocked(state, 2), + + RegionName.Sp: lambda state: self.sp_unlocked(state, 1), + RegionName.Sp2: lambda state: self.sp_unlocked(state, 2), + + RegionName.Stt: lambda state: self.stt_unlocked(state, 1), + + RegionName.Dc: lambda state: self.dc_unlocked(state, 1), + RegionName.Tr: lambda state: self.dc_unlocked(state, 2), + # Terra is a fight and can have more than just this requirement. + # RegionName.Terra: lambda state:state.has(ItemName.ProofofConnection,self.player), + + RegionName.Hb: lambda state: self.hb_unlocked(state, 1), + RegionName.Hb2: lambda state: self.hb_unlocked(state, 2), + RegionName.Mushroom13: lambda state: state.has(ItemName.ProofofPeace, self.player), + + RegionName.Pl: lambda state: self.pl_unlocked(state, 1), + RegionName.Pl2: lambda state: self.pl_unlocked(state, 2), + + RegionName.Ag: lambda state: self.ag_unlocked(state, 1), + RegionName.Ag2: lambda state: self.ag_unlocked(state, 2), + + RegionName.Bc: lambda state: self.bc_unlocked(state, 1), + RegionName.Bc2: lambda state: self.bc_unlocked(state, 2), + + RegionName.AtlanticaSongThree: lambda state: self.at_three_unlocked(state), + RegionName.AtlanticaSongFour: lambda state: self.at_four_unlocked(state), + + RegionName.Ha1: lambda state: True, + RegionName.Ha2: lambda state: self.hundred_acre_unlocked(state, 1), + RegionName.Ha3: lambda state: self.hundred_acre_unlocked(state, 2), + RegionName.Ha4: lambda state: self.hundred_acre_unlocked(state, 3), + RegionName.Ha5: lambda state: self.hundred_acre_unlocked(state, 4), + RegionName.Ha6: lambda state: self.hundred_acre_unlocked(state, 5), + + RegionName.LevelsVS1: lambda state: self.level_locking_unlock(state, 1), + RegionName.LevelsVS3: lambda state: self.level_locking_unlock(state, 3), + RegionName.LevelsVS6: lambda state: self.level_locking_unlock(state, 6), + RegionName.LevelsVS9: lambda state: self.level_locking_unlock(state, 9), + RegionName.LevelsVS12: lambda state: self.level_locking_unlock(state, 12), + RegionName.LevelsVS15: lambda state: self.level_locking_unlock(state, 15), + RegionName.LevelsVS18: lambda state: self.level_locking_unlock(state, 18), + RegionName.LevelsVS21: lambda state: self.level_locking_unlock(state, 21), + RegionName.LevelsVS24: lambda state: self.level_locking_unlock(state, 24), + RegionName.LevelsVS26: lambda state: self.level_locking_unlock(state, 26), + } + + def set_kh2_rules(self) -> None: + for region_name, rules in self.region_rules.items(): + region = self.multiworld.get_region(region_name, self.player) + for entrance in region.entrances: + entrance.access_rule = rules + + self.set_kh2_goal() + + weapon_region = self.multiworld.get_region(RegionName.Keyblade, self.player) + for location in weapon_region.locations: + add_rule(location, lambda state: state.has(exclusion_table["WeaponSlots"][location.name], self.player)) + if location.name in Goofy_Checks: + add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys()) + elif location.name in Donald_Checks: + add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys()) + + def set_kh2_goal(self): + + final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player) + if self.multiworld.Goal[self.player] == "three_proofs": + final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state) + if self.multiworld.FinalXemnas[self.player]: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1) + else: + self.multiworld.completion_condition[self.player] = lambda state: self.kh2_has_all(three_proofs, state) + # lucky emblem hunt + elif self.multiworld.Goal[self.player] == "lucky_emblem_hunt": + final_xemnas_location.access_rule = lambda state: state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) + if self.multiworld.FinalXemnas[self.player]: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1) + else: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) + # hitlist if == 2 + elif self.multiworld.Goal[self.player] == "hitlist": + final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) + if self.multiworld.FinalXemnas[self.player]: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1) + else: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) else: - world.completion_condition[player] = lambda state: state.kh_hitlist(player, world.BountyRequired[player].value) + final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and\ + state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) + if self.multiworld.FinalXemnas[self.player]: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1) + else: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and \ + state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) + + +class KH2FormRules(KH2Rules): + #: Dict[str, Callable[[CollectionState], bool]] + def __init__(self, world: KH2World) -> None: + super().__init__(world) + # access rules on where you can level a form. + + self.form_rules = { + LocationName.Valorlvl2: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 0), + LocationName.Valorlvl3: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 1), + LocationName.Valorlvl4: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 2), + LocationName.Valorlvl5: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 3), + LocationName.Valorlvl6: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 4), + LocationName.Valorlvl7: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 5), + LocationName.Wisdomlvl2: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 0), + LocationName.Wisdomlvl3: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 1), + LocationName.Wisdomlvl4: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 2), + LocationName.Wisdomlvl5: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 3), + LocationName.Wisdomlvl6: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 4), + LocationName.Wisdomlvl7: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 5), + LocationName.Limitlvl2: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 0), + LocationName.Limitlvl3: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 1), + LocationName.Limitlvl4: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 2), + LocationName.Limitlvl5: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 3), + LocationName.Limitlvl6: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 4), + LocationName.Limitlvl7: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 5), + LocationName.Masterlvl2: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 0), + LocationName.Masterlvl3: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 1), + LocationName.Masterlvl4: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 2), + LocationName.Masterlvl5: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 3), + LocationName.Masterlvl6: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 4), + LocationName.Masterlvl7: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 5), + LocationName.Finallvl2: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 0), + LocationName.Finallvl3: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 1), + LocationName.Finallvl4: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 2), + LocationName.Finallvl5: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 3), + LocationName.Finallvl6: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 4), + LocationName.Finallvl7: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 5), + LocationName.Summonlvl2: lambda state: self.summon_levels_unlocked(state, 1), + LocationName.Summonlvl3: lambda state: self.summon_levels_unlocked(state, 1), + LocationName.Summonlvl4: lambda state: self.summon_levels_unlocked(state, 2), + LocationName.Summonlvl5: lambda state: self.summon_levels_unlocked(state, 3), + LocationName.Summonlvl6: lambda state: self.summon_levels_unlocked(state, 4), + LocationName.Summonlvl7: lambda state: self.summon_levels_unlocked(state, 4), + } + self.form_region_rules = { + RegionName.Valor: lambda state: self.multi_form_region_access(), + RegionName.Wisdom: lambda state: self.multi_form_region_access(), + RegionName.Limit: lambda state: self.limit_form_region_access(), + RegionName.Master: lambda state: self.multi_form_region_access(), + RegionName.Final: lambda state: self.final_form_region_access(state) + } + + def final_form_region_access(self, state: CollectionState) -> bool: + """ + Can reach one of TT3,Twtnw post Roxas, BC2, LoD2 or PR2 + """ + # tt3 start, can beat roxas, can beat gr2, can beat xaldin, can beat storm rider. + + return any( + self.multiworld.get_location(location, self.player).can_reach(state) for location in + final_leveling_access + ) + + @staticmethod + def limit_form_region_access() -> bool: + """ + returns true since twtnw always is open and has enemies + """ + return True + + @staticmethod + def multi_form_region_access() -> bool: + """ + returns true since twtnw always is open and has enemies + Valor, Wisdom and Master Form region access. + Note: This does not account for having the drive form. See form_list_unlock + """ + # todo: if boss enemy start the player with oc stone because of cerb + return True + + def set_kh2_form_rules(self): + for region_name in drive_form_list: + if region_name == RegionName.Summon and not self.world.options.SummonLevelLocationToggle: + continue + # could get the location of each of these, but I feel like that would be less optimal + region = self.multiworld.get_region(region_name, self.player) + # if region_name in form_region_rules + if region_name != RegionName.Summon: + for entrance in region.entrances: + entrance.access_rule = self.form_region_rules[region_name] + for loc in region.locations: + loc.access_rule = self.form_rules[loc.name] + + +class KH2FightRules(KH2Rules): + player: int + world: KH2World + region_rules: Dict[str, Callable[[CollectionState], bool]] + location_rules: Dict[str, Callable[[CollectionState], bool]] + + # cor logic + # have 3 things for the logic + # region:movement_rules and (fight_rules or skip rules) + # if skip rules are of return false + def __init__(self, world: KH2World) -> None: + super().__init__(world) + self.fight_logic = self.multiworld.FightLogic[self.player].current_key + + self.fight_region_rules = { + RegionName.ShanYu: lambda state: self.get_shan_yu_rules(state), + RegionName.AnsemRiku: lambda state: self.get_ansem_riku_rules(state), + RegionName.StormRider: lambda state: self.get_storm_rider_rules(state), + RegionName.DataXigbar: lambda state: self.get_data_xigbar_rules(state), + RegionName.TwinLords: lambda state: self.get_fire_lord_rules(state) and self.get_blizzard_lord_rules(state), + RegionName.GenieJafar: lambda state: self.get_genie_jafar_rules(state), + RegionName.DataLexaeus: lambda state: self.get_data_lexaeus_rules(state), + RegionName.OldPete: lambda state: self.get_old_pete_rules(), + RegionName.FuturePete: lambda state: self.get_future_pete_rules(state), + RegionName.Terra: lambda state: self.get_terra_rules(state), + RegionName.DataMarluxia: lambda state: self.get_data_marluxia_rules(state), + RegionName.Barbosa: lambda state: self.get_barbosa_rules(state), + RegionName.GrimReaper1: lambda state: self.get_grim_reaper1_rules(), + RegionName.GrimReaper2: lambda state: self.get_grim_reaper2_rules(state), + RegionName.DataLuxord: lambda state: self.get_data_luxord_rules(state), + RegionName.Cerberus: lambda state: self.get_cerberus_rules(state), + RegionName.OlympusPete: lambda state: self.get_olympus_pete_rules(state), + RegionName.Hydra: lambda state: self.get_hydra_rules(state), + RegionName.Hades: lambda state: self.get_hades_rules(state), + RegionName.DataZexion: lambda state: self.get_data_zexion_rules(state), + RegionName.OcPainAndPanicCup: lambda state: self.get_pain_and_panic_cup_rules(state), + RegionName.OcCerberusCup: lambda state: self.get_cerberus_cup_rules(state), + RegionName.Oc2TitanCup: lambda state: self.get_titan_cup_rules(state), + RegionName.Oc2GofCup: lambda state: self.get_goddess_of_fate_cup_rules(state), + RegionName.HadesCups: lambda state: self.get_hades_cup_rules(state), + RegionName.Thresholder: lambda state: self.get_thresholder_rules(state), + RegionName.Beast: lambda state: self.get_beast_rules(), + RegionName.DarkThorn: lambda state: self.get_dark_thorn_rules(state), + RegionName.Xaldin: lambda state: self.get_xaldin_rules(state), + RegionName.DataXaldin: lambda state: self.get_data_xaldin_rules(state), + RegionName.HostileProgram: lambda state: self.get_hostile_program_rules(state), + RegionName.Mcp: lambda state: self.get_mcp_rules(state), + RegionName.DataLarxene: lambda state: self.get_data_larxene_rules(state), + RegionName.PrisonKeeper: lambda state: self.get_prison_keeper_rules(state), + RegionName.OogieBoogie: lambda state: self.get_oogie_rules(), + RegionName.Experiment: lambda state: self.get_experiment_rules(state), + RegionName.DataVexen: lambda state: self.get_data_vexen_rules(state), + RegionName.HBDemyx: lambda state: self.get_demyx_rules(state), + RegionName.ThousandHeartless: lambda state: self.get_thousand_heartless_rules(state), + RegionName.DataDemyx: lambda state: self.get_data_demyx_rules(state), + RegionName.Sephi: lambda state: self.get_sephiroth_rules(state), + RegionName.CorFirstFight: lambda state: self.get_cor_first_fight_movement_rules(state) and (self.get_cor_first_fight_rules(state) or self.get_cor_skip_first_rules(state)), + RegionName.CorSecondFight: lambda state: self.get_cor_second_fight_movement_rules(state), + RegionName.Transport: lambda state: self.get_transport_movement_rules(state), + RegionName.Scar: lambda state: self.get_scar_rules(state), + RegionName.GroundShaker: lambda state: self.get_groundshaker_rules(state), + RegionName.DataSaix: lambda state: self.get_data_saix_rules(state), + RegionName.TwilightThorn: lambda state: self.get_twilight_thorn_rules(), + RegionName.Axel1: lambda state: self.get_axel_one_rules(), + RegionName.Axel2: lambda state: self.get_axel_two_rules(), + RegionName.DataRoxas: lambda state: self.get_data_roxas_rules(state), + RegionName.DataAxel: lambda state: self.get_data_axel_rules(state), + RegionName.Roxas: lambda state: self.get_roxas_rules(state) and self.twtnw_unlocked(state, 1), + RegionName.Xigbar: lambda state: self.get_xigbar_rules(state), + RegionName.Luxord: lambda state: self.get_luxord_rules(state), + RegionName.Saix: lambda state: self.get_saix_rules(state), + RegionName.Xemnas: lambda state: self.get_xemnas_rules(state), + RegionName.ArmoredXemnas: lambda state: self.get_armored_xemnas_one_rules(state), + RegionName.ArmoredXemnas2: lambda state: self.get_armored_xemnas_two_rules(state), + RegionName.FinalXemnas: lambda state: self.get_final_xemnas_rules(state), + RegionName.DataXemnas: lambda state: self.get_data_xemnas_rules(state), + } + + def set_kh2_fight_rules(self) -> None: + for region_name, rules in self.fight_region_rules.items(): + region = self.multiworld.get_region(region_name, self.player) + for entrance in region.entrances: + entrance.access_rule = rules + + for loc_name in [LocationName.TransportEventLocation, LocationName.TransporttoRemembrance]: + location = self.multiworld.get_location(loc_name, self.player) + add_rule(location, lambda state: self.get_transport_fight_rules(state)) + + def get_shan_yu_rules(self, state: CollectionState) -> bool: + # easy: gap closer, defensive tool,drive form + # normal: 2 out of easy + # hard: defensive tool or drive form + shan_yu_rules = { + "easy": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 3, + "normal": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool, form_list], state) >= 1 + } + return shan_yu_rules[self.fight_logic] + + def get_ansem_riku_rules(self, state: CollectionState) -> bool: + # easy: gap closer,defensive tool,ground finisher/limit form + # normal: defensive tool and (gap closer/ground finisher/limit form) + # hard: defensive tool or limit form + ansem_riku_rules = { + "easy": self.kh2_list_any_sum([gap_closer, defensive_tool, [ItemName.LimitForm], ground_finisher], state) >= 3, + "normal": self.kh2_list_any_sum([gap_closer, defensive_tool, [ItemName.LimitForm], ground_finisher], state) >= 2, + "hard": self.kh2_has_any([ItemName.ReflectElement, ItemName.Guard, ItemName.LimitForm], state), + } + return ansem_riku_rules[self.fight_logic] + + def get_storm_rider_rules(self, state: CollectionState) -> bool: + # easy: has defensive tool,drive form, party limit,aerial move + # normal: has 3 of those things + # hard: has 2 of those things + storm_rider_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, party_limit, aerial_move, form_list], state) >= 4, + "normal": self.kh2_list_any_sum([defensive_tool, party_limit, aerial_move, form_list], state) >= 3, + "hard": self.kh2_list_any_sum([defensive_tool, party_limit, aerial_move, form_list], state) >= 2, + } + return storm_rider_rules[self.fight_logic] + + def get_data_xigbar_rules(self, state: CollectionState) -> bool: + # easy:final 7,firaga,2 air combo plus,air gap closer, finishing plus,guard,reflega,horizontal slash,donald limit + # normal:final 7,firaga,finishing plus,guard,reflect horizontal slash,donald limit + # hard:((final 5, fira) or donald limit), finishing plus,guard/reflect + data_xigbar_rules = { + "easy": self.kh2_dict_count(easy_data_xigbar_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_has_any(donald_limit, state), + "normal": self.kh2_dict_count(normal_data_xigbar_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_has_any(donald_limit, state), + "hard": ((self.form_list_unlock(state, ItemName.FinalForm, 3, True) and state.has(ItemName.FireElement, self.player, 2)) or self.kh2_has_any(donald_limit, state)) + and state.has(ItemName.FinishingPlus, self.player) and self.kh2_has_any(defensive_tool, state) + } + return data_xigbar_rules[self.fight_logic] + + def get_fire_lord_rules(self, state: CollectionState) -> bool: + # easy: drive form,defensive tool,one black magic,party limit + # normal: 3 of those things + # hard:2 of those things + # duplicate of the other because in boss rando there will be to bosses in arena and these bosses can be split. + fire_lords_rules = { + "easy": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 4, + "normal": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 3, + "hard": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 2, + } + return fire_lords_rules[self.fight_logic] + + def get_blizzard_lord_rules(self, state: CollectionState) -> bool: + # easy: drive form,defensive tool,one black magic,party limit + # normal: 3 of those things + # hard:2 of those things + # duplicate of the other because in boss rando there will be to bosses in arena and these bosses can be split. + blizzard_lords_rules = { + "easy": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 4, + "normal": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 3, + "hard": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 2, + } + return blizzard_lords_rules[self.fight_logic] + + def get_genie_jafar_rules(self, state: CollectionState) -> bool: + # easy: defensive tool,black magic,ground finisher,finishing plus + # normal: defensive tool, ground finisher,finishing plus + # hard: defensive tool,finishing plus + genie_jafar_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, black_magic, ground_finisher, {ItemName.FinishingPlus}], state) >= 4, + "normal": self.kh2_list_any_sum([defensive_tool, ground_finisher, {ItemName.FinishingPlus}], state) >= 3, + "hard": self.kh2_list_any_sum([defensive_tool, {ItemName.FinishingPlus}], state) >= 2, + } + return genie_jafar_rules[self.fight_logic] + + def get_data_lexaeus_rules(self, state: CollectionState) -> bool: + # easy:both gap closers,final 7,firaga,reflera,donald limit, guard + # normal:one gap closer,final 5,fira,reflect, donald limit,guard + # hard:defensive tool,gap closer + data_lexaues_rules = { + "easy": self.kh2_dict_count(easy_data_lex_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_list_any_sum([donald_limit], state) >= 1, + "normal": self.kh2_dict_count(normal_data_lex_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 3, True) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool, gap_closer], state) >= 2, + } + return data_lexaues_rules[self.fight_logic] + + @staticmethod + def get_old_pete_rules(): + # fight is free. + return True + + def get_future_pete_rules(self, state: CollectionState) -> bool: + # easy:defensive option,gap closer,drive form + # norma:2 of those things + # hard 1 of those things + future_pete_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, gap_closer, form_list], state) >= 3, + "normal": self.kh2_list_any_sum([defensive_tool, gap_closer, form_list], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool, gap_closer, form_list], state) >= 1, + } + return future_pete_rules[self.fight_logic] + + def get_data_marluxia_rules(self, state: CollectionState) -> bool: + # easy:both gap closers,final 7,firaga,reflera,donald limit, guard + # normal:one gap closer,final 5,fira,reflect, donald limit,guard + # hard:defensive tool,gap closer + data_marluxia_rules = { + "easy": self.kh2_dict_count(easy_data_marluxia_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_list_any_sum([donald_limit], state) >= 1, + "normal": self.kh2_dict_count(normal_data_marluxia_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 3, True) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool, gap_closer, [ItemName.AerialRecovery]], state) >= 3, + } + return data_marluxia_rules[self.fight_logic] + + def get_terra_rules(self, state: CollectionState) -> bool: + # easy:scom,gap closers,explosion,2 combo pluses,final 7,firaga, donald limits,reflect,guard,3 dodge roll,3 aerial dodge and 3glide + # normal:gap closers,explosion,2 combo pluses,2 dodge roll,2 aerial dodge and lvl 2glide,guard,donald limit, guard + # hard:1 gap closer,explosion,2 combo pluses,2 dodge roll,2 aerial dodge and lvl 2glide,guard + terra_rules = { + "easy": self.kh2_dict_count(easy_terra_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "normal": self.kh2_dict_count(normal_terra_tools, state) and self.kh2_list_any_sum([donald_limit], state) >= 1, + "hard": self.kh2_dict_count(hard_terra_tools, state) and self.kh2_list_any_sum([gap_closer], state) >= 1, + } + return terra_rules[self.fight_logic] + + def get_barbosa_rules(self, state: CollectionState) -> bool: + # easy:blizzara and thundara or one of each,defensive tool + # normal:(blizzard or thunder) and defensive tool + # hard: defensive tool + barbosa_rules = { + "easy": self.kh2_list_count_sum([ItemName.BlizzardElement, ItemName.ThunderElement], state) >= 2 and self.kh2_list_any_sum([defensive_tool], state) >= 1, + "normal": self.kh2_list_any_sum([defensive_tool, {ItemName.BlizzardElement, ItemName.ThunderElement}], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool], state) >= 1, + } + return barbosa_rules[self.fight_logic] + + @staticmethod + def get_grim_reaper1_rules(): + # fight is free. + return True + + def get_grim_reaper2_rules(self, state: CollectionState) -> bool: + # easy:master form,thunder,defensive option + # normal:master form/stitch,thunder,defensive option + # hard:any black magic,defensive option. + gr2_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, {ItemName.MasterForm, ItemName.ThunderElement}], state) >= 2, + "normal": self.kh2_list_any_sum([defensive_tool, {ItemName.MasterForm, ItemName.Stitch}, {ItemName.ThunderElement}], state) >= 3, + "hard": self.kh2_list_any_sum([black_magic, defensive_tool], state) >= 2 + } + return gr2_rules[self.fight_logic] + + def get_data_luxord_rules(self, state: CollectionState) -> bool: + # easy:gap closers,reflega,aerial dodge lvl 2,glide lvl 2,guard + # normal:1 gap closer,reflect,aerial dodge lvl 1,glide lvl 1,guard + # hard:quick run,defensive option + data_luxord_rules = { + "easy": self.kh2_dict_count(easy_data_luxord_tools, state), + "normal": self.kh2_has_all([ItemName.ReflectElement, ItemName.AerialDodge, ItemName.Glide, ItemName.Guard], state) and self.kh2_has_any(defensive_tool, state), + "hard": self.kh2_list_any_sum([{ItemName.QuickRun}, defensive_tool], state) + } + return data_luxord_rules[self.fight_logic] + + def get_cerberus_rules(self, state: CollectionState) -> bool: + # easy,normal:defensive option, offensive magic + # hard:defensive option + cerberus_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, black_magic], state) >= 2, + "normal": self.kh2_list_any_sum([defensive_tool, black_magic], state) >= 2, + "hard": self.kh2_has_any(defensive_tool, state), + } + return cerberus_rules[self.fight_logic] + + def get_pain_and_panic_cup_rules(self, state: CollectionState) -> bool: + # easy:2 party limit,reflect + # normal:1 party limit,reflect + # hard:reflect + pain_and_panic_rules = { + "easy": self.kh2_list_count_sum(party_limit, state) >= 2 and state.has(ItemName.ReflectElement, self.player), + "normal": self.kh2_list_count_sum(party_limit, state) >= 1 and state.has(ItemName.ReflectElement, self.player), + "hard": state.has(ItemName.ReflectElement, self.player) + } + return pain_and_panic_rules[self.fight_logic] and (self.kh2_has_all([ItemName.FuturePeteEvent], state) or state.has(ItemName.HadesCupTrophy, self.player)) + + def get_cerberus_cup_rules(self, state: CollectionState) -> bool: + # easy:3 drive forms,reflect + # normal:2 drive forms,reflect + # hard:reflect + cerberus_cup_rules = { + "easy": self.kh2_can_reach_any([LocationName.Valorlvl5, LocationName.Wisdomlvl5, LocationName.Limitlvl5, LocationName.Masterlvl5, LocationName.Finallvl5], state) and state.has(ItemName.ReflectElement, self.player), + "normal": self.kh2_can_reach_any([LocationName.Valorlvl4, LocationName.Wisdomlvl4, LocationName.Limitlvl4, LocationName.Masterlvl4, LocationName.Finallvl4], state) and state.has(ItemName.ReflectElement, self.player), + "hard": state.has(ItemName.ReflectElement, self.player) + } + return cerberus_cup_rules[self.fight_logic] and (self.kh2_has_all([ItemName.ScarEvent, ItemName.OogieBoogieEvent, ItemName.TwinLordsEvent], state) or state.has(ItemName.HadesCupTrophy, self.player)) + + def get_titan_cup_rules(self, state: CollectionState) -> bool: + # easy:4 summons,reflera + # normal:4 summons,reflera + # hard:2 summons,reflera + titan_cup_rules = { + "easy": self.kh2_list_count_sum(summons, state) >= 4 and state.has(ItemName.ReflectElement, self.player, 2), + "normal": self.kh2_list_count_sum(summons, state) >= 3 and state.has(ItemName.ReflectElement, self.player, 2), + "hard": self.kh2_list_count_sum(summons, state) >= 2 and state.has(ItemName.ReflectElement, self.player, 2), + } + return titan_cup_rules[self.fight_logic] and (state.has(ItemName.HadesEvent, self.player) or state.has(ItemName.HadesCupTrophy, self.player)) + + def get_goddess_of_fate_cup_rules(self, state: CollectionState) -> bool: + # can beat all the other cups+xemnas 1 + return self.kh2_has_all([ItemName.OcPainAndPanicCupEvent, ItemName.OcCerberusCupEvent, ItemName.Oc2TitanCupEvent], state) + + def get_hades_cup_rules(self, state: CollectionState) -> bool: + # can beat goddess of fate cup + return state.has(ItemName.Oc2GofCupEvent, self.player) + + def get_olympus_pete_rules(self, state: CollectionState) -> bool: + # easy:gap closer,defensive option,drive form + # normal:2 of those things + # hard:1 of those things + olympus_pete_rules = { + "easy": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 3, + "normal": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 2, + "hard": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 1, + } + return olympus_pete_rules[self.fight_logic] + + def get_hydra_rules(self, state: CollectionState) -> bool: + # easy:drive form,defensive option,offensive magic + # normal 2 of those things + # hard: one of those things + hydra_rules = { + "easy": self.kh2_list_any_sum([black_magic, defensive_tool, form_list], state) >= 3, + "normal": self.kh2_list_any_sum([black_magic, defensive_tool, form_list], state) >= 2, + "hard": self.kh2_list_any_sum([black_magic, defensive_tool, form_list], state) >= 1, + } + return hydra_rules[self.fight_logic] + + def get_hades_rules(self, state: CollectionState) -> bool: + # easy:drive form,summon,gap closer,defensive option + # normal:3 of those things + # hard:2 of those things + hades_rules = { + "easy": self.kh2_list_any_sum([gap_closer, summons, defensive_tool, form_list], state) >= 4, + "normal": self.kh2_list_any_sum([gap_closer, summons, defensive_tool, form_list], state) >= 3, + "hard": self.kh2_list_any_sum([gap_closer, summons, defensive_tool, form_list], state) >= 2, + } + return hades_rules[self.fight_logic] + + def get_data_zexion_rules(self, state: CollectionState) -> bool: + # easy: final 7,firaga,scom,both donald limits, Reflega ,guard,2 gap closers,quick run level 3 + # normal:final 7,firaga, donald limit, Reflega ,guard,1 gap closers,quick run level 3 + # hard:final 5,fira, donald limit, reflect,gap closer,quick run level 2 + data_zexion_rules = { + "easy": self.kh2_dict_count(easy_data_zexion, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "normal": self.kh2_dict_count(normal_data_zexion, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "hard": self.kh2_dict_count(hard_data_zexion, state) and self.form_list_unlock(state, ItemName.FinalForm, 3, True) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + } + return data_zexion_rules[self.fight_logic] + + def get_thresholder_rules(self, state: CollectionState) -> bool: + # easy:drive form,black magic,defensive tool + # normal:2 of those things + # hard:defensive tool or drive form + thresholder_rules = { + "easy": self.kh2_list_any_sum([form_list, black_magic, defensive_tool], state) >= 3, + "normal": self.kh2_list_any_sum([form_list, black_magic, defensive_tool], state) >= 2, + "hard": self.kh2_list_any_sum([form_list, defensive_tool], state) >= 1, + } + return thresholder_rules[self.fight_logic] + + @staticmethod + def get_beast_rules(): + # fight is free + return True + + def get_dark_thorn_rules(self, state: CollectionState) -> bool: + # easy:drive form,defensive tool,gap closer + # normal:drive form,defensive tool + # hard:defensive tool + dark_thorn_rules = { + "easy": self.kh2_list_any_sum([form_list, gap_closer, defensive_tool], state) >= 3, + "normal": self.kh2_list_any_sum([form_list, defensive_tool], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool], state) >= 1, + } + return dark_thorn_rules[self.fight_logic] + + def get_xaldin_rules(self, state: CollectionState) -> bool: + # easy:guard,2 aerial modifier,valor/master/final + # normal:guard,1 aerial modifier + # hard:guard + xaldin_rules = { + "easy": self.kh2_list_any_sum([[ItemName.Guard], [ItemName.ValorForm, ItemName.MasterForm, ItemName.FinalForm]], state) >= 2 and self.kh2_list_count_sum(aerial_move, state) >= 2, + "normal": self.kh2_list_any_sum([aerial_move], state) >= 1 and state.has(ItemName.Guard, self.player), + "hard": state.has(ItemName.Guard, self.player), + } + return xaldin_rules[self.fight_logic] + + def get_data_xaldin_rules(self, state: CollectionState) -> bool: + # easy:final 7,firaga,2 air combo plus, finishing plus,guard,reflega,donald limit,high jump aerial dodge glide lvl 3,magnet,aerial dive,aerial spiral,hori slash,berserk charge + # normal:final 7,firaga, finishing plus,guard,reflega,donald limit,high jump aerial dodge glide lvl 3,magnet,aerial dive,aerial spiral,hori slash + # hard:final 5, fira, party limit, finishing plus,guard,high jump aerial dodge glide lvl 2,magnet,aerial dive + data_xaldin_rules = { + "easy": self.kh2_dict_count(easy_data_xaldin, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "normal": self.kh2_dict_count(normal_data_xaldin, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "hard": self.kh2_dict_count(hard_data_xaldin, state) and self.form_list_unlock(state, ItemName.FinalForm, 3, True) and self.kh2_has_any(party_limit, state), + } + return data_xaldin_rules[self.fight_logic] + + def get_hostile_program_rules(self, state: CollectionState) -> bool: + # easy:donald limit,reflect,drive form,summon + # normal:3 of those things + # hard: 2 of those things + hostile_program_rules = { + "easy": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 4, + "normal": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 3, + "hard": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 2, + } + return hostile_program_rules[self.fight_logic] + + def get_mcp_rules(self, state: CollectionState) -> bool: + # easy:donald limit,reflect,drive form,summon + # normal:3 of those things + # hard: 2 of those things + mcp_rules = { + "easy": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 4, + "normal": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 3, + "hard": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 2, + } + return mcp_rules[self.fight_logic] + + def get_data_larxene_rules(self, state: CollectionState) -> bool: + # easy: final 7,firaga,scom,both donald limits, Reflega,guard,2 gap closers,2 ground finishers,aerial dodge 3,glide 3 + # normal:final 7,firaga, donald limit, Reflega ,guard,1 gap closers,1 ground finisher,aerial dodge 3,glide 3 + # hard:final 5,fira, donald limit, reflect,gap closer,aerial dodge 2,glide 2 + data_larxene_rules = { + "easy": self.kh2_dict_count(easy_data_larxene, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "normal": self.kh2_dict_count(normal_data_larxene, state) and self.kh2_list_any_sum([gap_closer, ground_finisher, donald_limit], state) >= 3 and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "hard": self.kh2_dict_count(hard_data_larxene, state) and self.kh2_list_any_sum([gap_closer, donald_limit], state) >= 2 and self.form_list_unlock(state, ItemName.FinalForm, 3, True), + } + return data_larxene_rules[self.fight_logic] + + def get_prison_keeper_rules(self, state: CollectionState) -> bool: + # easy:defensive tool,drive form, party limit + # normal:two of those things + # hard:one of those things + prison_keeper_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, form_list, party_limit], state) >= 3, + "normal": self.kh2_list_any_sum([defensive_tool, form_list, party_limit], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool, form_list, party_limit], state) >= 1, + } + return prison_keeper_rules[self.fight_logic] + + @staticmethod + def get_oogie_rules(): + # fight is free + return True + + def get_experiment_rules(self, state: CollectionState) -> bool: + # easy:drive form,defensive tool,summon,party limit + # normal:3 of those things + # hard 2 of those things + experiment_rules = { + "easy": self.kh2_list_any_sum([form_list, defensive_tool, party_limit, summons], state) >= 4, + "normal": self.kh2_list_any_sum([form_list, defensive_tool, party_limit, summons], state) >= 3, + "hard": self.kh2_list_any_sum([form_list, defensive_tool, party_limit, summons], state) >= 2, + } + return experiment_rules[self.fight_logic] + + def get_data_vexen_rules(self, state: CollectionState) -> bool: + # easy: final 7,firaga,scom,both donald limits, Reflega,guard,2 gap closers,2 ground finishers,aerial dodge 3,glide 3,dodge roll 3,quick run 3 + # normal:final 7,firaga, donald limit, Reflega,guard,1 gap closers,1 ground finisher,aerial dodge 3,glide 3,dodge roll 3,quick run 3 + # hard:final 5,fira, donald limit, reflect,gap closer,aerial dodge 2,glide 2,dodge roll 2,quick run 2 + data_vexen_rules = { + "easy": self.kh2_dict_count(easy_data_vexen, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "normal": self.kh2_dict_count(normal_data_vexen, state) and self.kh2_list_any_sum([gap_closer, ground_finisher, donald_limit], state) >= 3 and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "hard": self.kh2_dict_count(hard_data_vexen, state) and self.kh2_list_any_sum([gap_closer, donald_limit], state) >= 2 and self.form_list_unlock(state, ItemName.FinalForm, 3, True), + } + return data_vexen_rules[self.fight_logic] + + def get_demyx_rules(self, state: CollectionState) -> bool: + # defensive option,drive form,party limit + # defensive option,drive form + # defensive option + demyx_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, form_list, party_limit], state) >= 3, + "normal": self.kh2_list_any_sum([defensive_tool, form_list], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool], state) >= 1, + } + return demyx_rules[self.fight_logic] + + def get_thousand_heartless_rules(self, state: CollectionState) -> bool: + # easy:scom,limit form,guard,magnera + # normal:limit form, guard + # hard:guard + thousand_heartless_rules = { + "easy": self.kh2_dict_count(easy_thousand_heartless_rules, state), + "normal": self.kh2_dict_count(normal_thousand_heartless_rules, state), + "hard": state.has(ItemName.Guard, self.player), + } + return thousand_heartless_rules[self.fight_logic] + + def get_data_demyx_rules(self, state: CollectionState) -> bool: + # easy:wisdom 7,1 form boosts,reflera,firaga,duck flare,guard,scom,finishing plus + # normal:remove form boost and scom + # hard:wisdom 6,reflect,guard,duck flare,fira,finishing plus + data_demyx_rules = { + "easy": self.kh2_dict_count(easy_data_demyx, state) and self.form_list_unlock(state, ItemName.WisdomForm, 5, True), + "normal": self.kh2_dict_count(normal_data_demyx, state) and self.form_list_unlock(state, ItemName.WisdomForm, 5, True), + "hard": self.kh2_dict_count(hard_data_demyx, state) and self.form_list_unlock(state, ItemName.WisdomForm, 4, True), + } + return data_demyx_rules[self.fight_logic] + + def get_sephiroth_rules(self, state: CollectionState) -> bool: + # easy:both gap closers,limit 5,reflega,guard,both 2 ground finishers,3 dodge roll,finishing plus,scom + # normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus + # hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus + sephiroth_rules = { + "easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1, + "normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "hard": self.kh2_dict_count(hard_sephiroth_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2, + } + return sephiroth_rules[self.fight_logic] + + def get_cor_first_fight_movement_rules(self, state: CollectionState) -> bool: + # easy: quick run 3 or wisdom 5 (wisdom has qr 3) + # normal: quick run 2 and aerial dodge 1 or wisdom 5 (wisdom has qr 3) + # hard: (quick run 1, aerial dodge 1) or (wisdom form and aerial dodge 1) + cor_first_fight_movement_rules = { + "easy": state.has(ItemName.QuickRun, self.player, 3) or self.form_list_unlock(state, ItemName.WisdomForm, 3, True), + "normal": self.kh2_dict_count({ItemName.QuickRun: 2, ItemName.AerialDodge: 1}, state) or self.form_list_unlock(state, ItemName.WisdomForm, 3, True), + "hard": self.kh2_has_all([ItemName.AerialDodge, ItemName.QuickRun], state) or self.kh2_has_all([ItemName.AerialDodge, ItemName.WisdomForm], state), + } + return cor_first_fight_movement_rules[self.fight_logic] + + def get_cor_first_fight_rules(self, state: CollectionState) -> bool: + # easy:have 5 of these things (reflega,stitch and chicken,final form,magnera,explosion,thundara) + # normal:have 3 of these things (reflega,stitch and chicken,final form,magnera,explosion,thundara) + # hard: reflect,stitch or chicken,final form + cor_first_fight_rules = { + "easy": self.kh2_dict_one_count(not_hard_cor_tools_dict, state) >= 5 or self.kh2_dict_one_count(not_hard_cor_tools_dict, state) >= 4 and self.form_list_unlock(state, ItemName.FinalForm, 1, True), + "normal": self.kh2_dict_one_count(not_hard_cor_tools_dict, state) >= 3 or self.kh2_dict_one_count(not_hard_cor_tools_dict, state) >= 2 and self.form_list_unlock(state, ItemName.FinalForm, 1, True), + "hard": state.has(ItemName.ReflectElement, self.player) and self.kh2_has_any([ItemName.Stitch, ItemName.ChickenLittle], state) and self.form_list_unlock(state, ItemName.FinalForm, 1, True), + } + return cor_first_fight_rules[self.fight_logic] + + def get_cor_skip_first_rules(self, state: CollectionState) -> bool: + # if option is not allow skips return false else run rules + if not self.multiworld.CorSkipToggle[self.player]: + return False + # easy: aerial dodge 3,master form,fire + # normal: aerial dodge 2, master form,fire + # hard:void cross(quick run 3,aerial dodge 1) + # or (quick run 2,aerial dodge 2 and magic) + # or (final form and (magic or combo master)) + # or (master form and (reflect or fire or thunder or combo master) + # wall rise(aerial dodge 1 and (final form lvl 3 or glide 2) or (master form and (1 of black magic or combo master) + void_cross_rules = { + "easy": state.has(ItemName.AerialDodge, self.player, 3) and self.kh2_has_all([ItemName.MasterForm, ItemName.FireElement], state), + "normal": state.has(ItemName.AerialDodge, self.player, 2) and self.kh2_has_all([ItemName.MasterForm, ItemName.FireElement], state), + "hard": (self.kh2_dict_count({ItemName.QuickRun: 3, ItemName.AerialDodge: 1}, state)) \ + or (self.kh2_dict_count({ItemName.QuickRun: 2, ItemName.AerialDodge: 2}, state) and self.kh2_has_any(magic, state)) \ + or (state.has(ItemName.FinalForm, self.player) and (self.kh2_has_any(magic, state) or state.has(ItemName.ComboMaster, self.player))) \ + or (state.has(ItemName.MasterForm, self.player) and (self.kh2_has_any([ItemName.ReflectElement, ItemName.FireElement, ItemName.ComboMaster], state))) + } + wall_rise_rules = { + "easy": True, + "normal": True, + "hard": state.has(ItemName.AerialDodge, self.player) and (self.form_list_unlock(state, ItemName.FinalForm, 1, True) or state.has(ItemName.Glide, self.player, 2)) + } + return void_cross_rules[self.fight_logic] and wall_rise_rules[self.fight_logic] + + def get_cor_second_fight_movement_rules(self, state: CollectionState) -> bool: + # easy: quick run 2, aerial dodge 3 or master form 5 + # normal: quick run 2, aerial dodge 2 or master 5 + # hard: (glide 1,aerial dodge 1 any magic) or (master 3 any magic) or glide 1 and aerial dodge 2 + + cor_second_fight_movement_rules = { + "easy": self.kh2_dict_count({ItemName.QuickRun: 2, ItemName.AerialDodge: 3}, state) or self.form_list_unlock(state, ItemName.MasterForm, 3, True), + "normal": self.kh2_dict_count({ItemName.QuickRun: 2, ItemName.AerialDodge: 2}, state) or self.form_list_unlock(state, ItemName.MasterForm, 3, True), + "hard": (self.kh2_has_all([ItemName.Glide, ItemName.AerialDodge], state) and self.kh2_has_any(magic, state)) \ + or (state.has(ItemName.MasterForm, self.player) and self.kh2_has_any(magic, state)) \ + or (state.has(ItemName.Glide, self.player) and state.has(ItemName.AerialDodge, self.player, 2)), + } + return cor_second_fight_movement_rules[self.fight_logic] + + def get_transport_fight_rules(self, state: CollectionState) -> bool: + # easy: reflega,stitch and chicken,final form,magnera,explosion,finishing leap,thundaga,2 donald limits + # normal: 7 of those things + # hard: 5 of those things + transport_fight_rules = { + "easy": self.kh2_dict_count(transport_tools_dict, state), + "normal": self.kh2_dict_one_count(transport_tools_dict, state) >= 7, + "hard": self.kh2_dict_one_count(transport_tools_dict, state) >= 5, + } + return transport_fight_rules[self.fight_logic] + + def get_transport_movement_rules(self, state: CollectionState) -> bool: + # easy:high jump 3,aerial dodge 3,glide 3 + # normal: high jump 2,glide 3,aerial dodge 2 + # hard: (hj 2,glide 2,ad 1,any magic) or hj 1,glide 2,ad 3 any magic or (any magic master form,ad) or hj lvl 1,glide 3,ad 1 + transport_movement_rules = { + "easy": self.kh2_dict_count({ItemName.HighJump: 3, ItemName.AerialDodge: 3, ItemName.Glide: 3}, state), + "normal": self.kh2_dict_count({ItemName.HighJump: 2, ItemName.AerialDodge: 2, ItemName.Glide: 3}, state), + "hard": (self.kh2_dict_count({ItemName.HighJump: 2, ItemName.AerialDodge: 1, ItemName.Glide: 2}, state) and self.kh2_has_any(magic, state)) \ + or (self.kh2_dict_count({ItemName.HighJump: 1, ItemName.Glide: 2, ItemName.AerialDodge: 3}, state) and self.kh2_has_any(magic, state)) \ + or (self.kh2_dict_count({ItemName.HighJump: 1, ItemName.Glide: 3, ItemName.AerialDodge: 1}, state)) \ + or (self.kh2_has_all([ItemName.MasterForm, ItemName.AerialDodge], state) and self.kh2_has_any(magic, state)), + } + return transport_movement_rules[self.fight_logic] + + def get_scar_rules(self, state: CollectionState) -> bool: + # easy: reflect,thunder,fire + # normal:reflect,fire + # hard:reflect + scar_rules = { + "easy": self.kh2_has_all([ItemName.ReflectElement, ItemName.ThunderElement, ItemName.FireElement], state), + "normal": self.kh2_has_all([ItemName.ReflectElement, ItemName.FireElement], state), + "hard": state.has(ItemName.ReflectElement, self.player), + } + return scar_rules[self.fight_logic] + + def get_groundshaker_rules(self, state: CollectionState) -> bool: + # easy:berserk charge,cure,2 air combo plus,reflect + # normal:berserk charge,reflect,cure + # hard:berserk charge or 2 air combo plus. reflect + groundshaker_rules = { + "easy": state.has(ItemName.AirComboPlus, self.player, 2) and self.kh2_has_all([ItemName.BerserkCharge, ItemName.CureElement, ItemName.ReflectElement], state), + "normal": self.kh2_has_all([ItemName.BerserkCharge, ItemName.ReflectElement, ItemName.CureElement], state), + "hard": (state.has(ItemName.BerserkCharge, self.player) or state.has(ItemName.AirComboPlus, self.player, 2)) and state.has(ItemName.ReflectElement, self.player), + } + return groundshaker_rules[self.fight_logic] + + def get_data_saix_rules(self, state: CollectionState) -> bool: + # easy:guard,2 gap closers,thunder,blizzard,2 donald limit,reflega,2 ground finisher,aerial dodge 3,glide 3,final 7,firaga,scom + # normal:guard,1 gap closers,thunder,blizzard,1 donald limit,reflega,1 ground finisher,aerial dodge 3,glide 3,final 7,firaga + # hard:aerial dodge 3,glide 3,guard,reflect,blizzard,1 gap closer,1 ground finisher + easy_data_rules = { + "easy": self.kh2_dict_count(easy_data_saix, state) and self.form_list_unlock(state, ItemName.FinalForm, 5), + "normal": self.kh2_dict_count(normal_data_saix, state) and self.kh2_list_any_sum([gap_closer, ground_finisher, donald_limit], state) >= 3 and self.form_list_unlock(state, ItemName.FinalForm, 5), + "hard": self.kh2_dict_count(hard_data_saix, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2 + } + return easy_data_rules[self.fight_logic] + + @staticmethod + def get_twilight_thorn_rules() -> bool: + return True + + @staticmethod + def get_axel_one_rules() -> bool: + return True + + @staticmethod + def get_axel_two_rules() -> bool: + return True + + def get_data_roxas_rules(self, state: CollectionState) -> bool: + # easy:both gap closers,limit 5,reflega,guard,both 2 ground finishers,3 dodge roll,finishing plus,scom + # normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus + # hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus + data_roxas_rules = { + "easy": self.kh2_dict_count(easy_data_roxas_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1, + "normal": self.kh2_dict_count(normal_data_roxas_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "hard": self.kh2_dict_count(hard_data_roxas_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2 + } + return data_roxas_rules[self.fight_logic] + + def get_data_axel_rules(self, state: CollectionState) -> bool: + # easy:both gap closers,limit 5,reflega,guard,both 2 ground finishers,3 dodge roll,finishing plus,scom,blizzaga + # normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus,blizzaga + # hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus,blizzara + data_axel_rules = { + "easy": self.kh2_dict_count(easy_data_axel_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1, + "normal": self.kh2_dict_count(normal_data_axel_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "hard": self.kh2_dict_count(hard_data_axel_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2 + } + return data_axel_rules[self.fight_logic] + + def get_roxas_rules(self, state: CollectionState) -> bool: + # easy:aerial dodge 1,glide 1, limit form,thunder,reflera,guard break,2 gap closers,finishing plus,blizzard + # normal:thunder,reflera,guard break,2 gap closers,finishing plus,blizzard + # hard:guard + roxas_rules = { + "easy": self.kh2_dict_count(easy_roxas_tools, state), + "normal": self.kh2_dict_count(normal_roxas_tools, state), + "hard": state.has(ItemName.Guard, self.player), + } + return roxas_rules[self.fight_logic] + + def get_xigbar_rules(self, state: CollectionState) -> bool: + # easy:final 4,horizontal slash,fira,finishing plus,glide 2,aerial dodge 2,quick run 2,guard,reflect + # normal:final 4,fira,finishing plus,glide 2,aerial dodge 2,quick run 2,guard,reflect + # hard:guard,quick run,finishing plus + xigbar_rules = { + "easy": self.kh2_dict_count(easy_xigbar_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 1) and self.kh2_has_any([ItemName.LightDarkness, ItemName.FinalForm], state), + "normal": self.kh2_dict_count(normal_xigbar_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 1), + "hard": self.kh2_has_all([ItemName.Guard, ItemName.QuickRun, ItemName.FinishingPlus], state), + } + return xigbar_rules[self.fight_logic] + + def get_luxord_rules(self, state: CollectionState) -> bool: + # easy:aerial dodge 1,glide 1,quickrun 2,guard,reflera,2 gap closers,ground finisher,limit form + # normal:aerial dodge 1,glide 1,quickrun 2,guard,reflera,1 gap closers,ground finisher + # hard:quick run,guard + luxord_rules = { + "easy": self.kh2_dict_count(easy_luxord_tools, state) and self.kh2_has_any(ground_finisher, state), + "normal": self.kh2_dict_count(normal_luxord_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2, + "hard": self.kh2_has_all([ItemName.Guard, ItemName.QuickRun], state) + } + return luxord_rules[self.fight_logic] + + def get_saix_rules(self, state: CollectionState) -> bool: + # easy:aerial dodge 1,glide 1,quickrun 2,guard,reflera,2 gap closers,ground finisher,limit form + # normal:aerial dodge 1,glide 1,quickrun 2,guard,reflera,1 gap closers,ground finisher + # hard:,guard + + saix_rules = { + "easy": self.kh2_dict_count(easy_saix_tools, state) and self.kh2_has_any(ground_finisher, state), + "normal": self.kh2_dict_count(normal_saix_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2, + "hard": self.kh2_has_all([ItemName.Guard], state) + } + return saix_rules[self.fight_logic] + + def get_xemnas_rules(self, state: CollectionState) -> bool: + # easy:aerial dodge 1,glide 1,quickrun 2,guard,reflera,2 gap closers,ground finisher,limit form + # normal:aerial dodge 1,glide 1,quickrun 2,guard,reflera,1 gap closers,ground finisher + # hard:,guard + xemnas_rules = { + "easy": self.kh2_dict_count(easy_xemnas_tools, state) and self.kh2_has_any(ground_finisher, state), + "normal": self.kh2_dict_count(normal_xemnas_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2, + "hard": self.kh2_has_all([ItemName.Guard], state) + } + return xemnas_rules[self.fight_logic] - # Forbid Abilities on popups due to game limitations - for location in exclusion_table["Popups"]: - forbid_items(world.get_location(location, player), exclusionItem_table["Ability"]) - forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"]) + def get_armored_xemnas_one_rules(self, state: CollectionState) -> bool: + # easy:donald limit,reflect,1 gap closer,ground finisher + # normal:reflect,gap closer,ground finisher + # hard:reflect + armored_xemnas_one_rules = { + "easy": self.kh2_list_any_sum([donald_limit, gap_closer, ground_finisher, {ItemName.ReflectElement}], state) >= 4, + "normal": self.kh2_list_any_sum([gap_closer, ground_finisher, {ItemName.ReflectElement}], state) >= 3, + "hard": state.has(ItemName.ReflectElement, self.player), + } + return armored_xemnas_one_rules[self.fight_logic] - for location in STT_Checks: - forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"]) + def get_armored_xemnas_two_rules(self, state: CollectionState) -> bool: + # easy:donald limit,reflect,1 gap closer,ground finisher + # normal:reflect,gap closer,ground finisher + # hard:reflect + armored_xemnas_two_rules = { + "easy": self.kh2_list_any_sum([gap_closer, ground_finisher, {ItemName.ReflectElement}, {ItemName.ThunderElement}], state) >= 4, + "normal": self.kh2_list_any_sum([gap_closer, ground_finisher, {ItemName.ReflectElement}], state) >= 3, + "hard": state.has(ItemName.ReflectElement, self.player), + } + return armored_xemnas_two_rules[self.fight_logic] - # Santa's house also breaks with stat ups - for location in {LocationName.SantasHouseChristmasTownMap, LocationName.SantasHouseAPBoost}: - forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"]) + def get_final_xemnas_rules(self, state: CollectionState) -> bool: + # easy:reflera,limit form,finishing plus,gap closer,guard + # normal:reflect,finishing plus,guard + # hard:guard + final_xemnas_rules = { + "easy": self.kh2_has_all([ItemName.LimitForm, ItemName.FinishingPlus, ItemName.Guard], state) and state.has(ItemName.ReflectElement, self.player, 2) and self.kh2_has_any(gap_closer, state), + "normal": self.kh2_has_all([ItemName.ReflectElement, ItemName.FinishingPlus, ItemName.Guard], state), + "hard": state.has(ItemName.Guard, self.player), + } + return final_xemnas_rules[self.fight_logic] - add_rule(world.get_location(LocationName.TransporttoRemembrance, player), - lambda state: state.kh_transport(player)) + def get_data_xemnas_rules(self, state: CollectionState) -> bool: + # easy:combo master,slapshot,reflega,2 ground finishers,both gap closers,finishing plus,guard,limit 5,scom,trinity limit + # normal:combo master,slapshot,reflega,2 ground finishers,both gap closers,finishing plus,guard,limit 5, + # hard:combo master,slapshot,reflera,1 ground finishers,1 gap closers,finishing plus,guard,limit form + data_xemnas_rules = { + "easy": self.kh2_dict_count(easy_data_xemnas, state) and self.kh2_list_count_sum(ground_finisher, state) >= 2 and self.kh2_can_reach(LocationName.Limitlvl5, state), + "normal": self.kh2_dict_count(normal_data_xemnas, state) and self.kh2_list_count_sum(ground_finisher, state) >= 2 and self.kh2_can_reach(LocationName.Limitlvl5, state), + "hard": self.kh2_dict_count(hard_data_xemnas, state) and self.kh2_list_any_sum([ground_finisher, gap_closer], state) >= 2 + } + return data_xemnas_rules[self.fight_logic] diff --git a/worlds/kh2/WorldLocations.py b/worlds/kh2/WorldLocations.py index 172874c2b71a..6df18fc800e3 100644 --- a/worlds/kh2/WorldLocations.py +++ b/worlds/kh2/WorldLocations.py @@ -96,6 +96,10 @@ class WorldLocationData(typing.NamedTuple): LocationName.LingeringWillBonus: WorldLocationData(0x370C, 6), LocationName.LingeringWillProofofConnection: WorldLocationData(0x370C, 6), LocationName.LingeringWillManifestIllusion: WorldLocationData(0x370C, 6), + + 'Lingering Will Bonus: Sora Slot 1': WorldLocationData(14092, 6), + 'Lingering Will Proof of Connection': WorldLocationData(14092, 6), + 'Lingering Will Manifest Illusion': WorldLocationData(14092, 6), } TR_Checks = { LocationName.CornerstoneHillMap: WorldLocationData(0x23B2, 0), @@ -226,6 +230,8 @@ class WorldLocationData(typing.NamedTuple): LocationName.DonaldXaldinGetBonus: WorldLocationData(0x3704, 4), LocationName.SecretAnsemReport4: WorldLocationData(0x1D31, 2), LocationName.XaldinDataDefenseBoost: WorldLocationData(0x1D34, 7), + + 'Data Xaldin': WorldLocationData(7476, 7), } SP_Checks = { LocationName.PitCellAreaMap: WorldLocationData(0x23CA, 2), @@ -351,6 +357,7 @@ class WorldLocationData(typing.NamedTuple): LocationName.RestorationSiteMoonRecipe: WorldLocationData(0x23C9, 3), LocationName.RestorationSiteAPBoost: WorldLocationData(0x23DB, 2), LocationName.DemyxHB: WorldLocationData(0x3707, 4), + '(HB) Demyx Bonus: Donald Slot 1': WorldLocationData(14087, 4), LocationName.DemyxHBGetBonus: WorldLocationData(0x3707, 4), LocationName.DonaldDemyxHBGetBonus: WorldLocationData(0x3707, 4), LocationName.FFFightsCureElement: WorldLocationData(0x1D14, 6), @@ -409,6 +416,25 @@ class WorldLocationData(typing.NamedTuple): LocationName.VexenASRoadtoDiscovery: WorldLocationData(0x370C, 0), LocationName.VexenDataLostIllusion: WorldLocationData(0x370C, 0), # LocationName.DemyxDataAPBoost: WorldLocationData(0x1D26, 5), + + 'Lexaeus Bonus: Sora Slot 1': WorldLocationData(14092, 1), + 'AS Lexaeus': WorldLocationData(14092, 1), + 'Data Lexaeus': WorldLocationData(14092, 1), + 'Marluxia Bonus: Sora Slot 1': WorldLocationData(14092, 3), + 'AS Marluxia': WorldLocationData(14092, 3), + 'Data Marluxia': WorldLocationData(14092, 3), + 'Zexion Bonus: Sora Slot 1': WorldLocationData(14092, 2), + 'Zexion Bonus: Goofy Slot 1': WorldLocationData(14092, 2), + 'AS Zexion': WorldLocationData(14092, 2), + 'Data Zexion': WorldLocationData(14092, 2), + 'Larxene Bonus: Sora Slot 1': WorldLocationData(14092, 4), + 'AS Larxene': WorldLocationData(14092, 4), + 'Data Larxene': WorldLocationData(14092, 4), + 'Vexen Bonus: Sora Slot 1': WorldLocationData(14092, 0), + 'AS Vexen': WorldLocationData(14092, 0), + 'Data Vexen': WorldLocationData(14092, 0), + 'Data Demyx': WorldLocationData(7462, 5), + LocationName.GardenofAssemblageMap: WorldLocationData(0x23DF, 1), LocationName.GoALostIllusion: WorldLocationData(0x23DF, 2), LocationName.ProofofNonexistence: WorldLocationData(0x23DF, 3), @@ -549,50 +575,97 @@ class WorldLocationData(typing.NamedTuple): LocationName.BetwixtandBetween: WorldLocationData(0x370B, 7), LocationName.BetwixtandBetweenBondofFlame: WorldLocationData(0x1CE9, 1), LocationName.AxelDataMagicBoost: WorldLocationData(0x1CEB, 4), + + 'Data Axel': WorldLocationData(7403, 4), } TWTNW_Checks = { - LocationName.FragmentCrossingMythrilStone: WorldLocationData(0x23CB, 4), - LocationName.FragmentCrossingMythrilCrystal: WorldLocationData(0x23CB, 5), - LocationName.FragmentCrossingAPBoost: WorldLocationData(0x23CB, 6), - LocationName.FragmentCrossingOrichalcum: WorldLocationData(0x23CB, 7), - LocationName.Roxas: WorldLocationData(0x370C, 5), - LocationName.RoxasGetBonus: WorldLocationData(0x370C, 5), - LocationName.RoxasSecretAnsemReport8: WorldLocationData(0x1ED1, 1), - LocationName.TwoBecomeOne: WorldLocationData(0x1ED1, 1), - LocationName.MemorysSkyscaperMythrilCrystal: WorldLocationData(0x23CD, 3), - LocationName.MemorysSkyscaperAPBoost: WorldLocationData(0x23DC, 0), - LocationName.MemorysSkyscaperMythrilStone: WorldLocationData(0x23DC, 1), - LocationName.TheBrinkofDespairDarkCityMap: WorldLocationData(0x23CA, 5), - LocationName.TheBrinkofDespairOrichalcumPlus: WorldLocationData(0x23DA, 2), - LocationName.NothingsCallMythrilGem: WorldLocationData(0x23CC, 0), - LocationName.NothingsCallOrichalcum: WorldLocationData(0x23CC, 1), - LocationName.TwilightsViewCosmicBelt: WorldLocationData(0x23CA, 6), - LocationName.XigbarBonus: WorldLocationData(0x3706, 7), - LocationName.XigbarSecretAnsemReport3: WorldLocationData(0x1ED2, 2), - LocationName.NaughtsSkywayMythrilGem: WorldLocationData(0x23CC, 2), - LocationName.NaughtsSkywayOrichalcum: WorldLocationData(0x23CC, 3), - LocationName.NaughtsSkywayMythrilCrystal: WorldLocationData(0x23CC, 4), - LocationName.Oblivion: WorldLocationData(0x1ED2, 4), - LocationName.CastleThatNeverWasMap: WorldLocationData(0x1ED2, 4), - LocationName.Luxord: WorldLocationData(0x3707, 0), - LocationName.LuxordGetBonus: WorldLocationData(0x3707, 0), - LocationName.LuxordSecretAnsemReport9: WorldLocationData(0x1ED2, 7), - LocationName.SaixBonus: WorldLocationData(0x3707, 1), - LocationName.SaixSecretAnsemReport12: WorldLocationData(0x1ED3, 2), - LocationName.PreXemnas1SecretAnsemReport11: WorldLocationData(0x1ED3, 6), - LocationName.RuinandCreationsPassageMythrilStone: WorldLocationData(0x23CC, 7), - LocationName.RuinandCreationsPassageAPBoost: WorldLocationData(0x23CD, 0), - LocationName.RuinandCreationsPassageMythrilCrystal: WorldLocationData(0x23CD, 1), - LocationName.RuinandCreationsPassageOrichalcum: WorldLocationData(0x23CD, 2), - LocationName.Xemnas1: WorldLocationData(0x3707, 2), - LocationName.Xemnas1GetBonus: WorldLocationData(0x3707, 2), - LocationName.Xemnas1SecretAnsemReport13: WorldLocationData(0x1ED4, 5), - LocationName.FinalXemnas: WorldLocationData(0x1ED8, 1), - LocationName.XemnasDataPowerBoost: WorldLocationData(0x1EDA, 2), - LocationName.XigbarDataDefenseBoost: WorldLocationData(0x1ED9, 7), - LocationName.SaixDataDefenseBoost: WorldLocationData(0x1EDA, 0), - LocationName.LuxordDataAPBoost: WorldLocationData(0x1EDA, 1), - LocationName.RoxasDataMagicBoost: WorldLocationData(0x1ED9, 6), + LocationName.FragmentCrossingMythrilStone: WorldLocationData(0x23CB, 4), + LocationName.FragmentCrossingMythrilCrystal: WorldLocationData(0x23CB, 5), + LocationName.FragmentCrossingAPBoost: WorldLocationData(0x23CB, 6), + LocationName.FragmentCrossingOrichalcum: WorldLocationData(0x23CB, 7), + LocationName.Roxas: WorldLocationData(0x370C, 5), + LocationName.RoxasGetBonus: WorldLocationData(0x370C, 5), + LocationName.RoxasSecretAnsemReport8: WorldLocationData(0x1ED1, 1), + LocationName.TwoBecomeOne: WorldLocationData(0x1ED1, 1), + LocationName.MemorysSkyscaperMythrilCrystal: WorldLocationData(0x23CD, 3), + LocationName.MemorysSkyscaperAPBoost: WorldLocationData(0x23DC, 0), + LocationName.MemorysSkyscaperMythrilStone: WorldLocationData(0x23DC, 1), + LocationName.TheBrinkofDespairDarkCityMap: WorldLocationData(0x23CA, 5), + LocationName.TheBrinkofDespairOrichalcumPlus: WorldLocationData(0x23DA, 2), + LocationName.NothingsCallMythrilGem: WorldLocationData(0x23CC, 0), + LocationName.NothingsCallOrichalcum: WorldLocationData(0x23CC, 1), + LocationName.TwilightsViewCosmicBelt: WorldLocationData(0x23CA, 6), + LocationName.XigbarBonus: WorldLocationData(0x3706, 7), + LocationName.XigbarSecretAnsemReport3: WorldLocationData(0x1ED2, 2), + LocationName.NaughtsSkywayMythrilGem: WorldLocationData(0x23CC, 2), + LocationName.NaughtsSkywayOrichalcum: WorldLocationData(0x23CC, 3), + LocationName.NaughtsSkywayMythrilCrystal: WorldLocationData(0x23CC, 4), + LocationName.Oblivion: WorldLocationData(0x1ED2, 4), + LocationName.CastleThatNeverWasMap: WorldLocationData(0x1ED2, 4), + LocationName.Luxord: WorldLocationData(0x3707, 0), + LocationName.LuxordGetBonus: WorldLocationData(0x3707, 0), + LocationName.LuxordSecretAnsemReport9: WorldLocationData(0x1ED2, 7), + LocationName.SaixBonus: WorldLocationData(0x3707, 1), + LocationName.SaixSecretAnsemReport12: WorldLocationData(0x1ED3, 2), + LocationName.PreXemnas1SecretAnsemReport11: WorldLocationData(0x1ED3, 6), + LocationName.RuinandCreationsPassageMythrilStone: WorldLocationData(0x23CC, 7), + LocationName.RuinandCreationsPassageAPBoost: WorldLocationData(0x23CD, 0), + LocationName.RuinandCreationsPassageMythrilCrystal: WorldLocationData(0x23CD, 1), + LocationName.RuinandCreationsPassageOrichalcum: WorldLocationData(0x23CD, 2), + LocationName.Xemnas1: WorldLocationData(0x3707, 2), + LocationName.Xemnas1GetBonus: WorldLocationData(0x3707, 2), + LocationName.Xemnas1SecretAnsemReport13: WorldLocationData(0x1ED4, 5), + LocationName.FinalXemnas: WorldLocationData(0x1ED8, 1), + LocationName.XemnasDataPowerBoost: WorldLocationData(0x1EDA, 2), + LocationName.XigbarDataDefenseBoost: WorldLocationData(0x1ED9, 7), + LocationName.SaixDataDefenseBoost: WorldLocationData(0x1EDA, 0), + LocationName.LuxordDataAPBoost: WorldLocationData(0x1EDA, 1), + LocationName.RoxasDataMagicBoost: WorldLocationData(0x1ED9, 6), + + "(TWTNW) Roxas Bonus: Sora Slot 1": WorldLocationData(14092, 5), + "(TWTNW) Roxas Bonus: Sora Slot 2": WorldLocationData(14092, 5), + "(TWTNW) Roxas Secret Ansem Report 8": WorldLocationData(7889, 1), + "(TWTNW) Two Become One": WorldLocationData(7889, 1), + "(TWTNW) Memory's Skyscaper Mythril Crystal": WorldLocationData(9165, 3), + "(TWTNW) Memory's Skyscaper AP Boost": WorldLocationData(9180, 0), + "(TWTNW) Memory's Skyscaper Mythril Stone": WorldLocationData(9180, 1), + "(TWTNW) The Brink of Despair Dark City Map": WorldLocationData(9162, 5), + "(TWTNW) The Brink of Despair Orichalcum+": WorldLocationData(9178, 2), + "(TWTNW) Nothing's Call Mythril Gem": WorldLocationData(9164, 0), + "(TWTNW) Nothing's Call Orichalcum": WorldLocationData(9164, 1), + "(TWTNW) Twilight's View Cosmic Belt": WorldLocationData(9162, 6), + "(TWTNW) Xigbar Bonus: Sora Slot 1": WorldLocationData(14086, 7), + "(TWTNW) Xigbar Secret Ansem Report 3": WorldLocationData(7890, 2), + "(TWTNW) Naught's Skyway Mythril Gem": WorldLocationData(9164, 2), + "(TWTNW) Naught's Skyway Orichalcum": WorldLocationData(9164, 3), + "(TWTNW) Naught's Skyway Mythril Crystal": WorldLocationData(9164, 4), + "(TWTNW) Oblivion": WorldLocationData(7890, 4), + "(TWTNW) Castle That Never Was Map": WorldLocationData(7890, 4), + "(TWTNW) Luxord": WorldLocationData(14087, 0), + "(TWTNW) Luxord Bonus: Sora Slot 1": WorldLocationData(14087, 0), + "(TWTNW) Luxord Secret Ansem Report 9": WorldLocationData(7890, 7), + "(TWTNW) Saix Bonus: Sora Slot 1": WorldLocationData(14087, 1), + "(TWTNW) Saix Secret Ansem Report 12": WorldLocationData(7891, 2), + "(TWTNW) Secret Ansem Report 11 (Pre-Xemnas 1)": WorldLocationData(7891, 6), + "(TWTNW) Ruin and Creation's Passage Mythril Stone": WorldLocationData(9164, 7), + "(TWTNW) Ruin and Creation's Passage AP Boost": WorldLocationData(9165, 0), + "(TWTNW) Ruin and Creation's Passage Mythril Crystal": WorldLocationData(9165, 1), + "(TWTNW) Ruin and Creation's Passage Orichalcum": WorldLocationData(9165, 2), + "(TWTNW) Xemnas 1 Bonus: Sora Slot 1": WorldLocationData(14087, 2), + "(TWTNW) Xemnas 1 Bonus: Sora Slot 2": WorldLocationData(14087, 2), + "(TWTNW) Xemnas 1 Secret Ansem Report 13": WorldLocationData(7892, 5), + "Data Xemnas": WorldLocationData(7898, 2), + "Data Xigbar": WorldLocationData(7897, 7), + "Data Saix": WorldLocationData(7898, 0), + "Data Luxord": WorldLocationData(7898, 1), + "Data Roxas": WorldLocationData(7897, 6), + +} +Atlantica_Checks = { + LocationName.UnderseaKingdomMap: WorldLocationData(0x1DF4, 2), + LocationName.MysteriousAbyss: WorldLocationData(0x1DF5, 3), + LocationName.MusicalOrichalcumPlus: WorldLocationData(0x1DF4, 1), + LocationName.MusicalBlizzardElement: WorldLocationData(0x1DF4, 1) } SoraLevels = { # LocationName.Lvl1: WorldLocationData(0xFFFF,1), @@ -743,6 +816,15 @@ class WorldLocationData(typing.NamedTuple): LocationName.Finallvl6: WorldLocationData(0x33D6, 6), LocationName.Finallvl7: WorldLocationData(0x33D6, 7), +} +SummonLevels = { + LocationName.Summonlvl2: WorldLocationData(0x3526, 2), + LocationName.Summonlvl3: WorldLocationData(0x3526, 3), + LocationName.Summonlvl4: WorldLocationData(0x3526, 4), + LocationName.Summonlvl5: WorldLocationData(0x3526, 5), + LocationName.Summonlvl6: WorldLocationData(0x3526, 6), + LocationName.Summonlvl7: WorldLocationData(0x3526, 7), + } weaponSlots = { LocationName.AdamantShield: WorldLocationData(0x35E6, 1), @@ -817,7 +899,6 @@ class WorldLocationData(typing.NamedTuple): all_world_locations = { **TWTNW_Checks, **TT_Checks, - **TT_Checks, **HB_Checks, **BC_Checks, **Oc_Checks, @@ -828,11 +909,9 @@ class WorldLocationData(typing.NamedTuple): **DC_Checks, **TR_Checks, **HT_Checks, - **HB_Checks, **PR_Checks, **SP_Checks, - **TWTNW_Checks, - **HB_Checks, + **Atlantica_Checks, } levels_locations = { diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 23075a2084df..69f844f45a68 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -1,15 +1,25 @@ -from BaseClasses import Tutorial, ItemClassification import logging +from typing import List +from BaseClasses import Tutorial, ItemClassification +from Fill import fill_restrictive +from worlds.LauncherComponents import Component, components, Type, launch_subprocess +from worlds.AutoWorld import World, WebWorld from .Items import * -from .Locations import all_locations, setup_locations, exclusion_table, AllWeaponSlot -from .Names import ItemName, LocationName +from .Locations import * +from .Names import ItemName, LocationName, RegionName from .OpenKH import patch_kh2 -from .Options import KH2_Options +from .Options import KingdomHearts2Options from .Regions import create_regions, connect_regions -from .Rules import set_rules -from ..AutoWorld import World, WebWorld -from .logic import KH2Logic +from .Rules import * + + +def launch_client(): + from .Client import launch + launch_subprocess(launch, name="KH2Client") + + +components.append(Component("KH2 Client", "KH2Client", func=launch_client, component_type=Type.CLIENT)) class KingdomHearts2Web(WebWorld): @@ -23,99 +33,119 @@ class KingdomHearts2Web(WebWorld): )] -# noinspection PyUnresolvedReferences class KH2World(World): """ Kingdom Hearts II is an action role-playing game developed and published by Square Enix and released in 2005. It is the sequel to Kingdom Hearts and Kingdom Hearts: Chain of Memories, and like the two previous games, focuses on Sora and his friends' continued battle against the Darkness. """ - game: str = "Kingdom Hearts 2" + game = "Kingdom Hearts 2" web = KingdomHearts2Web() - data_version = 1 - required_client_version = (0, 4, 0) - option_definitions = KH2_Options - item_name_to_id = {name: data.code for name, data in item_dictionary_table.items()} - location_name_to_id = {item_name: data.code for item_name, data in all_locations.items() if data.code} + + required_client_version = (0, 4, 4) + options_dataclass = KingdomHearts2Options + options: KingdomHearts2Options + item_name_to_id = {item: item_id + for item_id, item in enumerate(item_dictionary_table.keys(), 0x130000)} + location_name_to_id = {item: location + for location, item in enumerate(all_locations.keys(), 0x130000)} item_name_groups = item_groups + visitlocking_dict: Dict[str, int] + plando_locations: Dict[str, str] + lucky_emblem_amount: int + lucky_emblem_required: int + bounties_required: int + bounties_amount: int + filler_items: List[str] + item_quantity_dict: Dict[str, int] + local_items: Dict[int, int] + sora_ability_dict: Dict[str, int] + goofy_ability_dict: Dict[str, int] + donald_ability_dict: Dict[str, int] + total_locations: int + + # growth_list: list[str] + def __init__(self, multiworld: "MultiWorld", player: int): super().__init__(multiworld, player) - self.valid_abilities = None - self.visitlocking_dict = None - self.plando_locations = None - self.luckyemblemamount = None - self.luckyemblemrequired = None - self.BountiesRequired = None - self.BountiesAmount = None - self.hitlist = None - self.LocalItems = {} - self.RandomSuperBoss = list() - self.filler_items = list() - self.item_quantity_dict = {} - self.donald_ability_pool = list() - self.goofy_ability_pool = list() - self.sora_keyblade_ability_pool = list() - self.keyblade_slot_copy = list(Locations.Keyblade_Slots.keys()) - self.keyblade_slot_copy.remove(LocationName.KingdomKeySlot) - self.totalLocations = len(all_locations.items()) + # random_super_boss_list List[str] + # has to be in __init__ or else other players affect each other's bounties + self.random_super_boss_list = list() self.growth_list = list() - for x in range(4): - self.growth_list.extend(Movement_Table.keys()) - self.slotDataDuping = set() - self.localItems = dict() + # lists of KH2Item + self.keyblade_ability_pool = list() + + self.goofy_get_bonus_abilities = list() + self.goofy_weapon_abilities = list() + self.donald_get_bonus_abilities = list() + self.donald_weapon_abilities = list() + + self.slot_data_goofy_weapon = dict() + self.slot_data_sora_weapon = dict() + self.slot_data_donald_weapon = dict() def fill_slot_data(self) -> dict: - for values in CheckDupingItems.values(): - if isinstance(values, set): - self.slotDataDuping = self.slotDataDuping.union(values) - else: - for inner_values in values.values(): - self.slotDataDuping = self.slotDataDuping.union(inner_values) - self.LocalItems = {location.address: item_dictionary_table[location.item.name].code - for location in self.multiworld.get_filled_locations(self.player) - if location.item.player == self.player - and location.item.name in self.slotDataDuping - and location.name not in AllWeaponSlot} - - return {"hitlist": self.hitlist, - "LocalItems": self.LocalItems, - "Goal": self.multiworld.Goal[self.player].value, - "FinalXemnas": self.multiworld.FinalXemnas[self.player].value, - "LuckyEmblemsRequired": self.multiworld.LuckyEmblemsRequired[self.player].value, - "BountyRequired": self.multiworld.BountyRequired[self.player].value} - - def create_item(self, name: str, ) -> Item: - data = item_dictionary_table[name] - if name in Progression_Dicts["Progression"]: + for ability in self.slot_data_sora_weapon: + if ability in self.sora_ability_dict and self.sora_ability_dict[ability] >= 1: + self.sora_ability_dict[ability] -= 1 + self.donald_ability_dict = {k: v.quantity for k, v in DonaldAbility_Table.items()} + for ability in self.slot_data_donald_weapon: + if ability in self.donald_ability_dict and self.donald_ability_dict[ability] >= 1: + self.donald_ability_dict[ability] -= 1 + self.goofy_ability_dict = {k: v.quantity for k, v in GoofyAbility_Table.items()} + for ability in self.slot_data_goofy_weapon: + if ability in self.goofy_ability_dict and self.goofy_ability_dict[ability] >= 1: + self.goofy_ability_dict[ability] -= 1 + + slot_data = self.options.as_dict("Goal", "FinalXemnas", "LuckyEmblemsRequired", "BountyRequired") + slot_data.update({ + "hitlist": [], # remove this after next update + "PoptrackerVersionCheck": 4.3, + "KeybladeAbilities": self.sora_ability_dict, + "StaffAbilities": self.donald_ability_dict, + "ShieldAbilities": self.goofy_ability_dict, + }) + return slot_data + + def create_item(self, name: str) -> Item: + """ + Returns created KH2Item + """ + # data = item_dictionary_table[name] + if name in progression_set: item_classification = ItemClassification.progression + elif name in useful_set: + item_classification = ItemClassification.useful else: item_classification = ItemClassification.filler - created_item = KH2Item(name, item_classification, data.code, self.player) + created_item = KH2Item(name, item_classification, self.item_name_to_id[name], self.player) return created_item def create_items(self) -> None: - self.visitlocking_dict = Progression_Dicts["AllVisitLocking"].copy() - if self.multiworld.Schmovement[self.player] != "level_0": - for _ in range(self.multiworld.Schmovement[self.player].value): - for name in {ItemName.HighJump, ItemName.QuickRun, ItemName.DodgeRoll, ItemName.AerialDodge, - ItemName.Glide}: + """ + Fills ItemPool and manages schmovement, random growth, visit locking and random starting visit locking. + """ + self.visitlocking_dict = visit_locking_dict["AllVisitLocking"].copy() + if self.options.Schmovement != "level_0": + for _ in range(self.options.Schmovement.value): + for name in Movement_Table.keys(): self.item_quantity_dict[name] -= 1 self.growth_list.remove(name) self.multiworld.push_precollected(self.create_item(name)) - if self.multiworld.RandomGrowth[self.player] != 0: - max_growth = min(self.multiworld.RandomGrowth[self.player].value, len(self.growth_list)) + if self.options.RandomGrowth: + max_growth = min(self.options.RandomGrowth.value, len(self.growth_list)) for _ in range(max_growth): - random_growth = self.multiworld.per_slot_randoms[self.player].choice(self.growth_list) + random_growth = self.random.choice(self.growth_list) self.item_quantity_dict[random_growth] -= 1 self.growth_list.remove(random_growth) self.multiworld.push_precollected(self.create_item(random_growth)) - if self.multiworld.Visitlocking[self.player] == "no_visit_locking": - for item, amount in Progression_Dicts["AllVisitLocking"].items(): + if self.options.Visitlocking == "no_visit_locking": + for item, amount in visit_locking_dict["AllVisitLocking"].items(): for _ in range(amount): self.multiworld.push_precollected(self.create_item(item)) self.item_quantity_dict[item] -= 1 @@ -123,19 +153,19 @@ def create_items(self) -> None: if self.visitlocking_dict[item] == 0: self.visitlocking_dict.pop(item) - elif self.multiworld.Visitlocking[self.player] == "second_visit_locking": - for item in Progression_Dicts["2VisitLocking"]: + elif self.options.Visitlocking == "second_visit_locking": + for item in visit_locking_dict["2VisitLocking"]: self.item_quantity_dict[item] -= 1 self.visitlocking_dict[item] -= 1 if self.visitlocking_dict[item] == 0: self.visitlocking_dict.pop(item) self.multiworld.push_precollected(self.create_item(item)) - for _ in range(self.multiworld.RandomVisitLockingItem[self.player].value): + for _ in range(self.options.RandomVisitLockingItem.value): if sum(self.visitlocking_dict.values()) <= 0: break visitlocking_set = list(self.visitlocking_dict.keys()) - item = self.multiworld.per_slot_randoms[self.player].choice(visitlocking_set) + item = self.random.choice(visitlocking_set) self.item_quantity_dict[item] -= 1 self.visitlocking_dict[item] -= 1 if self.visitlocking_dict[item] == 0: @@ -145,175 +175,258 @@ def create_items(self) -> None: itempool = [self.create_item(item) for item, data in self.item_quantity_dict.items() for _ in range(data)] # Creating filler for unfilled locations - itempool += [self.create_filler() - for _ in range(self.totalLocations - len(itempool))] + itempool += [self.create_filler() for _ in range(self.total_locations - len(itempool))] + self.multiworld.itempool += itempool def generate_early(self) -> None: - # Item Quantity dict because Abilities can be a problem for KH2's Software. + """ + Determines the quantity of items and maps plando locations to items. + """ + # Item: Quantity Map + # Example. Quick Run: 4 + self.total_locations = len(all_locations.keys()) + for x in range(4): + self.growth_list.extend(Movement_Table.keys()) + self.item_quantity_dict = {item: data.quantity for item, data in item_dictionary_table.items()} + self.sora_ability_dict = {k: v.quantity for dic in [SupportAbility_Table, ActionAbility_Table] for k, v in + dic.items()} # Dictionary to mark locations with their plandoed item # Example. Final Xemnas: Victory + # 3 random support abilities because there are left over slots + support_abilities = list(SupportAbility_Table.keys()) + for _ in range(6): + random_support_ability = self.random.choice(support_abilities) + self.item_quantity_dict[random_support_ability] += 1 + self.sora_ability_dict[random_support_ability] += 1 + self.plando_locations = dict() - self.hitlist = [] self.starting_invo_verify() + + for k, v in self.options.CustomItemPoolQuantity.value.items(): + # kh2's items cannot hold more than a byte + if 255 > v > self.item_quantity_dict[k] and k in default_itempool_option.keys(): + self.item_quantity_dict[k] = v + elif 255 <= v: + logging.warning( + f"{self.player} has too many {k} in their CustomItemPool setting. Setting to default quantity") # Option to turn off Promise Charm Item - if not self.multiworld.Promise_Charm[self.player]: - self.item_quantity_dict[ItemName.PromiseCharm] = 0 + if not self.options.Promise_Charm: + del self.item_quantity_dict[ItemName.PromiseCharm] + + if not self.options.AntiForm: + del self.item_quantity_dict[ItemName.AntiForm] self.set_excluded_locations() - if self.multiworld.Goal[self.player] == "lucky_emblem_hunt": - self.luckyemblemamount = self.multiworld.LuckyEmblemsAmount[self.player].value - self.luckyemblemrequired = self.multiworld.LuckyEmblemsRequired[self.player].value + if self.options.Goal not in ["hitlist", "three_proofs"]: + self.lucky_emblem_amount = self.options.LuckyEmblemsAmount.value + self.lucky_emblem_required = self.options.LuckyEmblemsRequired.value self.emblem_verify() # hitlist - elif self.multiworld.Goal[self.player] == "hitlist": - self.RandomSuperBoss.extend(exclusion_table["Hitlist"]) - self.BountiesAmount = self.multiworld.BountyAmount[self.player].value - self.BountiesRequired = self.multiworld.BountyRequired[self.player].value + if self.options.Goal not in ["lucky_emblem_hunt", "three_proofs"]: + self.random_super_boss_list.extend(exclusion_table["Hitlist"]) + self.bounties_amount = self.options.BountyAmount.value + self.bounties_required = self.options.BountyRequired.value self.hitlist_verify() - for bounty in range(self.BountiesAmount): - randomBoss = self.multiworld.per_slot_randoms[self.player].choice(self.RandomSuperBoss) - self.plando_locations[randomBoss] = ItemName.Bounty - self.hitlist.append(self.location_name_to_id[randomBoss]) - self.RandomSuperBoss.remove(randomBoss) - self.totalLocations -= 1 - - self.donald_fill() - self.goofy_fill() - self.keyblade_fill() + prio_hitlist = [location for location in self.multiworld.priority_locations[self.player].value if + location in self.random_super_boss_list] + for bounty in range(self.options.BountyAmount.value): + if prio_hitlist: + random_boss = self.random.choice(prio_hitlist) + prio_hitlist.remove(random_boss) + else: + random_boss = self.random.choice(self.random_super_boss_list) + self.plando_locations[random_boss] = ItemName.Bounty + self.random_super_boss_list.remove(random_boss) + self.total_locations -= 1 + + self.donald_gen_early() + self.goofy_gen_early() + self.keyblade_gen_early() if self.multiworld.FinalXemnas[self.player]: self.plando_locations[LocationName.FinalXemnas] = ItemName.Victory else: self.plando_locations[LocationName.FinalXemnas] = self.create_filler().name + self.total_locations -= 1 - # same item placed because you can only get one of these 2 locations - # they are both under the same flag so the player gets both locations just one of the two items - random_stt_item = self.create_filler().name - for location in {LocationName.JunkMedal, LocationName.JunkMedal}: - self.plando_locations[location] = random_stt_item - self.level_subtraction() - # subtraction from final xemnas and stt - self.totalLocations -= 3 + if self.options.WeaponSlotStartHint: + for location in all_weapon_slot: + self.multiworld.start_location_hints[self.player].value.add(location) + + if self.options.FillerItemsLocal: + for item in filler_items: + self.multiworld.local_items[self.player].value.add(item) + # By imitating remote this doesn't have to be plandoded filler anymore + # for location in {LocationName.JunkMedal, LocationName.JunkMedal}: + # self.plando_locations[location] = random_stt_item + if not self.options.SummonLevelLocationToggle: + self.total_locations -= 6 + + self.total_locations -= self.level_subtraction() def pre_fill(self): + """ + Plandoing Events and Fill_Restrictive for donald,goofy and sora + """ + self.donald_pre_fill() + self.goofy_pre_fill() + self.keyblade_pre_fill() + for location, item in self.plando_locations.items(): self.multiworld.get_location(location, self.player).place_locked_item( self.create_item(item)) def create_regions(self): - location_table = setup_locations() - create_regions(self.multiworld, self.player, location_table) - connect_regions(self.multiworld, self.player) + """ + Creates the Regions and Connects them. + """ + create_regions(self) + connect_regions(self) def set_rules(self): - set_rules(self.multiworld, self.player) + """ + Sets the Logic for the Regions and Locations. + """ + universal_logic = Rules.KH2WorldRules(self) + form_logic = Rules.KH2FormRules(self) + fight_rules = Rules.KH2FightRules(self) + fight_rules.set_kh2_fight_rules() + universal_logic.set_kh2_rules() + form_logic.set_kh2_form_rules() def generate_output(self, output_directory: str): + """ + Generates the .zip for OpenKH (The KH Mod Manager) + """ patch_kh2(self, output_directory) - def donald_fill(self): - for item in DonaldAbility_Table: - data = self.item_quantity_dict[item] - for _ in range(data): - self.donald_ability_pool.append(item) - self.item_quantity_dict[item] = 0 - # 32 is the amount of donald abilities - while len(self.donald_ability_pool) < 32: - self.donald_ability_pool.append( - self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool)) - # Placing Donald Abilities on donald locations - for donaldLocation in Locations.Donald_Checks.keys(): - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool) - self.plando_locations[donaldLocation] = random_ability - self.totalLocations -= 1 - self.donald_ability_pool.remove(random_ability) - - def goofy_fill(self): - for item in GoofyAbility_Table.keys(): - data = self.item_quantity_dict[item] - for _ in range(data): - self.goofy_ability_pool.append(item) - self.item_quantity_dict[item] = 0 - # 32 is the amount of goofy abilities - while len(self.goofy_ability_pool) < 33: - self.goofy_ability_pool.append( - self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool)) - # Placing Goofy Abilities on goofy locations - for goofyLocation in Locations.Goofy_Checks.keys(): - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool) - self.plando_locations[goofyLocation] = random_ability - self.totalLocations -= 1 - self.goofy_ability_pool.remove(random_ability) - - def keyblade_fill(self): - if self.multiworld.KeybladeAbilities[self.player] == "support": - self.sora_keyblade_ability_pool = { - **{item: data for item, data in self.item_quantity_dict.items() if item in SupportAbility_Table}, - **{ItemName.NegativeCombo: 1, ItemName.AirComboPlus: 1, ItemName.ComboPlus: 1, - ItemName.FinishingPlus: 1}} - - elif self.multiworld.KeybladeAbilities[self.player] == "action": - self.sora_keyblade_ability_pool = {item: data for item, data in self.item_quantity_dict.items() if - item in ActionAbility_Table} - # there are too little action abilities so 2 random support abilities are placed - for _ in range(3): - randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice( - list(SupportAbility_Table.keys())) - while randomSupportAbility in self.sora_keyblade_ability_pool: - randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice( - list(SupportAbility_Table.keys())) - self.sora_keyblade_ability_pool[randomSupportAbility] = 1 - else: - # both action and support on keyblades. - # TODO: make option to just exclude scom - self.sora_keyblade_ability_pool = { - **{item: data for item, data in self.item_quantity_dict.items() if item in SupportAbility_Table}, - **{item: data for item, data in self.item_quantity_dict.items() if item in ActionAbility_Table}, - **{ItemName.NegativeCombo: 1, ItemName.AirComboPlus: 1, ItemName.ComboPlus: 1, - ItemName.FinishingPlus: 1}} - - for ability in self.multiworld.BlacklistKeyblade[self.player].value: - if ability in self.sora_keyblade_ability_pool: - self.sora_keyblade_ability_pool.pop(ability) - - # magic number for amount of keyblades - if sum(self.sora_keyblade_ability_pool.values()) < 28: - raise Exception( - f"{self.multiworld.get_file_safe_player_name(self.player)} has too little Keyblade Abilities in the Keyblade Pool") - - self.valid_abilities = list(self.sora_keyblade_ability_pool.keys()) - # Kingdom Key cannot have No Experience so plandoed here instead of checking 26 times if its kingdom key - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.valid_abilities) - while random_ability == ItemName.NoExperience: - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.valid_abilities) - self.plando_locations[LocationName.KingdomKeySlot] = random_ability - self.item_quantity_dict[random_ability] -= 1 - self.sora_keyblade_ability_pool[random_ability] -= 1 - if self.sora_keyblade_ability_pool[random_ability] == 0: - self.valid_abilities.remove(random_ability) - self.sora_keyblade_ability_pool.pop(random_ability) - - # plando keyblades because they can only have abilities - for keyblade in self.keyblade_slot_copy: - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.valid_abilities) - self.plando_locations[keyblade] = random_ability + def donald_gen_early(self): + random_prog_ability = self.random.choice([ItemName.Fantasia, ItemName.FlareForce]) + donald_master_ability = [donald_ability for donald_ability in DonaldAbility_Table.keys() for _ in + range(self.item_quantity_dict[donald_ability]) if + donald_ability != random_prog_ability] + self.donald_weapon_abilities = [] + self.donald_get_bonus_abilities = [] + # fill goofy weapons first + for _ in range(15): + random_ability = self.random.choice(donald_master_ability) + donald_master_ability.remove(random_ability) + self.donald_weapon_abilities += [self.create_item(random_ability)] self.item_quantity_dict[random_ability] -= 1 - self.sora_keyblade_ability_pool[random_ability] -= 1 - if self.sora_keyblade_ability_pool[random_ability] == 0: - self.valid_abilities.remove(random_ability) - self.sora_keyblade_ability_pool.pop(random_ability) - self.totalLocations -= 1 + self.total_locations -= 1 + self.slot_data_donald_weapon = [item_name.name for item_name in self.donald_weapon_abilities] + if not self.multiworld.DonaldGoofyStatsanity[self.player]: + # pre plando donald get bonuses + self.donald_get_bonus_abilities += [self.create_item(random_prog_ability)] + self.total_locations -= 1 + for item_name in donald_master_ability: + self.donald_get_bonus_abilities += [self.create_item(item_name)] + self.item_quantity_dict[item_name] -= 1 + self.total_locations -= 1 + + def goofy_gen_early(self): + random_prog_ability = self.random.choice([ItemName.Teamwork, ItemName.TornadoFusion]) + goofy_master_ability = [goofy_ability for goofy_ability in GoofyAbility_Table.keys() for _ in + range(self.item_quantity_dict[goofy_ability]) if goofy_ability != random_prog_ability] + self.goofy_weapon_abilities = [] + self.goofy_get_bonus_abilities = [] + # fill goofy weapons first + for _ in range(15): + random_ability = self.random.choice(goofy_master_ability) + goofy_master_ability.remove(random_ability) + self.goofy_weapon_abilities += [self.create_item(random_ability)] + self.item_quantity_dict[random_ability] -= 1 + self.total_locations -= 1 + + self.slot_data_goofy_weapon = [item_name.name for item_name in self.goofy_weapon_abilities] + + if not self.options.DonaldGoofyStatsanity: + # pre plando goofy get bonuses + self.goofy_get_bonus_abilities += [self.create_item(random_prog_ability)] + self.total_locations -= 1 + for item_name in goofy_master_ability: + self.goofy_get_bonus_abilities += [self.create_item(item_name)] + self.item_quantity_dict[item_name] -= 1 + self.total_locations -= 1 + + def keyblade_gen_early(self): + keyblade_master_ability = [ability for ability in SupportAbility_Table.keys() if ability not in progression_set + for _ in range(self.item_quantity_dict[ability])] + self.keyblade_ability_pool = [] + + for _ in range(len(Keyblade_Slots)): + random_ability = self.random.choice(keyblade_master_ability) + keyblade_master_ability.remove(random_ability) + self.keyblade_ability_pool += [self.create_item(random_ability)] + self.item_quantity_dict[random_ability] -= 1 + self.total_locations -= 1 + self.slot_data_sora_weapon = [item_name.name for item_name in self.keyblade_ability_pool] + + def goofy_pre_fill(self): + """ + Removes donald locations from the location pool maps random donald items to be plandoded. + """ + goofy_weapon_location_list = [self.multiworld.get_location(location, self.player) for location in + Goofy_Checks.keys() if Goofy_Checks[location].yml == "Keyblade"] + # take one of the 2 out + # randomize the list with only + for location in goofy_weapon_location_list: + random_ability = self.random.choice(self.goofy_weapon_abilities) + location.place_locked_item(random_ability) + self.goofy_weapon_abilities.remove(random_ability) + + if not self.multiworld.DonaldGoofyStatsanity[self.player]: + # plando goofy get bonuses + goofy_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in + Goofy_Checks.keys() if Goofy_Checks[location].yml != "Keyblade"] + for location in goofy_get_bonus_location_pool: + self.random.choice(self.goofy_get_bonus_abilities) + random_ability = self.random.choice(self.goofy_get_bonus_abilities) + location.place_locked_item(random_ability) + self.goofy_get_bonus_abilities.remove(random_ability) + + def donald_pre_fill(self): + donald_weapon_location_list = [self.multiworld.get_location(location, self.player) for location in + Donald_Checks.keys() if Donald_Checks[location].yml == "Keyblade"] + + # take one of the 2 out + # randomize the list with only + for location in donald_weapon_location_list: + random_ability = self.random.choice(self.donald_weapon_abilities) + location.place_locked_item(random_ability) + self.donald_weapon_abilities.remove(random_ability) + + if not self.multiworld.DonaldGoofyStatsanity[self.player]: + # plando goofy get bonuses + donald_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in + Donald_Checks.keys() if Donald_Checks[location].yml != "Keyblade"] + for location in donald_get_bonus_location_pool: + random_ability = self.random.choice(self.donald_get_bonus_abilities) + location.place_locked_item(random_ability) + self.donald_get_bonus_abilities.remove(random_ability) + + def keyblade_pre_fill(self): + """ + Fills keyblade slots with abilities determined on player's setting + """ + keyblade_locations = [self.multiworld.get_location(location, self.player) for location in Keyblade_Slots.keys()] + state = self.multiworld.get_all_state(False) + keyblade_ability_pool_copy = self.keyblade_ability_pool.copy() + fill_restrictive(self.multiworld, state, keyblade_locations, keyblade_ability_pool_copy, True, True) def starting_invo_verify(self): + """ + Making sure the player doesn't put too many abilities in their starting inventory. + """ for item, value in self.multiworld.start_inventory[self.player].value.items(): if item in ActionAbility_Table \ - or item in SupportAbility_Table or exclusionItem_table["StatUps"] \ + or item in SupportAbility_Table or exclusion_item_table["StatUps"] \ or item in DonaldAbility_Table or item in GoofyAbility_Table: # cannot have more than the quantity for abilties if value > item_dictionary_table[item].quantity: @@ -324,78 +437,100 @@ def starting_invo_verify(self): self.item_quantity_dict[item] -= value def emblem_verify(self): - if self.luckyemblemamount < self.luckyemblemrequired: + """ + Making sure lucky emblems have amount>=required. + """ + if self.lucky_emblem_amount < self.lucky_emblem_required: logging.info( - f"Lucky Emblem Amount {self.multiworld.LuckyEmblemsAmount[self.player].value} is less than required " - f"{self.multiworld.LuckyEmblemsRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}." - f" Setting amount to {self.multiworld.LuckyEmblemsRequired[self.player].value}") - luckyemblemamount = max(self.luckyemblemamount, self.luckyemblemrequired) - self.multiworld.LuckyEmblemsAmount[self.player].value = luckyemblemamount + f"Lucky Emblem Amount {self.options.LuckyEmblemsAmount.value} is less than required " + f"{self.options.LuckyEmblemsRequired.value} for player {self.multiworld.get_file_safe_player_name(self.player)}." + f" Setting amount to {self.options.LuckyEmblemsRequired.value}") + luckyemblemamount = max(self.lucky_emblem_amount, self.lucky_emblem_required) + self.options.LuckyEmblemsAmount.value = luckyemblemamount - self.item_quantity_dict[ItemName.LuckyEmblem] = self.multiworld.LuckyEmblemsAmount[self.player].value + self.item_quantity_dict[ItemName.LuckyEmblem] = self.options.LuckyEmblemsAmount.value # give this proof to unlock the final door once the player has the amount of lucky emblem required - self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 + if ItemName.ProofofNonexistence in self.item_quantity_dict: + del self.item_quantity_dict[ItemName.ProofofNonexistence] def hitlist_verify(self): + """ + Making sure hitlist have amount>=required. + """ for location in self.multiworld.exclude_locations[self.player].value: - if location in self.RandomSuperBoss: - self.RandomSuperBoss.remove(location) + if location in self.random_super_boss_list: + self.random_super_boss_list.remove(location) + + if not self.options.SummonLevelLocationToggle: + self.random_super_boss_list.remove(LocationName.Summonlvl7) # Testing if the player has the right amount of Bounties for Completion. - if len(self.RandomSuperBoss) < self.BountiesAmount: + if len(self.random_super_boss_list) < self.bounties_amount: logging.info( f"{self.multiworld.get_file_safe_player_name(self.player)} has more bounties than bosses." - f" Setting total bounties to {len(self.RandomSuperBoss)}") - self.BountiesAmount = len(self.RandomSuperBoss) - self.multiworld.BountyAmount[self.player].value = self.BountiesAmount + f" Setting total bounties to {len(self.random_super_boss_list)}") + self.bounties_amount = len(self.random_super_boss_list) + self.options.BountyAmount.value = self.bounties_amount - if len(self.RandomSuperBoss) < self.BountiesRequired: + if len(self.random_super_boss_list) < self.bounties_required: logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many required bounties." - f" Setting required bounties to {len(self.RandomSuperBoss)}") - self.BountiesRequired = len(self.RandomSuperBoss) - self.multiworld.BountyRequired[self.player].value = self.BountiesRequired + f" Setting required bounties to {len(self.random_super_boss_list)}") + self.bounties_required = len(self.random_super_boss_list) + self.options.BountyRequired.value = self.bounties_required - if self.BountiesAmount < self.BountiesRequired: - logging.info(f"Bounties Amount {self.multiworld.BountyAmount[self.player].value} is less than required " - f"{self.multiworld.BountyRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}." - f" Setting amount to {self.multiworld.BountyRequired[self.player].value}") - self.BountiesAmount = max(self.BountiesAmount, self.BountiesRequired) - self.multiworld.BountyAmount[self.player].value = self.BountiesAmount + if self.bounties_amount < self.bounties_required: + logging.info( + f"Bounties Amount is less than required for player {self.multiworld.get_file_safe_player_name(self.player)}." + f" Swapping Amount and Required") + temp = self.options.BountyRequired.value + self.options.BountyRequired.value = self.options.BountyAmount.value + self.options.BountyAmount.value = temp - self.multiworld.start_hints[self.player].value.add(ItemName.Bounty) - self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 + if self.options.BountyStartingHintToggle: + self.multiworld.start_hints[self.player].value.add(ItemName.Bounty) + + if ItemName.ProofofNonexistence in self.item_quantity_dict: + del self.item_quantity_dict[ItemName.ProofofNonexistence] def set_excluded_locations(self): + """ + Fills excluded_locations from player's settings. + """ # Option to turn off all superbosses. Can do this individually but its like 20+ checks - if not self.multiworld.SuperBosses[self.player] and not self.multiworld.Goal[self.player] == "hitlist": - for superboss in exclusion_table["Datas"]: - self.multiworld.exclude_locations[self.player].value.add(superboss) + if not self.options.SuperBosses: for superboss in exclusion_table["SuperBosses"]: self.multiworld.exclude_locations[self.player].value.add(superboss) # Option to turn off Olympus Colosseum Cups. - if self.multiworld.Cups[self.player] == "no_cups": + if self.options.Cups == "no_cups": for cup in exclusion_table["Cups"]: self.multiworld.exclude_locations[self.player].value.add(cup) # exclude only hades paradox. If cups and hades paradox then nothing is excluded - elif self.multiworld.Cups[self.player] == "cups": + elif self.options.Cups == "cups": self.multiworld.exclude_locations[self.player].value.add(LocationName.HadesCupTrophyParadoxCups) + if not self.options.AtlanticaToggle: + for loc in exclusion_table["Atlantica"]: + self.multiworld.exclude_locations[self.player].value.add(loc) + def level_subtraction(self): - # there are levels but level 1 is there for the yamls - if self.multiworld.LevelDepth[self.player] == "level_99_sanity": - # level 99 sanity - self.totalLocations -= 1 - elif self.multiworld.LevelDepth[self.player] == "level_50_sanity": + """ + Determine how many locations are on sora's levels. + """ + if self.options.LevelDepth == "level_50_sanity": # level 50 sanity - self.totalLocations -= 50 - elif self.multiworld.LevelDepth[self.player] == "level_1": + return 49 + elif self.options.LevelDepth == "level_1": # level 1. No checks on levels - self.totalLocations -= 99 + return 98 + elif self.options.LevelDepth in ["level_50", "level_99"]: + # could be if leveldepth!= 99 sanity but this reads better imo + return 75 else: - # level 50/99 since they contain the same amount of levels - self.totalLocations -= 76 + return 0 def get_filler_item_name(self) -> str: - return self.multiworld.random.choice( - [ItemName.PowerBoost, ItemName.MagicBoost, ItemName.DefenseBoost, ItemName.APBoost]) + """ + Returns random filler item name. + """ + return self.random.choice(filler_items) diff --git a/worlds/kh2/logic.py b/worlds/kh2/logic.py deleted file mode 100644 index 10af4144a7fb..000000000000 --- a/worlds/kh2/logic.py +++ /dev/null @@ -1,312 +0,0 @@ -from .Names import ItemName -from ..AutoWorld import LogicMixin - - -class KH2Logic(LogicMixin): - def kh_lod_unlocked(self, player, amount): - return self.has(ItemName.SwordoftheAncestor, player, amount) - - def kh_oc_unlocked(self, player, amount): - return self.has(ItemName.BattlefieldsofWar, player, amount) - - def kh_twtnw_unlocked(self, player, amount): - return self.has(ItemName.WaytotheDawn, player, amount) - - def kh_ht_unlocked(self, player, amount): - return self.has(ItemName.BoneFist, player, amount) - - def kh_tt_unlocked(self, player, amount): - return self.has(ItemName.IceCream, player, amount) - - def kh_pr_unlocked(self, player, amount): - return self.has(ItemName.SkillandCrossbones, player, amount) - - def kh_sp_unlocked(self, player, amount): - return self.has(ItemName.IdentityDisk, player, amount) - - def kh_stt_unlocked(self, player: int, amount): - return self.has(ItemName.NamineSketches, player, amount) - - # Using Dummy 13 for this - def kh_dc_unlocked(self, player: int, amount): - return self.has(ItemName.CastleKey, player, amount) - - def kh_hb_unlocked(self, player, amount): - return self.has(ItemName.MembershipCard, player, amount) - - def kh_pl_unlocked(self, player, amount): - return self.has(ItemName.ProudFang, player, amount) - - def kh_ag_unlocked(self, player, amount): - return self.has(ItemName.Scimitar, player, amount) - - def kh_bc_unlocked(self, player, amount): - return self.has(ItemName.BeastsClaw, player, amount) - - def kh_amount_of_forms(self, player, amount, requiredform="None"): - level = 0 - formList = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm, - ItemName.FinalForm] - # required form is in the logic for region connections - if requiredform != "None": - formList.remove(requiredform) - for form in formList: - if self.has(form, player): - level += 1 - return level >= amount - - def kh_visit_locking_amount(self, player, amount): - visit = 0 - # torn pages are not added since you cannot get exp from that world - for item in {ItemName.CastleKey, ItemName.BattlefieldsofWar, ItemName.SwordoftheAncestor, ItemName.BeastsClaw, - ItemName.BoneFist, ItemName.ProudFang, ItemName.SkillandCrossbones, ItemName.Scimitar, - ItemName.MembershipCard, - ItemName.IceCream, ItemName.WaytotheDawn, - ItemName.IdentityDisk, ItemName.NamineSketches}: - visit += self.count(item, player) - return visit >= amount - - def kh_three_proof_unlocked(self, player): - return self.has(ItemName.ProofofConnection, player, 1) \ - and self.has(ItemName.ProofofNonexistence, player, 1) \ - and self.has(ItemName.ProofofPeace, player, 1) - - def kh_hitlist(self, player, amount): - return self.has(ItemName.Bounty, player, amount) - - def kh_lucky_emblem_unlocked(self, player, amount): - return self.has(ItemName.LuckyEmblem, player, amount) - - def kh_victory(self, player): - return self.has(ItemName.Victory, player, 1) - - def kh_summon(self, player, amount): - summonlevel = 0 - for summon in {ItemName.Genie, ItemName.ChickenLittle, ItemName.Stitch, ItemName.PeterPan}: - if self.has(summon, player): - summonlevel += 1 - return summonlevel >= amount - - # magic progression - def kh_fire(self, player): - return self.has(ItemName.FireElement, player, 1) - - def kh_fira(self, player): - return self.has(ItemName.FireElement, player, 2) - - def kh_firaga(self, player): - return self.has(ItemName.FireElement, player, 3) - - def kh_blizzard(self, player): - return self.has(ItemName.BlizzardElement, player, 1) - - def kh_blizzara(self, player): - return self.has(ItemName.BlizzardElement, player, 2) - - def kh_blizzaga(self, player): - return self.has(ItemName.BlizzardElement, player, 3) - - def kh_thunder(self, player): - return self.has(ItemName.ThunderElement, player, 1) - - def kh_thundara(self, player): - return self.has(ItemName.ThunderElement, player, 2) - - def kh_thundaga(self, player): - return self.has(ItemName.ThunderElement, player, 3) - - def kh_magnet(self, player): - return self.has(ItemName.MagnetElement, player, 1) - - def kh_magnera(self, player): - return self.has(ItemName.MagnetElement, player, 2) - - def kh_magnega(self, player): - return self.has(ItemName.MagnetElement, player, 3) - - def kh_reflect(self, player): - return self.has(ItemName.ReflectElement, player, 1) - - def kh_reflera(self, player): - return self.has(ItemName.ReflectElement, player, 2) - - def kh_reflega(self, player): - return self.has(ItemName.ReflectElement, player, 3) - - def kh_highjump(self, player, amount): - return self.has(ItemName.HighJump, player, amount) - - def kh_quickrun(self, player, amount): - return self.has(ItemName.QuickRun, player, amount) - - def kh_dodgeroll(self, player, amount): - return self.has(ItemName.DodgeRoll, player, amount) - - def kh_aerialdodge(self, player, amount): - return self.has(ItemName.AerialDodge, player, amount) - - def kh_glide(self, player, amount): - return self.has(ItemName.Glide, player, amount) - - def kh_comboplus(self, player, amount): - return self.has(ItemName.ComboPlus, player, amount) - - def kh_aircomboplus(self, player, amount): - return self.has(ItemName.AirComboPlus, player, amount) - - def kh_valorgenie(self, player): - return self.has(ItemName.Genie, player) and self.has(ItemName.ValorForm, player) - - def kh_wisdomgenie(self, player): - return self.has(ItemName.Genie, player) and self.has(ItemName.WisdomForm, player) - - def kh_mastergenie(self, player): - return self.has(ItemName.Genie, player) and self.has(ItemName.MasterForm, player) - - def kh_finalgenie(self, player): - return self.has(ItemName.Genie, player) and self.has(ItemName.FinalForm, player) - - def kh_rsr(self, player): - return self.has(ItemName.Slapshot, player, 1) and self.has(ItemName.ComboMaster, player) and self.kh_reflect( - player) - - def kh_gapcloser(self, player): - return self.has(ItemName.FlashStep, player, 1) or self.has(ItemName.SlideDash, player) - - # Crowd Control and Berserk Hori will be used when I add hard logic. - - def kh_crowdcontrol(self, player): - return self.kh_magnera(player) and self.has(ItemName.ChickenLittle, player) \ - or self.kh_magnega(player) and self.kh_mastergenie(player) - - def kh_berserkhori(self, player): - return self.has(ItemName.HorizontalSlash, player, 1) and self.has(ItemName.BerserkCharge, player) - - def kh_donaldlimit(self, player): - return self.has(ItemName.FlareForce, player, 1) or self.has(ItemName.Fantasia, player) - - def kh_goofylimit(self, player): - return self.has(ItemName.TornadoFusion, player, 1) or self.has(ItemName.Teamwork, player) - - def kh_basetools(self, player): - # TODO: if option is easy then add reflect,gap closer and second chance&once more. #option east scom option normal adds gap closer or combo master #hard is what is right now - return self.has(ItemName.Guard, player, 1) and self.has(ItemName.AerialRecovery, player, 1) \ - and self.has(ItemName.FinishingPlus, player, 1) - - def kh_roxastools(self, player): - return self.kh_basetools(player) and ( - self.has(ItemName.QuickRun, player) or self.has(ItemName.NegativeCombo, player, 2)) - - def kh_painandpanic(self, player): - return (self.kh_goofylimit(player) or self.kh_donaldlimit(player)) and self.kh_dc_unlocked(player, 2) - - def kh_cerberuscup(self, player): - return self.kh_amount_of_forms(player, 2) and self.kh_thundara(player) \ - and self.kh_ag_unlocked(player, 1) and self.kh_ht_unlocked(player, 1) \ - and self.kh_pl_unlocked(player, 1) - - def kh_titan(self, player: int): - return self.kh_summon(player, 2) and (self.kh_thundara(player) or self.kh_magnera(player)) \ - and self.kh_oc_unlocked(player, 2) - - def kh_gof(self, player): - return self.kh_titan(player) and self.kh_cerberuscup(player) \ - and self.kh_painandpanic(player) and self.kh_twtnw_unlocked(player, 1) - - def kh_dataroxas(self, player): - return self.kh_basetools(player) and \ - ((self.has(ItemName.LimitForm, player) and self.kh_amount_of_forms(player, 3) and self.has( - ItemName.TrinityLimit, player) and self.kh_gapcloser(player)) - or (self.has(ItemName.NegativeCombo, player, 2) or self.kh_quickrun(player, 2))) - - def kh_datamarluxia(self, player): - return self.kh_basetools(player) and self.kh_reflera(player) \ - and ((self.kh_amount_of_forms(player, 3) and self.has(ItemName.FinalForm, player) and self.kh_fira( - player)) or self.has(ItemName.NegativeCombo, player, 2) or self.kh_donaldlimit(player)) - - def kh_datademyx(self, player): - return self.kh_basetools(player) and self.kh_amount_of_forms(player, 5) and self.kh_firaga(player) \ - and (self.kh_donaldlimit(player) or self.kh_blizzard(player)) - - def kh_datalexaeus(self, player): - return self.kh_basetools(player) and self.kh_amount_of_forms(player, 3) and self.kh_reflera(player) \ - and (self.has(ItemName.NegativeCombo, player, 2) or self.kh_donaldlimit(player)) - - def kh_datasaix(self, player): - return self.kh_basetools(player) and (self.kh_thunder(player) or self.kh_blizzard(player)) \ - and self.kh_highjump(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, 2) and self.kh_amount_of_forms(player, 3) \ - and (self.kh_rsr(player) or self.has(ItemName.NegativeCombo, player, 2) or self.has(ItemName.PeterPan, - player)) - - def kh_dataxaldin(self, player): - return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.kh_goofylimit(player) \ - and self.kh_highjump(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, - 2) and self.kh_magnet( - player) - # and (self.kh_form_level_unlocked(player, 3) or self.kh_berserkhori(player)) - - def kh_dataxemnas(self, player): - return self.kh_basetools(player) and self.kh_rsr(player) and self.kh_gapcloser(player) \ - and (self.has(ItemName.LimitForm, player) or self.has(ItemName.TrinityLimit, player)) - - def kh_dataxigbar(self, player): - return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \ - and self.kh_amount_of_forms(player, 3) and self.kh_reflera(player) - - def kh_datavexen(self, player): - return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \ - and self.kh_amount_of_forms(player, 4) and self.kh_reflera(player) and self.kh_fira(player) - - def kh_datazexion(self, player): - return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \ - and self.kh_amount_of_forms(player, 3) \ - and self.kh_reflera(player) and self.kh_fira(player) - - def kh_dataaxel(self, player): - return self.kh_basetools(player) \ - and ((self.kh_reflera(player) and self.kh_blizzara(player)) or self.has(ItemName.NegativeCombo, player, 2)) - - def kh_dataluxord(self, player): - return self.kh_basetools(player) and self.kh_reflect(player) - - def kh_datalarxene(self, player): - return self.kh_basetools(player) and self.kh_reflera(player) \ - and ((self.has(ItemName.FinalForm, player) and self.kh_amount_of_forms(player, 4) and self.kh_fire( - player)) - or (self.kh_donaldlimit(player) and self.kh_amount_of_forms(player, 2))) - - def kh_sephi(self, player): - return self.kh_dataxemnas(player) - - def kh_onek(self, player): - return self.kh_reflect(player) or self.has(ItemName.Guard, player) - - def kh_terra(self, player): - return self.has(ItemName.ProofofConnection, player) and self.kh_basetools(player) \ - and self.kh_dodgeroll(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, 3) \ - and ((self.kh_comboplus(player, 2) and self.has(ItemName.Explosion, player)) or self.has( - ItemName.NegativeCombo, player, 2)) - - def kh_cor(self, player): - return self.kh_reflect(player) \ - and self.kh_highjump(player, 2) and self.kh_quickrun(player, 2) and self.kh_aerialdodge(player, 2) \ - and (self.has(ItemName.MasterForm, player) and self.kh_fire(player) - or (self.has(ItemName.ChickenLittle, player) and self.kh_donaldlimit(player) and self.kh_glide(player, - 2))) - - def kh_transport(self, player): - return self.kh_basetools(player) and self.kh_reflera(player) \ - and ((self.kh_mastergenie(player) and self.kh_magnera(player) and self.kh_donaldlimit(player)) - or (self.has(ItemName.FinalForm, player) and self.kh_amount_of_forms(player, 4) and self.kh_fira( - player))) - - def kh_gr2(self, player): - return (self.has(ItemName.MasterForm, player) or self.has(ItemName.Stitch, player)) \ - and (self.kh_fire(player) or self.kh_blizzard(player) or self.kh_thunder(player)) - - def kh_xaldin(self, player): - return self.kh_basetools(player) and (self.kh_donaldlimit(player) or self.kh_amount_of_forms(player, 1)) - - def kh_mcp(self, player): - return self.kh_reflect(player) and ( - self.has(ItemName.MasterForm, player) or self.has(ItemName.FinalForm, player)) diff --git a/worlds/kh2/mod_template/mod.yml b/worlds/kh2/mod_template/mod.yml deleted file mode 100644 index 4246132c2641..000000000000 --- a/worlds/kh2/mod_template/mod.yml +++ /dev/null @@ -1,38 +0,0 @@ -assets: -- method: binarc - name: 00battle.bin - source: - - method: listpatch - name: fmlv - source: - - name: FmlvList.yml - type: fmlv - type: List - - method: listpatch - name: lvup - source: - - name: LvupList.yml - type: lvup - type: List - - method: listpatch - name: bons - source: - - name: BonsList.yml - type: bons - type: List -- method: binarc - name: 03system.bin - source: - - method: listpatch - name: trsr - source: - - name: TrsrList.yml - type: trsr - type: List - - method: listpatch - name: item - source: - - name: ItemList.yml - type: item - type: List -title: Randomizer Seed diff --git a/worlds/kh2/test/TestGoal.py b/worlds/kh2/test/TestGoal.py deleted file mode 100644 index 97874da2d090..000000000000 --- a/worlds/kh2/test/TestGoal.py +++ /dev/null @@ -1,30 +0,0 @@ -from . import KH2TestBase -from ..Names import ItemName - - -class TestDefault(KH2TestBase): - options = {} - - def testEverything(self): - self.collect_all_but([ItemName.Victory]) - self.assertBeatable(True) - - -class TestLuckyEmblem(KH2TestBase): - options = { - "Goal": 1, - } - - def testEverything(self): - self.collect_all_but([ItemName.LuckyEmblem]) - self.assertBeatable(True) - - -class TestHitList(KH2TestBase): - options = { - "Goal": 2, - } - - def testEverything(self): - self.collect_all_but([ItemName.Bounty]) - self.assertBeatable(True) diff --git a/worlds/kh2/test/TestSlotData.py b/worlds/kh2/test/TestSlotData.py deleted file mode 100644 index 656cd48d5a6f..000000000000 --- a/worlds/kh2/test/TestSlotData.py +++ /dev/null @@ -1,21 +0,0 @@ -import unittest - -from test.general import setup_solo_multiworld -from . import KH2TestBase -from .. import KH2World, all_locations, item_dictionary_table, CheckDupingItems, AllWeaponSlot, KH2Item -from ..Names import ItemName -from ... import AutoWorldRegister -from ...AutoWorld import call_all - - -class TestLocalItems(KH2TestBase): - - def testSlotData(self): - gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") - multiworld = setup_solo_multiworld(KH2World, gen_steps) - for location in multiworld.get_locations(): - if location.item is None: - location.place_locked_item(multiworld.worlds[1].create_item(ItemName.NoExperience)) - call_all(multiworld, "fill_slot_data") - slotdata = multiworld.worlds[1].fill_slot_data() - assert len(slotdata["LocalItems"]) > 0, f"{slotdata['LocalItems']} is empty" diff --git a/worlds/kh2/test/__init__.py b/worlds/kh2/test/__init__.py index dfef22762745..6cefe6e79197 100644 --- a/worlds/kh2/test/__init__.py +++ b/worlds/kh2/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class KH2TestBase(WorldTestBase): diff --git a/worlds/kh2/test/test_fight_logic.py b/worlds/kh2/test/test_fight_logic.py new file mode 100644 index 000000000000..0c47d132f0a0 --- /dev/null +++ b/worlds/kh2/test/test_fight_logic.py @@ -0,0 +1,19 @@ +from . import KH2TestBase + + +class TestEasy(KH2TestBase): + options = { + "FightLogic": 0 + } + + +class TestNormal(KH2TestBase): + options = { + "FightLogic": 1 + } + + +class TestHard(KH2TestBase): + options = { + "FightLogic": 2 + } diff --git a/worlds/kh2/test/test_form_logic.py b/worlds/kh2/test/test_form_logic.py new file mode 100644 index 000000000000..1cd850a985dd --- /dev/null +++ b/worlds/kh2/test/test_form_logic.py @@ -0,0 +1,214 @@ +from . import KH2TestBase +from ..Names import ItemName, LocationName + +global_all_possible_forms = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm, ItemName.FinalForm] + [ItemName.AutoValor, ItemName.AutoWisdom, ItemName.AutoLimit, ItemName.AutoMaster, ItemName.AutoFinal] + + +class KH2TestFormBase(KH2TestBase): + allForms = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm, ItemName.FinalForm] + autoForms = [ItemName.AutoValor, ItemName.AutoWisdom, ItemName.AutoLimit, ItemName.AutoMaster, ItemName.AutoFinal] + allLevel2 = [LocationName.Valorlvl2, LocationName.Wisdomlvl2, LocationName.Limitlvl2, LocationName.Masterlvl2, + LocationName.Finallvl2] + allLevel3 = [LocationName.Valorlvl3, LocationName.Wisdomlvl3, LocationName.Limitlvl3, LocationName.Masterlvl3, + LocationName.Finallvl3] + allLevel4 = [LocationName.Valorlvl4, LocationName.Wisdomlvl4, LocationName.Limitlvl4, LocationName.Masterlvl4, + LocationName.Finallvl4] + allLevel5 = [LocationName.Valorlvl5, LocationName.Wisdomlvl5, LocationName.Limitlvl5, LocationName.Masterlvl5, + LocationName.Finallvl5] + allLevel6 = [LocationName.Valorlvl6, LocationName.Wisdomlvl6, LocationName.Limitlvl6, LocationName.Masterlvl6, + LocationName.Finallvl6] + allLevel7 = [LocationName.Valorlvl7, LocationName.Wisdomlvl7, LocationName.Limitlvl7, LocationName.Masterlvl7, + LocationName.Finallvl7] + driveToAuto = { + ItemName.FinalForm: ItemName.AutoFinal, + ItemName.MasterForm: ItemName.AutoMaster, + ItemName.LimitForm: ItemName.AutoLimit, + ItemName.WisdomForm: ItemName.AutoWisdom, + ItemName.ValorForm: ItemName.AutoValor, + } + AutoToDrive = {Auto: Drive for Drive, Auto in driveToAuto.items()} + driveFormMap = { + ItemName.ValorForm: [LocationName.Valorlvl2, + LocationName.Valorlvl3, + LocationName.Valorlvl4, + LocationName.Valorlvl5, + LocationName.Valorlvl6, + LocationName.Valorlvl7], + ItemName.WisdomForm: [LocationName.Wisdomlvl2, + LocationName.Wisdomlvl3, + LocationName.Wisdomlvl4, + LocationName.Wisdomlvl5, + LocationName.Wisdomlvl6, + LocationName.Wisdomlvl7], + ItemName.LimitForm: [LocationName.Limitlvl2, + LocationName.Limitlvl3, + LocationName.Limitlvl4, + LocationName.Limitlvl5, + LocationName.Limitlvl6, + LocationName.Limitlvl7], + ItemName.MasterForm: [LocationName.Masterlvl2, + LocationName.Masterlvl3, + LocationName.Masterlvl4, + LocationName.Masterlvl5, + LocationName.Masterlvl6, + LocationName.Masterlvl7], + ItemName.FinalForm: [LocationName.Finallvl2, + LocationName.Finallvl3, + LocationName.Finallvl4, + LocationName.Finallvl5, + LocationName.Finallvl6, + LocationName.Finallvl7], + } + # global_all_possible_forms = allForms + autoForms + + +class TestDefaultForms(KH2TestFormBase): + """ + Test default form access rules. + """ + options = { + "AutoFormLogic": False, + "FinalFormLogic": "light_and_darkness" + } + + def test_default_Auto_Form_Logic(self): + allPossibleForms = global_all_possible_forms + # this tests with a light and darkness in the inventory. + self.collect_all_but(allPossibleForms) + for form in self.allForms: + self.assertFalse((self.can_reach_location(self.driveFormMap[form][0])), form) + self.collect(self.get_item_by_name(self.driveToAuto[form])) + self.assertFalse((self.can_reach_location(self.driveFormMap[form][0])), form) + + def test_default_Final_Form(self): + allPossibleForms = global_all_possible_forms + self.collect_all_but(allPossibleForms) + self.collect_by_name(ItemName.FinalForm) + self.assertTrue((self.can_reach_location(LocationName.Finallvl2))) + self.assertTrue((self.can_reach_location(LocationName.Finallvl3))) + self.assertFalse((self.can_reach_location(LocationName.Finallvl4))) + + def test_default_without_LnD(self): + allPossibleForms = self.allForms + self.collect_all_but(allPossibleForms) + for form, levels in self.driveFormMap.items(): + # final form is unique and breaks using this test. Tested above. + if levels[0] == LocationName.Finallvl2: + continue + for driveForm in self.allForms: + if self.count(driveForm) >= 1: + for _ in range(self.count(driveForm)): + self.remove(self.get_item_by_name(driveForm)) + allFormsCopy = self.allForms.copy() + allFormsCopy.remove(form) + self.collect(self.get_item_by_name(form)) + for _ in range(self.count(ItemName.LightDarkness)): + self.remove(self.get_item_by_name(ItemName.LightDarkness)) + self.assertTrue((self.can_reach_location(levels[0])), levels[0]) + self.assertTrue((self.can_reach_location(levels[1])), levels[1]) + self.assertFalse((self.can_reach_location(levels[2])), levels[2]) + for i in range(3): + self.collect(self.get_item_by_name(allFormsCopy[i])) + # for some reason after collecting a form it can pick up light and darkness + for _ in range(self.count(ItemName.LightDarkness)): + self.remove(self.get_item_by_name(ItemName.LightDarkness)) + + self.assertTrue((self.can_reach_location(levels[2 + i]))) + if i < 2: + self.assertFalse((self.can_reach_location(levels[3 + i]))) + else: + self.collect(self.get_item_by_name(allFormsCopy[i + 1])) + for _ in range(self.count(ItemName.LightDarkness)): + self.remove(self.get_item_by_name(ItemName.LightDarkness)) + self.assertTrue((self.can_reach_location(levels[3 + i]))) + + def test_default_with_lnd(self): + allPossibleForms = self.allForms + self.collect_all_but(allPossibleForms) + for form, levels in self.driveFormMap.items(): + if form != ItemName.FinalForm: + for driveForm in self.allForms: + for _ in range(self.count(driveForm)): + self.remove(self.get_item_by_name(driveForm)) + allFormsCopy = self.allForms.copy() + allFormsCopy.remove(form) + self.collect(self.get_item_by_name(ItemName.LightDarkness)) + self.assertFalse((self.can_reach_location(levels[0]))) + self.collect(self.get_item_by_name(form)) + + self.assertTrue((self.can_reach_location(levels[0]))) + self.assertTrue((self.can_reach_location(levels[1]))) + self.assertTrue((self.can_reach_location(levels[2]))) + self.assertFalse((self.can_reach_location(levels[3]))) + for i in range(2): + self.collect(self.get_item_by_name(allFormsCopy[i])) + self.assertTrue((self.can_reach_location(levels[i + 3]))) + if i <= 2: + self.assertFalse((self.can_reach_location(levels[i + 4]))) + + +class TestJustAForm(KH2TestFormBase): + # this test checks if you can unlock final form with just a form. + options = { + "AutoFormLogic": False, + "FinalFormLogic": "just_a_form" + } + + def test_just_a_form_connections(self): + allPossibleForms = self.allForms + self.collect_all_but(allPossibleForms) + allPossibleForms.remove(ItemName.FinalForm) + for form, levels in self.driveFormMap.items(): + for driveForm in self.allForms: + for _ in range(self.count(driveForm)): + self.remove(self.get_item_by_name(driveForm)) + if form != ItemName.FinalForm: + # reset the forms + allFormsCopy = self.allForms.copy() + allFormsCopy.remove(form) + self.assertFalse((self.can_reach_location(levels[0]))) + self.collect(self.get_item_by_name(form)) + self.assertTrue((self.can_reach_location(levels[0]))) + self.assertTrue((self.can_reach_location(levels[1]))) + self.assertTrue((self.can_reach_location(levels[2]))) + + # level 4 of a form. This tests if the player can unlock final form. + self.assertFalse((self.can_reach_location(levels[3]))) + # amount of forms left in the pool are 3. 1 already collected and one is final form. + for i in range(3): + allFormsCopy.remove(allFormsCopy[0]) + # so we don't accidentally collect another form like light and darkness in the above tests. + self.collect_all_but(allFormsCopy) + self.assertTrue((self.can_reach_location(levels[3 + i])), levels[3 + i]) + if i < 2: + self.assertFalse((self.can_reach_location(levels[4 + i])), levels[4 + i]) + + +class TestAutoForms(KH2TestFormBase): + options = { + "AutoFormLogic": True, + "FinalFormLogic": "light_and_darkness" + } + + def test_Nothing(self): + KH2TestBase() + + def test_auto_forms_level_progression(self): + allPossibleForms = self.allForms + [ItemName.LightDarkness] + # state has all auto forms + self.collect_all_but(allPossibleForms) + allPossibleFormsCopy = allPossibleForms.copy() + collectedDrives = [] + i = 0 + for form in allPossibleForms: + currentDriveForm = form + collectedDrives += [currentDriveForm] + allPossibleFormsCopy.remove(currentDriveForm) + self.collect_all_but(allPossibleFormsCopy) + for driveForm in self.allForms: + # +1 every iteration. + self.assertTrue((self.can_reach_location(self.driveFormMap[driveForm][i])), driveForm) + # making sure having the form still gives an extra drive level to its own form. + if driveForm in collectedDrives and i < 5: + self.assertTrue((self.can_reach_location(self.driveFormMap[driveForm][i + 1])), driveForm) + i += 1 diff --git a/worlds/kh2/test/test_goal.py b/worlds/kh2/test/test_goal.py new file mode 100644 index 000000000000..1a481ad3d91f --- /dev/null +++ b/worlds/kh2/test/test_goal.py @@ -0,0 +1,59 @@ +from . import KH2TestBase +from ..Names import ItemName + + +class TestDefault(KH2TestBase): + options = {} + + +class TestThreeProofs(KH2TestBase): + options = { + "Goal": 0, + } + + +class TestLuckyEmblem(KH2TestBase): + options = { + "Goal": 1, + } + + +class TestHitList(KH2TestBase): + options = { + "Goal": 2, + } + + +class TestLuckyEmblemHitlist(KH2TestBase): + options = { + "Goal": 3, + } + + +class TestThreeProofsNoXemnas(KH2TestBase): + options = { + "Goal": 0, + "FinalXemnas": False, + } + + +class TestLuckyEmblemNoXemnas(KH2TestBase): + options = { + "Goal": 1, + "FinalXemnas": False, + } + + +class TestHitListNoXemnas(KH2TestBase): + options = { + "Goal": 2, + "FinalXemnas": False, + } + + +class TestLuckyEmblemHitlistNoXemnas(KH2TestBase): + options = { + "Goal": 3, + "FinalXemnas": False, + } + From d46e68cb5fdeb68674e4ffcb2fdf591067d456e8 Mon Sep 17 00:00:00 2001 From: Dinopony Date: Sat, 25 Nov 2023 16:00:15 +0100 Subject: [PATCH 092/104] Landstalker: implement new game (#1808) Co-authored-by: Anthony Demarcy Co-authored-by: Phar --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/landstalker/Hints.py | 140 ++ worlds/landstalker/Items.py | 105 + worlds/landstalker/Locations.py | 53 + worlds/landstalker/Options.py | 228 ++ worlds/landstalker/Regions.py | 118 + worlds/landstalker/Rules.py | 134 ++ worlds/landstalker/__init__.py | 262 +++ worlds/landstalker/data/hint_source.py | 1989 ++++++++++++++++ worlds/landstalker/data/item_source.py | 2017 +++++++++++++++++ worlds/landstalker/data/world_node.py | 411 ++++ worlds/landstalker/data/world_path.py | 446 ++++ worlds/landstalker/data/world_region.py | 299 +++ .../landstalker/data/world_teleport_tree.py | 62 + ...andstalker - The Treasures of King Nole.md | 60 + .../landstalker/docs/landstalker_setup_en.md | 119 + worlds/landstalker/docs/ls_guide_ap.png | Bin 0 -> 2283 bytes worlds/landstalker/docs/ls_guide_client.png | Bin 0 -> 86096 bytes worlds/landstalker/docs/ls_guide_emu.png | Bin 0 -> 2598 bytes worlds/landstalker/docs/ls_guide_rom.png | Bin 0 -> 3951 bytes 21 files changed, 6447 insertions(+) create mode 100644 worlds/landstalker/Hints.py create mode 100644 worlds/landstalker/Items.py create mode 100644 worlds/landstalker/Locations.py create mode 100644 worlds/landstalker/Options.py create mode 100644 worlds/landstalker/Regions.py create mode 100644 worlds/landstalker/Rules.py create mode 100644 worlds/landstalker/__init__.py create mode 100644 worlds/landstalker/data/hint_source.py create mode 100644 worlds/landstalker/data/item_source.py create mode 100644 worlds/landstalker/data/world_node.py create mode 100644 worlds/landstalker/data/world_path.py create mode 100644 worlds/landstalker/data/world_region.py create mode 100644 worlds/landstalker/data/world_teleport_tree.py create mode 100644 worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md create mode 100644 worlds/landstalker/docs/landstalker_setup_en.md create mode 100644 worlds/landstalker/docs/ls_guide_ap.png create mode 100644 worlds/landstalker/docs/ls_guide_client.png create mode 100644 worlds/landstalker/docs/ls_guide_emu.png create mode 100644 worlds/landstalker/docs/ls_guide_rom.png diff --git a/README.md b/README.md index b51fe00f9ac2..3508dd16095c 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Currently, the following games are supported: * DOOM II * Shivers * Heretic +* Landstalker: The Treasures of King Nole For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index c589b1333c9b..0764fa927464 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -67,6 +67,9 @@ # Kingdom Hearts 2 /worlds/kh2/ @JaredWeakStrike +# Landstalker: The Treasures of King Nole +/worlds/landstalker/ @Dinopony + # Lingo /worlds/lingo/ @hatkirby diff --git a/worlds/landstalker/Hints.py b/worlds/landstalker/Hints.py new file mode 100644 index 000000000000..93274f1d68bb --- /dev/null +++ b/worlds/landstalker/Hints.py @@ -0,0 +1,140 @@ +from typing import TYPE_CHECKING + +from BaseClasses import Location +from .data.hint_source import HINT_SOURCES_JSON + +if TYPE_CHECKING: + from random import Random + from . import LandstalkerWorld + + +def generate_blurry_location_hint(location: Location, random: "Random"): + cleaned_location_name = location.hint_text.lower().translate({ord(c): None for c in "(),:"}) + cleaned_location_name.replace("-", " ") + cleaned_location_name.replace("/", " ") + cleaned_location_name.replace(".", " ") + location_name_words = [w for w in cleaned_location_name.split(" ") if len(w) > 3] + + random_word_1 = "mysterious" + random_word_2 = "place" + if location_name_words: + random_word_1 = random.choice(location_name_words) + location_name_words.remove(random_word_1) + if location_name_words: + random_word_2 = random.choice(location_name_words) + return [random_word_1, random_word_2] + + +def generate_lithograph_hint(world: "LandstalkerWorld"): + hint_text = "It's barely readable:\n" + jewel_items = world.jewel_items + + for item in jewel_items: + # Jewel hints are composed of 4 'words' shuffled randomly: + # - the name of the player whose world contains said jewel (if not ours) + # - the color of the jewel (if relevant) + # - two random words from the location name + words = generate_blurry_location_hint(item.location, world.random) + words[0] = words[0].upper() + words[1] = words[1].upper() + if len(jewel_items) < 6: + # Add jewel color if we are not using generic jewels because jewel count is 6 or more + words.append(item.name.split(" ")[0].upper()) + if item.location.player != world.player: + # Add player name if it's not in our own world + player_name = world.multiworld.get_player_name(world.player) + words.append(player_name.upper()) + world.random.shuffle(words) + hint_text += " ".join(words) + "\n" + return hint_text.rstrip("\n") + + +def generate_random_hints(world: "LandstalkerWorld"): + hints = {} + hint_texts = [] + random = world.random + multiworld = world.multiworld + this_player = world.player + + # Exclude Life Stock from the hints as some of them are considered as progression for Fahl, but isn't really + # exciting when hinted + excluded_items = ["Life Stock", "EkeEke"] + + progression_items = [item for item in multiworld.itempool if item.advancement and + item.name not in excluded_items] + + local_own_progression_items = [item for item in progression_items if item.player == this_player + and item.location.player == this_player] + remote_own_progression_items = [item for item in progression_items if item.player == this_player + and item.location.player != this_player] + local_unowned_progression_items = [item for item in progression_items if item.player != this_player + and item.location.player == this_player] + remote_unowned_progression_items = [item for item in progression_items if item.player != this_player + and item.location.player != this_player] + + # Hint-type #1: Own progression item in own world + for item in local_own_progression_items: + region_hint = item.location.parent_region.hint_text + hint_texts.append(f"I can sense {item.name} {region_hint}.") + + # Hint-type #2: Remote progression item in own world + for item in local_unowned_progression_items: + other_player = multiworld.get_player_name(item.player) + own_local_region = item.location.parent_region.hint_text + hint_texts.append(f"You might find something useful for {other_player} {own_local_region}. " + f"It is a {item.name}, to be precise.") + + # Hint-type #3: Own progression item in remote location + for item in remote_own_progression_items: + other_player = multiworld.get_player_name(item.location.player) + if item.location.game == "Landstalker - The Treasures of King Nole": + region_hint_name = item.location.parent_region.hint_text + hint_texts.append(f"If you need {item.name}, tell {other_player} to look {region_hint_name}.") + else: + [word_1, word_2] = generate_blurry_location_hint(item.location, random) + if word_1 == "mysterious" and word_2 == "place": + continue + hint_texts.append(f"Looking for {item.name}? I read something about {other_player}'s world... " + f"Does \"{word_1} {word_2}\" remind you anything?") + + # Hint-type #4: Remote progression item in remote location + for item in remote_unowned_progression_items: + owner_name = multiworld.get_player_name(item.player) + if item.location.player == item.player: + world_name = "their own world" + else: + world_name = f"{multiworld.get_player_name(item.location.player)}'s world" + [word_1, word_2] = generate_blurry_location_hint(item.location, random) + if word_1 == "mysterious" and word_2 == "place": + continue + hint_texts.append(f"I once found {owner_name}'s {item.name} in {world_name}. " + f"I remember \"{word_1} {word_2}\"... Does that make any sense?") + + # Hint-type #5: Jokes + other_player_names = [multiworld.get_player_name(player) for player in multiworld.player_ids if + player != this_player] + if other_player_names: + random_player_name = random.choice(other_player_names) + hint_texts.append(f"{random_player_name}'s world is objectively better than yours.") + + hint_texts.append(f"Have you found all of the {len(multiworld.itempool)} items in this universe?") + + local_progression_item_count = len(local_own_progression_items) + len(local_unowned_progression_items) + remote_progression_item_count = len(remote_own_progression_items) + len(remote_unowned_progression_items) + percent = (local_progression_item_count / (local_progression_item_count + remote_progression_item_count)) * 100 + hint_texts.append(f"Did you know that your world contains {int(percent)} percent of all progression items?") + + # Shuffle hint texts and hint source names, and pair the two of those together + hint_texts = list(set(hint_texts)) + random.shuffle(hint_texts) + + hint_count = world.options.hint_count.value + del hint_texts[hint_count:] + + hint_source_names = [source["description"] for source in HINT_SOURCES_JSON if + source["description"].startswith("Foxy")] + random.shuffle(hint_source_names) + + for i in range(hint_count): + hints[hint_source_names[i]] = hint_texts[i] + return hints diff --git a/worlds/landstalker/Items.py b/worlds/landstalker/Items.py new file mode 100644 index 000000000000..ad7efa1cb27a --- /dev/null +++ b/worlds/landstalker/Items.py @@ -0,0 +1,105 @@ +from typing import Dict, List, NamedTuple + +from BaseClasses import Item, ItemClassification + +BASE_ITEM_ID = 4000 + + +class LandstalkerItem(Item): + game: str = "Landstalker - The Treasures of King Nole" + price_in_shops: int + + +class LandstalkerItemData(NamedTuple): + id: int + classification: ItemClassification + price_in_shops: int + quantity: int = 1 + + +item_table: Dict[str, LandstalkerItemData] = { + "EkeEke": LandstalkerItemData(0, ItemClassification.filler, 20, 0), # Variable amount + "Magic Sword": LandstalkerItemData(1, ItemClassification.useful, 300), + "Sword of Ice": LandstalkerItemData(2, ItemClassification.useful, 300), + "Thunder Sword": LandstalkerItemData(3, ItemClassification.useful, 500), + "Sword of Gaia": LandstalkerItemData(4, ItemClassification.progression, 300), + "Fireproof": LandstalkerItemData(5, ItemClassification.progression, 150), + "Iron Boots": LandstalkerItemData(6, ItemClassification.progression, 150), + "Healing Boots": LandstalkerItemData(7, ItemClassification.useful, 300), + "Snow Spikes": LandstalkerItemData(8, ItemClassification.progression, 400), + "Steel Breast": LandstalkerItemData(9, ItemClassification.useful, 200), + "Chrome Breast": LandstalkerItemData(10, ItemClassification.useful, 350), + "Shell Breast": LandstalkerItemData(11, ItemClassification.useful, 500), + "Hyper Breast": LandstalkerItemData(12, ItemClassification.useful, 700), + "Mars Stone": LandstalkerItemData(13, ItemClassification.useful, 150), + "Moon Stone": LandstalkerItemData(14, ItemClassification.useful, 150), + "Saturn Stone": LandstalkerItemData(15, ItemClassification.useful, 200), + "Venus Stone": LandstalkerItemData(16, ItemClassification.useful, 300), + # Awakening Book: 17 + "Detox Grass": LandstalkerItemData(18, ItemClassification.filler, 25, 9), + "Statue of Gaia": LandstalkerItemData(19, ItemClassification.filler, 75, 12), + "Golden Statue": LandstalkerItemData(20, ItemClassification.filler, 150, 10), + "Mind Repair": LandstalkerItemData(21, ItemClassification.filler, 25, 7), + "Casino Ticket": LandstalkerItemData(22, ItemClassification.progression, 50), + "Axe Magic": LandstalkerItemData(23, ItemClassification.progression, 400), + "Blue Ribbon": LandstalkerItemData(24, ItemClassification.filler, 50), + "Buyer Card": LandstalkerItemData(25, ItemClassification.progression, 150), + "Lantern": LandstalkerItemData(26, ItemClassification.progression, 200), + "Garlic": LandstalkerItemData(27, ItemClassification.progression, 150, 2), + "Anti Paralyze": LandstalkerItemData(28, ItemClassification.filler, 20, 7), + "Statue of Jypta": LandstalkerItemData(29, ItemClassification.useful, 250), + "Sun Stone": LandstalkerItemData(30, ItemClassification.progression, 300), + "Armlet": LandstalkerItemData(31, ItemClassification.progression, 300), + "Einstein Whistle": LandstalkerItemData(32, ItemClassification.progression, 200), + "Blue Jewel": LandstalkerItemData(33, ItemClassification.progression, 500, 0), # Detox Book in base game + "Yellow Jewel": LandstalkerItemData(34, ItemClassification.progression, 500, 0), # AntiCurse Book in base game + # Record Book: 35 + # Spell Book: 36 + # Hotel Register: 37 + # Island Map: 38 + "Lithograph": LandstalkerItemData(39, ItemClassification.progression, 250), + "Red Jewel": LandstalkerItemData(40, ItemClassification.progression, 500, 0), + "Pawn Ticket": LandstalkerItemData(41, ItemClassification.useful, 200, 4), + "Purple Jewel": LandstalkerItemData(42, ItemClassification.progression, 500, 0), + "Gola's Eye": LandstalkerItemData(43, ItemClassification.progression, 400), + "Death Statue": LandstalkerItemData(44, ItemClassification.filler, 150), + "Dahl": LandstalkerItemData(45, ItemClassification.filler, 100, 18), + "Restoration": LandstalkerItemData(46, ItemClassification.filler, 40, 9), + "Logs": LandstalkerItemData(47, ItemClassification.progression, 100, 2), + "Oracle Stone": LandstalkerItemData(48, ItemClassification.progression, 250), + "Idol Stone": LandstalkerItemData(49, ItemClassification.progression, 200), + "Key": LandstalkerItemData(50, ItemClassification.progression, 150), + "Safety Pass": LandstalkerItemData(51, ItemClassification.progression, 250), + "Green Jewel": LandstalkerItemData(52, ItemClassification.progression, 500, 0), # No52 in base game + "Bell": LandstalkerItemData(53, ItemClassification.useful, 200), + "Short Cake": LandstalkerItemData(54, ItemClassification.useful, 250), + "Gola's Nail": LandstalkerItemData(55, ItemClassification.progression, 800), + "Gola's Horn": LandstalkerItemData(56, ItemClassification.progression, 800), + "Gola's Fang": LandstalkerItemData(57, ItemClassification.progression, 800), + # Broad Sword: 58 + # Leather Breast: 59 + # Leather Boots: 60 + # No Ring: 61 + "Life Stock": LandstalkerItemData(62, ItemClassification.filler, 250, 0), # Variable amount + "No Item": LandstalkerItemData(63, ItemClassification.filler, 0, 0), + "1 Gold": LandstalkerItemData(64, ItemClassification.filler, 1), + "20 Golds": LandstalkerItemData(65, ItemClassification.filler, 20, 15), + "50 Golds": LandstalkerItemData(66, ItemClassification.filler, 50, 7), + "100 Golds": LandstalkerItemData(67, ItemClassification.filler, 100, 5), + "200 Golds": LandstalkerItemData(68, ItemClassification.useful, 200, 2), + + "Progressive Armor": LandstalkerItemData(69, ItemClassification.useful, 250, 0), + "Kazalt Jewel": LandstalkerItemData(70, ItemClassification.progression, 500, 0) +} + + +def get_weighted_filler_item_names(): + weighted_item_names: List[str] = [] + for name, data in item_table.items(): + if data.classification == ItemClassification.filler: + weighted_item_names += [name for _ in range(data.quantity)] + return weighted_item_names + + +def build_item_name_to_id_table(): + return {name: data.id + BASE_ITEM_ID for name, data in item_table.items()} diff --git a/worlds/landstalker/Locations.py b/worlds/landstalker/Locations.py new file mode 100644 index 000000000000..5e42fbecda72 --- /dev/null +++ b/worlds/landstalker/Locations.py @@ -0,0 +1,53 @@ +from typing import Dict, Optional + +from BaseClasses import Location +from .Regions import LandstalkerRegion +from .data.item_source import ITEM_SOURCES_JSON + +BASE_LOCATION_ID = 4000 +BASE_GROUND_LOCATION_ID = BASE_LOCATION_ID + 256 +BASE_SHOP_LOCATION_ID = BASE_GROUND_LOCATION_ID + 30 +BASE_REWARD_LOCATION_ID = BASE_SHOP_LOCATION_ID + 50 + + +class LandstalkerLocation(Location): + game: str = "Landstalker - The Treasures of King Nole" + type_string: str + price: int = 0 + + def __init__(self, player: int, name: str, location_id: Optional[int], region: LandstalkerRegion, type_string: str): + super().__init__(player, name, location_id, region) + self.type_string = type_string + + +def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], name_to_id_table: Dict[str, int]): + # Create real locations from the data inside the corresponding JSON file + for data in ITEM_SOURCES_JSON: + region_id = data["nodeId"] + region = regions_table[region_id] + new_location = LandstalkerLocation(player, data["name"], name_to_id_table[data["name"]], region, data["type"]) + region.locations.append(new_location) + + # Create a specific end location that will contain a fake win-condition item + end_location = LandstalkerLocation(player, "End", None, regions_table["end"], "reward") + regions_table["end"].locations.append(end_location) + + +def build_location_name_to_id_table(): + location_name_to_id_table = {} + + for data in ITEM_SOURCES_JSON: + if data["type"] == "chest": + location_id = BASE_LOCATION_ID + int(data["chestId"]) + elif data["type"] == "ground": + location_id = BASE_GROUND_LOCATION_ID + int(data["groundItemId"]) + elif data["type"] == "shop": + location_id = BASE_SHOP_LOCATION_ID + int(data["shopItemId"]) + else: # if data["type"] == "reward": + location_id = BASE_REWARD_LOCATION_ID + int(data["rewardId"]) + location_name_to_id_table[data["name"]] = location_id + + # Win condition location ID + location_name_to_id_table["Gola"] = BASE_REWARD_LOCATION_ID + 10 + + return location_name_to_id_table diff --git a/worlds/landstalker/Options.py b/worlds/landstalker/Options.py new file mode 100644 index 000000000000..65ffd2c1f31e --- /dev/null +++ b/worlds/landstalker/Options.py @@ -0,0 +1,228 @@ +from dataclasses import dataclass + +from Options import Choice, DeathLink, DefaultOnToggle, PerGameCommonOptions, Range, Toggle + + +class LandstalkerGoal(Choice): + """ + The goal to accomplish in order to complete the seed. + - Beat Gola: beat the usual final boss (same as vanilla) + - Reach Kazalt: find the jewels and take the teleporter to Kazalt + - Beat Dark Nole: the door to King Nole's fight brings you into a final dungeon with an absurdly hard boss you have + to beat to win the game + """ + display_name = "Goal" + + option_beat_gola = 0 + option_reach_kazalt = 1 + option_beat_dark_nole = 2 + + default = 0 + + +class JewelCount(Range): + """ + Determines the number of jewels to find to be able to reach Kazalt. + """ + display_name = "Jewel Count" + range_start = 0 + range_end = 9 + default = 5 + + +class ProgressiveArmors(DefaultOnToggle): + """ + When obtaining an armor, you get the next armor tier instead of getting the specific armor tier that was + placed here by randomization. Enabling this provides a smoother progression. + """ + display_name = "Progressive Armors" + + +class UseRecordBook(DefaultOnToggle): + """ + Gives a Record Book item in starting inventory, allowing to save the game anywhere. + This makes the game significantly less frustrating and enables interesting save-scumming strategies in some places. + """ + display_name = "Use Record Book" + + +class UseSpellBook(DefaultOnToggle): + """ + Gives a Spell Book item in starting inventory, allowing to warp back to the starting location at any time. + This prevents any kind of softlock and makes the world easier to explore. + """ + display_name = "Use Spell Book" + + +class EnsureEkeEkeInShops(DefaultOnToggle): + """ + Ensures an EkeEke will always be for sale in one shop per region in the game. + Disabling this can lead to frustrating situations where you cannot refill your health items and might get locked. + """ + display_name = "Ensure EkeEke in Shops" + + +class RemoveGumiBoulder(Toggle): + """ + Removes the boulder between Gumi and Ryuma, which is usually a one-way path. + This makes the vanilla early game (Massan, Gumi...) more easily accessible when starting outside it. + """ + display_name = "Remove Boulder After Gumi" + + +class EnemyJumpingInLogic(Toggle): + """ + Adds jumping on enemies' heads as a logical rule. + This gives access to Mountainous Area from Lake Shrine sector and to the cliff chest behind a magic tree near Mir Tower. + These tricks not being easy, you should leave this disabled until practiced. + """ + display_name = "Enemy Jumping in Logic" + + +class TreeCuttingGlitchInLogic(Toggle): + """ + Adds tree-cutting glitch as a logical rule, enabling access to both chests behind magic trees in Mir Tower Sector + without having Axe Magic. + """ + display_name = "Tree-cutting Glitch in Logic" + + +class DamageBoostingInLogic(Toggle): + """ + Adds damage boosting as a logical rule, removing any requirements involving Iron Boots or Fireproof Boots. + Who doesn't like walking on spikes and lava? + """ + display_name = "Damage Boosting in Logic" + + +class WhistleUsageBehindTrees(DefaultOnToggle): + """ + In Greenmaze, Einstein Whistle can only be used to call Cutter from the intended side by default. + Enabling this allows using Einstein Whistle from both sides of the magic trees. + This is only useful in seeds starting in the "waterfall" spawn region or where teleportation trees are made open from the start. + """ + display_name = "Allow Using Einstein Whistle Behind Trees" + + +class SpawnRegion(Choice): + """ + List of spawn locations that can be picked by the randomizer. + It is advised to keep Massan as your spawn location for your first few seeds. + Picking a late-game location can make the seed significantly harder, both for logic and combat. + """ + display_name = "Starting Region" + + option_massan = 0 + option_gumi = 1 + option_kado = 2 + option_waterfall = 3 + option_ryuma = 4 + option_mercator = 5 + option_verla = 6 + option_greenmaze = 7 + option_destel = 8 + + default = 0 + + +class TeleportTreeRequirements(Choice): + """ + Determines the requirements to be able to use a teleport tree pair. + - None: All teleport trees are available right from the start + - Clear Tibor: Tibor needs to be cleared before unlocking any tree + - Visit Trees: Both trees from a tree pair need to be visited to teleport between them + Vanilla behavior is "Clear Tibor And Visit Trees" + """ + display_name = "Teleportation Trees Requirements" + + option_none = 0 + option_clear_tibor = 1 + option_visit_trees = 2 + option_clear_tibor_and_visit_trees = 3 + + default = 3 + + +class ShuffleTrees(Toggle): + """ + If enabled, all teleportation trees will be shuffled into new pairs. + """ + display_name = "Shuffle Teleportation Trees" + + +class ReviveUsingEkeeke(DefaultOnToggle): + """ + In the vanilla game, when you die, you are automatically revived by Friday using an EkeEke. + This setting allows disabling this feature, making the game extremely harder. + USE WITH CAUTION! + """ + display_name = "Revive Using EkeEke" + + +class ShopPricesFactor(Range): + """ + Applies a percentage factor on all prices in shops. Having higher prices can lead to a bit of gold farming, which + can make seeds longer but also sometimes more frustrating. + """ + display_name = "Shop Prices Factor (%)" + range_start = 50 + range_end = 200 + default = 100 + + +class CombatDifficulty(Choice): + """ + Determines the overall combat difficulty in the game by modifying both monsters HP & damage. + - Peaceful: 50% HP & damage + - Easy: 75% HP & damage + - Normal: 100% HP & damage + - Hard: 140% HP & damage + - Insane: 200% HP & damage + """ + display_name = "Combat Difficulty" + + option_peaceful = 0 + option_easy = 1 + option_normal = 2 + option_hard = 3 + option_insane = 4 + + default = 2 + + +class HintCount(Range): + """ + Determines the number of Foxy NPCs that will be scattered across the world, giving various types of hints + """ + display_name = "Hint Count" + range_start = 0 + range_end = 25 + default = 12 + + +@dataclass +class LandstalkerOptions(PerGameCommonOptions): + goal: LandstalkerGoal + spawn_region: SpawnRegion + jewel_count: JewelCount + progressive_armors: ProgressiveArmors + use_record_book: UseRecordBook + use_spell_book: UseSpellBook + + shop_prices_factor: ShopPricesFactor + combat_difficulty: CombatDifficulty + + teleport_tree_requirements: TeleportTreeRequirements + shuffle_trees: ShuffleTrees + + ensure_ekeeke_in_shops: EnsureEkeEkeInShops + remove_gumi_boulder: RemoveGumiBoulder + allow_whistle_usage_behind_trees: WhistleUsageBehindTrees + handle_damage_boosting_in_logic: DamageBoostingInLogic + handle_enemy_jumping_in_logic: EnemyJumpingInLogic + handle_tree_cutting_glitch_in_logic: TreeCuttingGlitchInLogic + + hint_count: HintCount + + revive_using_ekeeke: ReviveUsingEkeeke + death_link: DeathLink diff --git a/worlds/landstalker/Regions.py b/worlds/landstalker/Regions.py new file mode 100644 index 000000000000..21704194f157 --- /dev/null +++ b/worlds/landstalker/Regions.py @@ -0,0 +1,118 @@ +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING + +from BaseClasses import MultiWorld, Region +from .data.world_node import WORLD_NODES_JSON +from .data.world_path import WORLD_PATHS_JSON +from .data.world_region import WORLD_REGIONS_JSON +from .data.world_teleport_tree import WORLD_TELEPORT_TREES_JSON + +if TYPE_CHECKING: + from . import LandstalkerWorld + + +class LandstalkerRegion(Region): + code: str + + def __init__(self, code: str, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None): + super().__init__(name, player, multiworld, hint) + self.code = code + + +class LandstalkerRegionData(NamedTuple): + locations: Optional[List[str]] + region_exits: Optional[List[str]] + + +def create_regions(world: "LandstalkerWorld"): + regions_table: Dict[str, LandstalkerRegion] = {} + multiworld = world.multiworld + player = world.player + + # Create the hardcoded starting "Menu" region + menu_region = LandstalkerRegion("menu", "Menu", player, multiworld) + regions_table["menu"] = menu_region + multiworld.regions.append(menu_region) + + # Create regions from world_nodes + for code, region_data in WORLD_NODES_JSON.items(): + random_hint_name = None + if "hints" in region_data: + random_hint_name = multiworld.random.choice(region_data["hints"]) + region = LandstalkerRegion(code, region_data["name"], player, multiworld, random_hint_name) + regions_table[code] = region + multiworld.regions.append(region) + + # Create exits/entrances from world_paths + for data in WORLD_PATHS_JSON: + two_way = data["twoWay"] if "twoWay" in data else False + create_entrance(data["fromId"], data["toId"], two_way, regions_table) + + # Create a path between the fake Menu location and the starting location + starting_region = get_starting_region(world, regions_table) + menu_region.connect(starting_region, f"menu -> {starting_region.code}") + + add_specific_paths(world, regions_table) + + return regions_table + + +def add_specific_paths(world: "LandstalkerWorld", regions_table: Dict[str, LandstalkerRegion]): + # If Gumi boulder is removed, add a path from "route_gumi_ryuma" to "gumi" + if world.options.remove_gumi_boulder == 1: + create_entrance("route_gumi_ryuma", "gumi", False, regions_table) + + # If enemy jumping is in logic, Mountainous Area can be reached from route to Lake Shrine by doing a "ghost jump" + # at crossroads map + if world.options.handle_enemy_jumping_in_logic == 1: + create_entrance("route_lake_shrine", "route_lake_shrine_cliff", False, regions_table) + + # If using Einstein Whistle behind trees is allowed, add a new logic path there to reflect that change + if world.options.allow_whistle_usage_behind_trees == 1: + create_entrance("greenmaze_post_whistle", "greenmaze_pre_whistle", False, regions_table) + + +def create_entrance(from_id: str, to_id: str, two_way: bool, regions_table: Dict[str, LandstalkerRegion]): + created_entrances = [] + + name = from_id + " -> " + to_id + from_region = regions_table[from_id] + to_region = regions_table[to_id] + + created_entrances.append(from_region.connect(to_region, name)) + + if two_way: + reverse_name = to_id + " -> " + from_id + created_entrances.append(to_region.connect(from_region, reverse_name)) + + return created_entrances + + +def get_starting_region(world: "LandstalkerWorld", regions_table: Dict[str, LandstalkerRegion]): + # Most spawn locations have the same name as the region they are bound to, but a few vary. + spawn_id = world.options.spawn_region.current_key + if spawn_id == "waterfall": + return regions_table["greenmaze_post_whistle"] + elif spawn_id == "kado": + return regions_table["route_gumi_ryuma"] + elif spawn_id == "greenmaze": + return regions_table["greenmaze_pre_whistle"] + return regions_table[spawn_id] + + +def get_darkenable_regions(): + return {data["name"]: data["nodeIds"] for data in WORLD_REGIONS_JSON if "darkMapIds" in data} + + +def load_teleport_trees(): + pairs = [] + for pair in WORLD_TELEPORT_TREES_JSON: + first_tree = { + 'name': pair[0]["name"], + 'region': pair[0]["nodeId"] + } + second_tree = { + 'name': pair[1]["name"], + 'region': pair[1]["nodeId"] + } + pairs.append([first_tree, second_tree]) + return pairs diff --git a/worlds/landstalker/Rules.py b/worlds/landstalker/Rules.py new file mode 100644 index 000000000000..51357c9480b0 --- /dev/null +++ b/worlds/landstalker/Rules.py @@ -0,0 +1,134 @@ +from typing import List, TYPE_CHECKING + +from BaseClasses import CollectionState +from .data.world_path import WORLD_PATHS_JSON +from .Locations import LandstalkerLocation +from .Regions import LandstalkerRegion + +if TYPE_CHECKING: + from . import LandstalkerWorld + + +def _landstalker_has_visited_regions(state: CollectionState, player: int, regions): + return all([state.can_reach(region, None, player) for region in regions]) + + +def _landstalker_has_health(state: CollectionState, player: int, health): + return state.has("Life Stock", player, health) + + +# multiworld: MultiWorld, player: int, regions_table: Dict[str, Region], dark_region_ids: List[str] +def create_rules(world: "LandstalkerWorld"): + # Item & exploration requirements to take paths + add_path_requirements(world) + add_specific_path_requirements(world) + + # Location rules to forbid some item types depending on location types + add_location_rules(world) + + # Win condition + world.multiworld.completion_condition[world.player] = lambda state: state.has("King Nole's Treasure", world.player) + + +# multiworld: MultiWorld, player: int, regions_table: Dict[str, Region], +# dark_region_ids: List[str] +def add_path_requirements(world: "LandstalkerWorld"): + for data in WORLD_PATHS_JSON: + name = data["fromId"] + " -> " + data["toId"] + + # Determine required items to reach this region + required_items = data["requiredItems"] if "requiredItems" in data else [] + if "itemsPlacedWhenCrossing" in data: + required_items += data["itemsPlacedWhenCrossing"] + + if data["toId"] in world.dark_region_ids: + # Make Lantern required to reach the randomly selected dark regions + required_items.append("Lantern") + if world.options.handle_damage_boosting_in_logic: + # If damage boosting is handled in logic, remove all iron boots & fireproof requirements + required_items = [item for item in required_items if item != "Iron Boots" and item != "Fireproof"] + + # Determine required other visited regions to reach this region + required_region_ids = data["requiredNodes"] if "requiredNodes" in data else [] + required_regions = [world.regions_table[region_id] for region_id in required_region_ids] + + if not (required_items or required_regions): + continue + + # Create the rule lambda using those requirements + access_rule = make_path_requirement_lambda(world.player, required_items, required_regions) + world.multiworld.get_entrance(name, world.player).access_rule = access_rule + + # If two-way, also apply the rule to the opposite path + if "twoWay" in data and data["twoWay"] is True: + reverse_name = data["toId"] + " -> " + data["fromId"] + world.multiworld.get_entrance(reverse_name, world.player).access_rule = access_rule + + +def add_specific_path_requirements(world: "LandstalkerWorld"): + multiworld = world.multiworld + player = world.player + + # Make the jewels required to reach Kazalt + jewel_count = world.options.jewel_count.value + path_to_kazalt = multiworld.get_entrance("king_nole_cave -> kazalt", player) + if jewel_count < 6: + # 5- jewels => the player needs to find as many uniquely named jewel items + required_jewels = ["Red Jewel", "Purple Jewel", "Green Jewel", "Blue Jewel", "Yellow Jewel"] + del required_jewels[jewel_count:] + path_to_kazalt.access_rule = make_path_requirement_lambda(player, required_jewels, []) + else: + # 6+ jewels => the player needs to find as many "Kazalt Jewel" items + path_to_kazalt.access_rule = lambda state: state.has("Kazalt Jewel", player, jewel_count) + + # If enemy jumping is enabled, Mir Tower sector first tree can be bypassed to reach the elevated ledge + if world.options.handle_enemy_jumping_in_logic == 1: + remove_requirements_for(world, "mir_tower_sector -> mir_tower_sector_tree_ledge") + + # Both trees in Mir Tower sector can be abused using tree cutting glitch + if world.options.handle_tree_cutting_glitch_in_logic == 1: + remove_requirements_for(world, "mir_tower_sector -> mir_tower_sector_tree_ledge") + remove_requirements_for(world, "mir_tower_sector -> mir_tower_sector_tree_coast") + + # If Whistle can be used from behind the trees, it adds a new path that requires the whistle as well + if world.options.allow_whistle_usage_behind_trees == 1: + entrance = multiworld.get_entrance("greenmaze_post_whistle -> greenmaze_pre_whistle", player) + entrance.access_rule = make_path_requirement_lambda(player, ["Einstein Whistle"], []) + + +def make_path_requirement_lambda(player: int, required_items: List[str], required_regions: List[LandstalkerRegion]): + """ + Lambdas are created in a for loop, so values need to be captured + """ + return lambda state: \ + state.has_all(set(required_items), player) and _landstalker_has_visited_regions(state, player, required_regions) + + +def make_shop_location_requirement_lambda(player: int, location: LandstalkerLocation): + """ + Lambdas are created in a for loop, so values need to be captured + """ + # Prevent local golds in shops, as well as duplicates + other_locations_in_shop = [loc for loc in location.parent_region.locations if loc != location] + return lambda item: \ + item.player != player \ + or (" Gold" not in item.name + and item.name not in [loc.item.name for loc in other_locations_in_shop if loc.item is not None]) + + +def remove_requirements_for(world: "LandstalkerWorld", entrance_name: str): + entrance = world.multiworld.get_entrance(entrance_name, world.player) + entrance.access_rule = lambda state: True + + +def add_location_rules(world: "LandstalkerWorld"): + location: LandstalkerLocation + for location in world.multiworld.get_locations(world.player): + if location.type_string == "ground": + location.item_rule = lambda item: not (item.player == world.player and " Gold" in item.name) + elif location.type_string == "shop": + location.item_rule = make_shop_location_requirement_lambda(world.player, location) + + # Add a special rule for Fahl + fahl_location = world.multiworld.get_location("Mercator: Fahl's dojo challenge reward", world.player) + fahl_location.access_rule = lambda state: _landstalker_has_health(state, world.player, 15) diff --git a/worlds/landstalker/__init__.py b/worlds/landstalker/__init__.py new file mode 100644 index 000000000000..baa1deb620a4 --- /dev/null +++ b/worlds/landstalker/__init__.py @@ -0,0 +1,262 @@ +from typing import ClassVar, Set + +from BaseClasses import LocationProgressType, Tutorial +from worlds.AutoWorld import WebWorld, World +from .Hints import * +from .Items import * +from .Locations import * +from .Options import JewelCount, LandstalkerGoal, LandstalkerOptions, ProgressiveArmors, TeleportTreeRequirements +from .Regions import * +from .Rules import * + + +class LandstalkerWeb(WebWorld): + theme = "grass" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Landstalker Randomizer software on your computer.", + "English", + "landstalker_setup_en.md", + "landstalker_setup/en", + ["Dinopony"] + )] + + +class LandstalkerWorld(World): + """ + Landstalker: The Treasures of King Nole is a classic Action-RPG with an isometric view (also known as "2.5D"). + You play Nigel, a treasure hunter exploring the island of Mercator trying to find the legendary treasure. + Roam freely on the island, get stronger to beat dungeons and gather the required key items in order to reach the + hidden palace and claim the treasure. + """ + game = "Landstalker - The Treasures of King Nole" + options_dataclass = LandstalkerOptions + options: LandstalkerOptions + required_client_version = (0, 4, 4) + web = LandstalkerWeb() + + item_name_to_id = build_item_name_to_id_table() + location_name_to_id = build_location_name_to_id_table() + + cached_spheres: ClassVar[List[Set[Location]]] + + def __init__(self, multiworld, player): + super().__init__(multiworld, player) + self.regions_table: Dict[str, LandstalkerRegion] = {} + self.dark_dungeon_id = "None" + self.dark_region_ids = [] + self.teleport_tree_pairs = [] + self.jewel_items = [] + + def fill_slot_data(self) -> dict: + # Generate hints. + self.adjust_shop_prices() + hints = Hints.generate_random_hints(self) + hints["Lithograph"] = Hints.generate_lithograph_hint(self) + hints["Oracle Stone"] = f"It shows {self.dark_dungeon_id}\nenshrouded in darkness." + + # Put options, locations' contents and some additional data inside slot data + options = [ + "goal", "jewel_count", "progressive_armors", "use_record_book", "use_spell_book", "shop_prices_factor", + "combat_difficulty", "teleport_tree_requirements", "shuffle_trees", "ensure_ekeeke_in_shops", + "remove_gumi_boulder", "allow_whistle_usage_behind_trees", "handle_damage_boosting_in_logic", + "handle_enemy_jumping_in_logic", "handle_tree_cutting_glitch_in_logic", "hint_count", "death_link", + "revive_using_ekeeke", + ] + + slot_data = self.options.as_dict(*options) + slot_data["spawn_region"] = self.options.spawn_region.current_key + slot_data["seed"] = self.random.randint(0, 2 ** 32 - 1) + slot_data["dark_region"] = self.dark_dungeon_id + slot_data["hints"] = hints + slot_data["teleport_tree_pairs"] = [[pair[0]["name"], pair[1]["name"]] for pair in self.teleport_tree_pairs] + + # Type hinting for location. + location: LandstalkerLocation + slot_data["location_prices"] = { + location.name: location.price for location in self.multiworld.get_locations(self.player) if location.price} + + return slot_data + + def generate_early(self): + # Randomly pick a set of dark regions where Lantern is needed + darkenable_regions = get_darkenable_regions() + self.dark_dungeon_id = self.random.choice(list(darkenable_regions)) + self.dark_region_ids = darkenable_regions[self.dark_dungeon_id] + + def create_regions(self): + self.regions_table = Regions.create_regions(self) + Locations.create_locations(self.player, self.regions_table, self.location_name_to_id) + self.create_teleportation_trees() + + def create_item(self, name: str, classification_override: Optional[ItemClassification] = None) -> LandstalkerItem: + data = item_table[name] + classification = classification_override or data.classification + item = LandstalkerItem(name, classification, BASE_ITEM_ID + data.id, self.player) + item.price_in_shops = data.price_in_shops + return item + + def create_event(self, name: str) -> LandstalkerItem: + return LandstalkerItem(name, ItemClassification.progression, None, self.player) + + def get_filler_item_name(self) -> str: + return "EkeEke" + + def create_items(self): + item_pool: List[LandstalkerItem] = [] + for name, data in item_table.items(): + # If item is an armor and progressive armors are enabled, transform it into a progressive armor item + if self.options.progressive_armors and "Breast" in name: + name = "Progressive Armor" + item_pool += [self.create_item(name) for _ in range(data.quantity)] + + # If the appropriate setting is on, place one EkeEke in one shop in every town in the game + if self.options.ensure_ekeeke_in_shops: + shops_to_fill = [ + "Massan: Shop item #1", + "Gumi: Inn item #1", + "Ryuma: Inn item", + "Mercator: Shop item #1", + "Verla: Shop item #1", + "Destel: Inn item", + "Route to Lake Shrine: Greedly's shop item #1", + "Kazalt: Shop item #1" + ] + for location_name in shops_to_fill: + self.multiworld.get_location(location_name, self.player).place_locked_item(self.create_item("EkeEke")) + + # Add a fixed amount of progression Life Stock for a specific requirement (Fahl) + fahl_lifestock_req = 15 + item_pool += [self.create_item("Life Stock", ItemClassification.progression) for _ in range(fahl_lifestock_req)] + # Add a unique progression EkeEke for a specific requirement (Cutter) + item_pool.append(self.create_item("EkeEke", ItemClassification.progression)) + + # Add a variable amount of "useful" Life Stock to the pool, depending on the amount of starting Life Stock + # (i.e. on the starting location) + starting_lifestocks = self.get_starting_health() - 4 + lifestock_count = 80 - starting_lifestocks - fahl_lifestock_req + item_pool += [self.create_item("Life Stock") for _ in range(lifestock_count)] + + # Add jewels to the item pool depending on the number of jewels set in generation settings + self.jewel_items = [self.create_item(name) for name in self.get_jewel_names(self.options.jewel_count)] + item_pool += self.jewel_items + + # Add a pre-placed fake win condition item + self.multiworld.get_location("End", self.player).place_locked_item(self.create_event("King Nole's Treasure")) + + # Fill the rest of the item pool with EkeEke + remaining_items = len(self.multiworld.get_unfilled_locations(self.player)) - len(item_pool) + item_pool += [self.create_item(self.get_filler_item_name()) for _ in range(remaining_items)] + + self.multiworld.itempool += item_pool + + def create_teleportation_trees(self): + self.teleport_tree_pairs = load_teleport_trees() + + def pairwise(iterable): + """Yields pairs of elements from the given list -> [0,1], [2,3]...""" + a = iter(iterable) + return zip(a, a) + + # Shuffle teleport tree pairs if the matching setting is on + if self.options.shuffle_trees: + all_trees = [item for pair in self.teleport_tree_pairs for item in pair] + self.random.shuffle(all_trees) + self.teleport_tree_pairs = [[x, y] for x, y in pairwise(all_trees)] + + # If a specific setting is set, teleport trees are potentially active without visiting both sides. + # This means we need to add those as explorable paths for the generation algorithm. + teleport_trees_mode = self.options.teleport_tree_requirements.value + created_entrances = [] + if teleport_trees_mode in [TeleportTreeRequirements.option_none, TeleportTreeRequirements.option_clear_tibor]: + for pair in self.teleport_tree_pairs: + entrances = create_entrance(pair[0]["region"], pair[1]["region"], True, self.regions_table) + created_entrances += entrances + + # Teleport trees are open but require access to Tibor to work + if teleport_trees_mode == TeleportTreeRequirements.option_clear_tibor: + for entrance in created_entrances: + entrance.access_rule = make_path_requirement_lambda(self.player, [], [self.regions_table["tibor"]]) + + def set_rules(self): + Rules.create_rules(self) + + # In "Reach Kazalt" goal, player doesn't have access to Kazalt, King Nole's Labyrinth & King Nole's Palace. + # As a consequence, all locations inside those regions must be excluded, and the teleporter from + # King Nole's Cave to Kazalt must go to the end region instead. + if self.options.goal == LandstalkerGoal.option_reach_kazalt: + kazalt_tp = self.multiworld.get_entrance("king_nole_cave -> kazalt", self.player) + kazalt_tp.connected_region = self.regions_table["end"] + + excluded_regions = [ + "kazalt", + "king_nole_labyrinth_pre_door", + "king_nole_labyrinth_post_door", + "king_nole_labyrinth_exterior", + "king_nole_labyrinth_fall_from_exterior", + "king_nole_labyrinth_raft_entrance", + "king_nole_labyrinth_raft", + "king_nole_labyrinth_sacred_tree", + "king_nole_labyrinth_path_to_palace", + "king_nole_palace" + ] + + for location in self.multiworld.get_locations(self.player): + if location.parent_region.name in excluded_regions: + location.progress_type = LocationProgressType.EXCLUDED + + def get_starting_health(self): + spawn_id = self.options.spawn_region.current_key + if spawn_id == "destel": + return 20 + elif spawn_id == "verla": + return 16 + elif spawn_id in ["waterfall", "mercator", "greenmaze"]: + return 10 + else: + return 4 + + @classmethod + def stage_post_fill(cls, multiworld): + # Cache spheres for hint calculation after fill completes. + cls.cached_spheres = list(multiworld.get_spheres()) + + @classmethod + def stage_modify_multidata(cls, *_): + # Clean up all references in cached spheres after generation completes. + del cls.cached_spheres + + def adjust_shop_prices(self): + # Calculate prices for items in shops once all items have their final position + unknown_items_price = 250 + earlygame_price_factor = 1.0 + endgame_price_factor = 2.0 + factor_diff = endgame_price_factor - earlygame_price_factor + + global_price_factor = self.options.shop_prices_factor / 100.0 + + spheres = self.cached_spheres + sphere_count = len(spheres) + for sphere_id, sphere in enumerate(spheres): + location: LandstalkerLocation # after conditional, we guarantee it's this kind of location. + for location in sphere: + if location.player != self.player or location.type_string != "shop": + continue + + current_playthrough_progression = sphere_id / sphere_count + progression_price_factor = earlygame_price_factor + (current_playthrough_progression * factor_diff) + + price = location.item.price_in_shops \ + if location.item.game == "Landstalker - The Treasures of King Nole" else unknown_items_price + price *= progression_price_factor + price *= global_price_factor + price -= price % 5 + price = max(price, 5) + location.price = int(price) + + @staticmethod + def get_jewel_names(count: JewelCount): + if count < 6: + return ["Red Jewel", "Purple Jewel", "Green Jewel", "Blue Jewel", "Yellow Jewel"][:count] + + return ["Kazalt Jewel"] * count diff --git a/worlds/landstalker/data/hint_source.py b/worlds/landstalker/data/hint_source.py new file mode 100644 index 000000000000..4f22cac4bdd6 --- /dev/null +++ b/worlds/landstalker/data/hint_source.py @@ -0,0 +1,1989 @@ +HINT_SOURCES_JSON = [ + { + "description": "Lithograph", + "smallTextbox": True + }, + { + "description": "Oracle Stone", + "smallTextbox": True + }, + { + "description": "Mercator fortune teller", + "textIds": [ + 654 + ] + }, + { + "description": "King Nole's Cave sign", + "textIds": [ + 253 + ] + }, + { + "description": "Foxy (next to Ryuma's mayor house)", + "entity": { + "mapId": 611, + "position": { + "x": 47, + "y": 25, + "z": 3 + }, + "orientation": "sw" + }, + "nodeId": "ryuma" + }, + { + "description": "Foxy (behind trees in Gumi)", + "entity": { + "mapId": [602, 603], + "position": { + "x": 24, + "y": 35, + "z": 6 + }, + "orientation": "sw" + }, + "nodeId": "gumi" + }, + { + "description": "Foxy (next to Mercator gates)", + "entity": { + "mapId": 454, + "position": { + "x": 18, + "y": 46, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "route_gumi_ryuma" + }, + { + "description": "Foxy (near basin behind Mercator)", + "entity": { + "mapId": 636, + "position": { + "x": 18, + "y": 27, + "z": 1 + }, + "orientation": "nw" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (near cabin on Verla Shore)", + "entity": { + "mapId": 468, + "position": { + "x": 24, + "y": 45, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "verla_shore" + }, + { + "description": "Foxy (outside Verla Mines entrance)", + "entity": { + "mapId": 470, + "position": { + "x": 24, + "y": 29, + "z": 5 + }, + "orientation": "sw" + }, + "nodeId": "verla_shore" + }, + { + "description": "Foxy (room below Thieves Hideout summit)", + "entity": { + "mapId": 221, + "position": { + "x": 29, + "y": 19, + "z": 2 + }, + "orientation": "nw" + }, + "nodeId": "thieves_hideout_post_key" + }, + { + "description": "Foxy (near waterfall in Mountainous Area)", + "entity": { + "mapId": 485, + "position": { + "x": 42, + "y": 62, + "z": 2 + }, + "orientation": "nw", + "highPalette": True + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in Mercator Castle left court)", + "entity": { + "mapId": 32, + "position": { + "x": 36, + "y": 38, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (on Mercator inn balcony)", + "entity": { + "mapId": 632, + "position": { + "x": 19, + "y": 35, + "z": 4 + }, + "orientation": "se" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (on a beach between Ryuma and Mercator)", + "entity": { + "mapId": 450, + "position": { + "x": 18, + "y": 28, + "z": 0 + }, + "orientation": "nw" + }, + "nodeId": "route_gumi_ryuma" + }, + { + "description": "Foxy (atop Ryuma's lighthouse)", + "entity": { + "mapId": [628, 629], + "position": { + "x": 26, + "y": 21, + "z": 1 + }, + "orientation": "ne" + }, + "nodeId": "ryuma" + }, + { + "description": "Foxy (looking at dead man in Thieves Hideout)", + "entity": { + "mapId": 210, + "position": { + "x": 25, + "y": 20, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "thieves_hideout_pre_key" + }, + { + "description": "Foxy (contemplating water near goddess statue in Thieves Hideout)", + "entity": { + "mapId": [219, 220], + "position": { + "x": 36, + "y": 31, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "thieves_hideout_pre_key" + }, + { + "description": "Foxy (after timed trial in Thieves Hideout)", + "entity": { + "mapId": 196, + "position": { + "x": 49, + "y": 24, + "z": 10 + }, + "orientation": "sw" + }, + "nodeId": "thieves_hideout_post_key" + }, + { + "description": "Foxy (inside Mercator Castle armory tower)", + "entity": { + "mapId": 106, + "position": { + "x": 31, + "y": 30, + "z": 4 + }, + "orientation": "nw" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (near Mercator Castle kitchen)", + "entity": { + "mapId": 71, + "position": { + "x": 15, + "y": 19, + "z": 1 + }, + "orientation": "nw" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (in Mercator Castle library)", + "entity": { + "mapId": 73, + "position": { + "x": 18, + "y": 29, + "z": 0 + }, + "orientation": "nw" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (in Mercator Dungeon main room)", + "entity": { + "mapId": 38, + "position": { + "x": 24, + "y": 35, + "z": 3 + }, + "orientation": "se" + }, + "nodeId": "mercator_dungeon" + }, + { + "description": "Foxy (in hallway before tower in Mercator Dungeon)", + "entity": { + "mapId": 46, + "position": { + "x": 24, + "y": 13, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "mercator_dungeon" + }, + { + "description": "Foxy (atop Mercator Dungeon tower)", + "entity": { + "mapId": 35, + "position": { + "x": 31, + "y": 31, + "z": 12 + }, + "orientation": "nw" + }, + "nodeId": "mercator_dungeon" + }, + { + "description": "Foxy (inside Mercator Crypt)", + "entity": { + "mapId": 647, + "position": { + "x": 30, + "y": 21, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "crypt" + }, + { + "description": "Foxy (on Verla beach)", + "entity": { + "mapId": 474, + "position": { + "x": 43, + "y": 30, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "verla_shore" + }, + { + "description": "Foxy (spying on house in Verla)", + "entity": { + "mapId": [711, 712], + "position": { + "x": 48, + "y": 29, + "z": 5 + }, + "orientation": "nw" + }, + "nodeId": "verla" + }, + { + "description": "Foxy (on upper Verla shore, reachable from Dex exit)", + "entity": { + "mapId": 530, + "position": { + "x": 18, + "y": 29, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (in Verla Mines jar staircase room)", + "entity": { + "mapId": 235, + "position": { + "x": 42, + "y": 22, + "z": 6 + }, + "orientation": "sw" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (in Verla Mines lizards and crates room)", + "entity": { + "mapId": 239, + "position": { + "x": 32, + "y": 31, + "z": 3, + "halfX": True, + "halfY": True + }, + "orientation": "ne" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (in Verla Mines lava room in Slasher sector)", + "entity": { + "mapId": 252, + "position": { + "x": 16, + "y": 13, + "z": 1, + "halfX": True, + "halfY": True + }, + "orientation": "sw" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (in Verla Mines room behind lava)", + "entity": { + "mapId": 265, + "position": { + "x": 13, + "y": 16, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (in Verla Mines lava room in Marley sector)", + "entity": { + "mapId": 264, + "position": { + "x": 18, + "y": 19, + "z": 6, + "halfX": True, + "halfY": True + }, + "orientation": "sw" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (on small rocky ledge in elevator map near Kelketo shop)", + "entity": { + "mapId": 473, + "position": { + "x": 35, + "y": 25, + "z": 8 + }, + "orientation": "se" + }, + "nodeId": "route_verla_destel" + }, + { + "description": "Foxy (contemplating fast currents below Kelketo shop)", + "entity": { + "mapId": 481, + "position": { + "x": 40, + "y": 48, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "route_verla_destel" + }, + { + "description": "Foxy (in Destel)", + "entity": { + "mapId": 726, + "position": { + "x": 48, + "y": 55, + "z": 5 + }, + "orientation": "sw" + }, + "nodeId": "destel" + }, + { + "description": "Foxy (contemplating water near boatmaker house in route after Destel)", + "entity": { + "mapId": 489, + "position": { + "x": 23, + "y": 20, + "z": 1 + }, + "orientation": "ne" + }, + "nodeId": "route_after_destel" + }, + { + "description": "Foxy (looking at Lake Shrine from elevated viewpoint)", + "entity": { + "mapId": 525, + "position": { + "x": 53, + "y": 45, + "z": 5 + }, + "orientation": "ne" + }, + "nodeId": "route_after_destel" + }, + { + "description": "Foxy (on small floating block in Destel Well)", + "entity": { + "mapId": 275, + "position": { + "x": 27, + "y": 36, + "z": 5 + }, + "orientation": "nw" + }, + "nodeId": "destel_well" + }, + { + "description": "Foxy (in Destel Well watery hub room)", + "entity": { + "mapId": 283, + "position": { + "x": 34, + "y": 41, + "z": 2 + }, + "orientation": "nw" + }, + "nodeId": "destel_well" + }, + { + "description": "Foxy (in Destel Well watery room before boss)", + "entity": { + "mapId": 287, + "position": { + "x": 50, + "y": 46, + "z": 8, + "halfX": True, + "halfY": True + }, + "orientation": "nw" + }, + "nodeId": "destel_well" + }, + { + "description": "Foxy (at Destel Well exit on Lake Shrine side)", + "entity": { + "mapId": 545, + "position": { + "x": 58, + "y": 18, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "route_lake_shrine" + }, + { + "description": "Foxy (at crossroads on route to Lake Shrine)", + "entity": { + "mapId": 515, + "position": { + "x": 30, + "y": 20, + "z": 4 + }, + "orientation": "nw" + }, + "nodeId": "route_lake_shrine" + }, + { + "description": "Foxy (on mountainous path to Lake Shrine)", + "entity": { + "mapId": 514, + "position": { + "x": 57, + "y": 24, + "z": 1 + }, + "orientation": "sw" + }, + "nodeId": "route_lake_shrine" + }, + { + "description": "Foxy (in volcano to Lake Shrine)", + "entity": { + "mapId": 522, + "position": { + "x": 50, + "y": 39, + "z": 6, + "halfX": True, + "halfY": True + }, + "orientation": "nw" + }, + "nodeId": "route_lake_shrine" + }, + { + "description": "Foxy (next to Lake Shrine door)", + "entity": { + "mapId": 524, + "position": { + "x": 24, + "y": 51, + "z": 2, + "halfX": True + }, + "orientation": "nw" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (above Greedly's shop)", + "entity": { + "mapId": 503, + "position": { + "x": 23, + "y": 35, + "z": 8 + }, + "orientation": "se" + }, + "nodeId": "route_lake_shrine" + }, + { + "description": "Foxy (contemplating water near Greedly's teleport tree)", + "entity": { + "mapId": 501, + "position": { + "x": 30, + "y": 26, + "z": 5 + }, + "orientation": "sw" + }, + "nodeId": "route_lake_shrine" + }, + { + "description": "Foxy (in room after golem hops riddle in Lake Shrine)", + "entity": { + "mapId": 298, + "position": { + "x": 21, + "y": 19, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (in room next to green golem roundabout in Lake Shrine)", + "entity": { + "mapId": 293, + "position": { + "x": 19, + "y": 18, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (in Lake Shrine 'throne room')", + "entity": { + "mapId": 327, + "position": { + "x": 31, + "y": 31, + "z": 2 + }, + "orientation": "ne" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (in room next to golden golems roundabout in Lake Shrine)", + "entity": { + "mapId": 353, + "position": { + "x": 31, + "y": 20, + "z": 4 + }, + "orientation": "sw" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (in room near white golems roundabout in Lake Shrine)", + "entity": { + "mapId": 329, + "position": { + "x": 25, + "y": 25, + "z": 2, + "halfY": True + }, + "orientation": "nw" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (next to Mir Tower)", + "entity": { + "mapId": 475, + "position": { + "x": 34, + "y": 17, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_sector" + }, + { + "description": "Foxy (on the way to Mir Tower)", + "entity": { + "mapId": 464, + "position": { + "x": 22, + "y": 40, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_sector" + }, + { + "description": "Foxy (near Twinkle Village)", + "entity": { + "mapId": 461, + "position": { + "x": 20, + "y": 21, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_sector" + }, + { + "description": "Foxy (inside Tibor)", + "entity": { + "mapId": 813, + "position": { + "x": 19, + "y": 32, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "tibor" + }, + { + "description": "Foxy (inside Tibor spikeballs room)", + "entity": { + "mapId": 810, + "position": { + "x": 21, + "y": 33, + "z": 2, + "halfX": True, + "halfY": True + }, + "orientation": "ne" + }, + "nodeId": "tibor" + }, + { + "description": "Foxy (near Kado's house)", + "entity": { + "mapId": 430, + "position": { + "x": 24, + "y": 27, + "z": 11 + }, + "orientation": "se" + }, + "nodeId": "route_gumi_ryuma" + }, + { + "description": "Foxy (in Gumi boulder map)", + "entity": { + "mapId": 449, + "position": { + "x": 48, + "y": 20, + "z": 1, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "route_gumi_ryuma" + }, + { + "description": "Foxy (at Waterfall Shrine crossroads)", + "entity": { + "mapId": 425, + "position": { + "x": 22, + "y": 56, + "z": 0, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "route_massan_gumi" + }, + { + "description": "Foxy (in upstairs room inside Waterfall Shrine)", + "entity": { + "mapId": 182, + "position": { + "x": 29, + "y": 19, + "z": 4 + }, + "orientation": "nw" + }, + "nodeId": "waterfall_shrine" + }, + { + "description": "Foxy (inside Waterfall Shrine pit)", + "entity": { + "mapId": 174, + "position": { + "x": 32, + "y": 29, + "z": 1 + }, + "orientation": "sw" + }, + "nodeId": "waterfall_shrine" + }, + { + "description": "Foxy (in Massan)", + "entity": { + "mapId": 592, + "position": { + "x": 24, + "y": 46, + "z": 0, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "massan" + }, + { + "description": "Foxy (in room at the bottom of ladders in Massan Cave)", + "entity": { + "mapId": 805, + "position": { + "x": 34, + "y": 30, + "z": 2, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "massan_cave" + }, + { + "description": "Foxy (in treasure room of Massan Cave)", + "entity": { + "mapId": 807, + "position": { + "x": 28, + "y": 22, + "z": 1 + }, + "orientation": "sw" + }, + "nodeId": "massan_cave" + }, + { + "description": "Foxy (bathing in the swamp next to Swamp Shrine entrance)", + "entity": { + "mapId": 433, + "position": { + "x": 39, + "y": 20, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "massan_cave" + }, + { + "description": "Foxy (in side room of Swamp Shrine accessible without Idol Stone)", + "entity": { + "mapId": 10, + "position": { + "x": 25, + "y": 27, + "z": 2, + "halfX": True + }, + "orientation": "ne" + }, + "nodeId": "route_massan_gumi" + }, + { + "description": "Foxy (in wooden room with falling EkeEke chest in Swamp Shrine)", + "entity": { + "mapId": 7, + "position": { + "x": 29, + "y": 25, + "z": 1, + "halfY": True + }, + "orientation": "nw" + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (in Swamp Shrine carpet room)", + "entity": { + "mapId": 2, + "position": { + "x": 19, + "y": 33, + "z": 4 + }, + "orientation": "se" + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (in Swamp Shrine spikeball storage room)", + "entity": { + "mapId": 16, + "position": { + "x": 25, + "y": 24, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (in Swamp Shrine spiked floor room)", + "entity": { + "mapId": 21, + "position": { + "x": 27, + "y": 17, + "z": 4 + }, + "orientation": "sw" + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (in Mercator Castle backdoor court)", + "entity": { + "mapId": 639, + "position": { + "x": 23, + "y": 15, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (on Greenmaze / Mountainous Area crossroad)", + "entity": { + "mapId": 460, + "position": { + "x": 16, + "y": 27, + "z": 4 + }, + "orientation": "se" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (below Mountainous Area bridge)", + "entity": { + "mapId": 486, + "position": { + "x": 52, + "y": 45, + "z": 5 + }, + "orientation": "se" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in Mountainous Area isolated cave)", + "entity": { + "mapId": 553, + "position": { + "x": 23, + "y": 21, + "z": 3 + }, + "orientation": "ne" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in access to Zak arena inside Mountainous Area)", + "entity": { + "mapId": 487, + "position": { + "x": 44, + "y": 51, + "z": 3 + }, + "orientation": "se" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in Zak arena inside Mountainous Area)", + "entity": { + "mapId": 492, + "position": { + "x": 27, + "y": 55, + "z": 9 + }, + "orientation": "se" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in empty secret room inside Mountainous Area cave)", + "entity": { + "mapId": 552, + "position": { + "x": 24, + "y": 27, + "z": 0, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in empty visible room inside Mountainous Area cave)", + "entity": { + "mapId": 547, + "position": { + "x": 23, + "y": 23, + "z": 0, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in waterfall entrance of Mountainous Area cave)", + "entity": { + "mapId": 549, + "position": { + "x": 27, + "y": 40, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (on Mir Tower sector crossroads)", + "entity": { + "mapId": 458, + "position": { + "x": 21, + "y": 21, + "z": 1 + }, + "orientation": "se", + "highPalette": True + }, + "nodeId": "mir_tower_sector" + }, + { + "description": "Foxy (near Mountainous Area teleport tree)", + "entity": { + "mapId": 484, + "position": { + "x": 38, + "y": 57, + "z": 0 + }, + "orientation": "sw", + "highPalette": True + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (on route to Mountainous Area, in rocky arch map)", + "entity": { + "mapId": 500, + "position": { + "x": 19, + "y": 19, + "z": 7 + }, + "orientation": "sw", + "highPalette": True + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (on route to Mountainous Area, in L-shaped turn map)", + "entity": { + "mapId": 540, + "position": { + "x": 16, + "y": 23, + "z": 3 + }, + "orientation": "se", + "halfY": True, + "highPalette": True + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in map next to Mountainous Area goddess statue)", + "entity": { + "mapId": 518, + "position": { + "x": 38, + "y": 33, + "z": 12 + }, + "orientation": "sw", + "highPalette": True + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in King Nole's Cave isolated chest room)", + "entity": { + "mapId": 156, + "position": { + "x": 21, + "y": 27, + "z": 0, + "halfX": True + }, + "orientation": "ne" + }, + "nodeId": "king_nole_cave" + }, + { + "description": "Foxy (in King Nole's Cave crate stairway room)", + "entity": { + "mapId": 158, + "position": { + "x": 29, + "y": 26, + "z": 6 + }, + "orientation": "sw", + "highPalette": True + }, + "nodeId": "king_nole_cave" + }, + { + "description": "Foxy (in room before boulder hallway inside King Nole's Cave)", + "entity": { + "mapId": 147, + "position": { + "x": 26, + "y": 23, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "king_nole_cave" + }, + { + "description": "Foxy (in empty isolated room inside King Nole's Cave)", + "entity": { + "mapId": 162, + "position": { + "x": 26, + "y": 17, + "z": 0, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "king_nole_cave" + }, + { + "description": "Foxy (looking at the waterfall in King Nole's Cave)", + "entity": { + "mapId": 164, + "position": { + "x": 22, + "y": 48, + "z": 1 + }, + "orientation": "sw", + "highPalette": True + }, + "nodeId": "king_nole_cave" + }, + { + "description": "Foxy (in King Nole's Cave teleporter to Kazalt)", + "entity": { + "mapId": 170, + "position": { + "x": 22, + "y": 27, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "king_nole_cave" + }, + { + "description": "Foxy (in access to Kazalt)", + "entity": { + "mapId": 739, + "position": { + "x": 17, + "y": 28, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "kazalt" + }, + { + "description": "Foxy (on Kazalt bridge)", + "entity": { + "mapId": 737, + "position": { + "x": 46, + "y": 34, + "z": 7 + }, + "orientation": "se" + }, + "nodeId": "kazalt" + }, + { + "description": "Foxy (in Mir Tower 0F isolated chest room)", + "entity": { + "mapId": 757, + "position": { + "x": 19, + "y": 24, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_pre_garlic" + }, + { + "description": "Foxy (in Mir Tower activatable bridge room)", + "entity": { + "mapId": [752, 753], + "position": { + "x": 29, + "y": 34, + "z": 3, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "mir_tower_pre_garlic" + }, + { + "description": "Foxy (in Garlic trial room inside Mir Tower)", + "entity": { + "mapId": 750, + "position": { + "x": 22, + "y": 21, + "z": 4 + }, + "orientation": "sw" + }, + "nodeId": "mir_tower_pre_garlic" + }, + { + "description": "Foxy (in Mir Tower library)", + "entity": { + "mapId": 759, + "position": { + "x": 38, + "y": 29, + "z": 4 + }, + "orientation": "ne" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in Mir Tower priest room)", + "entity": { + "mapId": 775, + "position": { + "x": 23, + "y": 22, + "z": 1, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (right after making Miro flee with Garlic in Mir Tower)", + "entity": { + "mapId": 758, + "position": { + "x": 14, + "y": 34, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in falling spikeballs room inside Mir Tower)", + "entity": { + "mapId": 761, + "position": { + "x": 14, + "y": 24, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in first room of Mir Tower teleporter maze)", + "entity": { + "mapId": 767, + "position": { + "x": 18, + "y": 18, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in small spikeballs room of Mir Tower teleporter maze)", + "entity": { + "mapId": 771, + "position": { + "x": 18, + "y": 18, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in wooden elevators room after Mir Tower teleporter maze)", + "entity": { + "mapId": 779, + "position": { + "x": 32, + "y": 20, + "z": 7, + "halfY": True + }, + "orientation": "nw" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in room before Mir Tower boss room)", + "entity": { + "mapId": 783, + "position": { + "x": 32, + "y": 19, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in Mir Tower treasure room)", + "entity": { + "mapId": 781, + "position": { + "x": 53, + "y": 26, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (next to Waterfall Shrine entrance)", + "entity": { + "mapId": 426, + "position": { + "x": 46, + "y": 31, + "z": 0, + "halfX": True, + "halfY": True + }, + "orientation": "sw" + }, + "nodeId": "route_massan_gumi" + }, + { + "description": "Foxy (looking at river next to Massan teleport tree)", + "entity": { + "mapId": 424, + "position": { + "x": 44, + "y": 35, + "z": 0 + }, + "orientation": "nw" + }, + "nodeId": "route_massan_gumi" + }, + { + "description": "Foxy (looking at bush at Swamp Shrine crossroads)", + "entity": { + "mapId": 440, + "position": { + "x": 25, + "y": 42, + "z": 4 + }, + "orientation": "nw", + "highPalette": True + }, + "nodeId": "route_massan_gumi" + }, + { + "description": "Foxy (at Helga's Hut crossroads)", + "entity": { + "mapId": 447, + "position": { + "x": 24, + "y": 17, + "z": 1 + }, + "orientation": "se", + "highPalette": True + }, + "nodeId": "route_gumi_ryuma" + }, + { + "description": "Foxy (near Helga's Hut)", + "entity": { + "mapId": 444, + "position": { + "x": 25, + "y": 26, + "z": 7 + }, + "orientation": "sw" + }, + "nodeId": "route_gumi_ryuma" + }, + { + "description": "Foxy (in reapers room at Greenmaze entrance)", + "entity": { + "mapId": 571, + "position": { + "x": 31, + "y": 20, + "z": 6 + }, + "orientation": "nw", + "highPalette": True + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (near Greenmaze swamp)", + "entity": { + "mapId": 566, + "position": { + "x": 53, + "y": 51, + "z": 1 + }, + "orientation": "ne" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (spying on Cutter in Greenmaze)", + "entity": { + "mapId": 560, + "position": { + "x": 31, + "y": 52, + "z": 9 + }, + "orientation": "nw" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (in sector with red orcs making an elevator appear in Greenmaze)", + "entity": { + "mapId": 565, + "position": { + "x": 50, + "y": 30, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (in center of Greenmaze)", + "entity": { + "mapId": 576, + "position": { + "x": 32, + "y": 38, + "z": 5, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (in waterfall sector of Greenmaze)", + "entity": { + "mapId": 568, + "position": { + "x": 29, + "y": 41, + "z": 7, + "halfX": True + }, + "orientation": "ne", + "highPalette": True + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (in ropes sector of Greenmaze)", + "entity": { + "mapId": 567, + "position": { + "x": 38, + "y": 28, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (in Sun Stone sector of Greenmaze)", + "entity": { + "mapId": 564, + "position": { + "x": 30, + "y": 35, + "z": 1 + }, + "orientation": "sw" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (in first chest map of Greenmaze after cutting trees)", + "entity": { + "mapId": 570, + "position": { + "x": 26, + "y": 15, + "z": 1 + }, + "orientation": "sw" + }, + "nodeId": "greenmaze_post_whistle" + }, + { + "description": "Foxy (near shortcut cavern entrance in Greenmaze after cutting trees)", + "entity": { + "mapId": 569, + "position": { + "x": 20, + "y": 24, + "z": 6, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "greenmaze_post_whistle" + }, + { + "description": "Foxy (in room next to spiked floor and keydoor room in King Nole's Labyrinth)", + "entity": { + "mapId": 380, + "position": { + "x": 17, + "y": 18, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "king_nole_labyrinth_pre_door" + }, + { + "description": "Foxy (in ice shortcut room in King Nole's Labyrinth)", + "entity": { + "mapId": 390, + "position": { + "x": 19, + "y": 41, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "king_nole_labyrinth_pre_door" + }, + { + "description": "Foxy (in exterior room of King Nole's Labyrinth)", + "entity": { + "mapId": 362, + "position": { + "x": 35, + "y": 21, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "king_nole_labyrinth_pre_door" + }, + { + "description": "Foxy (in room above Iron Boots in King Nole's Labyrinth)", + "entity": { + "mapId": 373, + "position": { + "x": 26, + "y": 30, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "king_nole_labyrinth_post_door" + }, + { + "description": "Foxy (next to raft starting point in King Nole's Labyrinth)", + "entity": { + "mapId": 406, + "position": { + "x": 46, + "y": 40, + "z": 7 + }, + "orientation": "nw", + "highPalette": True + }, + "nodeId": "king_nole_labyrinth_raft_entrance" + }, + { + "description": "Foxy (in fast boulder room in King Nole's Labyrinth)", + "entity": { + "mapId": 382, + "position": { + "x": 30, + "y": 30, + "z": 7, + "halfX": True + }, + "orientation": "ne" + }, + "nodeId": "king_nole_labyrinth_post_door" + }, + { + "description": "Foxy (in first maze room inside King Nole's Labyrinth)", + "entity": { + "mapId": 367, + "position": { + "x": 43, + "y": 38, + "z": 1 + }, + "orientation": "sw" + }, + "nodeId": "king_nole_labyrinth_post_door" + }, + { + "description": "Foxy (in lava sector of King Nole's Labyrinth)", + "entity": { + "mapId": 399, + "position": { + "x": 23, + "y": 19, + "z": 2, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "king_nole_labyrinth_post_door" + }, + { + "description": "Foxy (in hands room inside King Nole's Labyrinth)", + "entity": { + "mapId": 418, + "position": { + "x": 41, + "y": 31, + "z": 7 + }, + "orientation": "sw" + }, + "nodeId": "king_nole_labyrinth_post_door" + }, + { + "description": "Foxy (next to King Nole's Palace entrance)", + "entity": { + "mapId": 422, + "position": { + "x": 27, + "y": 25, + "z": 2 + }, + "orientation": "ne" + }, + "nodeId": "king_nole_labyrinth_path_to_palace" + }, + { + "description": "Foxy (in King Nole's Palace entrance room)", + "entity": { + "mapId": 122, + "position": { + "x": 30, + "y": 35, + "z": 8 + }, + "orientation": "ne" + }, + "nodeId": "king_nole_palace" + }, + { + "description": "Foxy (in King Nole's Palace jar and moving platforms room)", + "entity": { + "mapId": 126, + "position": { + "x": 27, + "y": 37, + "z": 6 + }, + "orientation": "se" + }, + "nodeId": "king_nole_palace" + }, + { + "description": "Foxy (in King Nole's Palace last chest room)", + "entity": { + "mapId": 125, + "position": { + "x": 25, + "y": 39, + "z": 2 + }, + "orientation": "ne" + }, + "nodeId": "king_nole_palace" + }, + { + "description": "Foxy (in Mercator casino)", + "entity": { + "mapId": 663, + "position": { + "x": 16, + "y": 58, + "z": 0, + "halfX": True, + "halfY": True + }, + "orientation": "ne" + }, + "nodeId": "mercator_casino" + }, + { + "description": "Foxy (in Helga's hut basement)", + "entity": { + "mapId": 479, + "position": { + "x": 20, + "y": 33, + "z": 0, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "helga_hut" + }, + { + "description": "Foxy (in Helga's hut dungeon deepest room)", + "entity": { + "mapId": 802, + "position": { + "x": 28, + "y": 19, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "helga_hut" + }, + { + "description": "Foxy (in Helga's hut dungeon topmost room)", + "entity": { + "mapId": 786, + "position": { + "x": 25, + "y": 23, + "z": 2, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "helga_hut" + }, + { + "description": "Foxy (in Swamp Shrine right aisle room)", + "entity": { + "mapId": 1, + "position": { + "x": 34, + "y": 20, + "z": 2 + }, + "orientation": "se", + "highPalette": True + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (upstairs in Swamp Shrine main hall)", + "entity": { + "mapId": [5, 15], + "position": { + "x": 45, + "y": 24, + "z": 8, + "halfY": True + }, + "orientation": "nw" + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (in room before boss inside Swamp Shrine)", + "entity": { + "mapId": 30, + "position": { + "x": 19, + "y": 25, + "z": 2, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (in Thieves Hideout entrance room)", + "entity": { + "mapId": [185, 186], + "position": { + "x": 40, + "y": 35, + "z": 2 + }, + "orientation": "se", + "highPalette": True + }, + "nodeId": "thieves_hideout_pre_key" + }, + { + "description": "Foxy (in Thieves Hideout room with hidden door behind waterfall)", + "entity": { + "mapId": [192, 193], + "position": { + "x": 30, + "y": 34, + "z": 1 + }, + "orientation": "nw" + }, + "nodeId": "thieves_hideout_pre_key" + }, + { + "description": "Foxy (in Thieves Hideout double chest room before goddess statue)", + "entity": { + "mapId": 215, + "position": { + "x": 17, + "y": 17, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "thieves_hideout_pre_key" + }, + { + "description": "Foxy (in hub room after Thieves Hideout keydoor)", + "entity": { + "mapId": 199, + "position": { + "x": 24, + "y": 52, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "thieves_hideout_post_key" + }, + { + "description": "Foxy (in reward room after Thieves Hideout moving balls riddle)", + "entity": { + "mapId": 205, + "position": { + "x": 32, + "y": 24, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "thieves_hideout_post_key" + }, + { + "description": "Foxy (in Lake Shrine main hallway)", + "entity": { + "mapId": 302, + "position": { + "x": 20, + "y": 19, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (in triple chest room in Slasher sector of Verla Mines)", + "entity": { + "mapId": 256, + "position": { + "x": 23, + "y": 23, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (near teleport tree after Destel)", + "entity": { + "mapId": 488, + "position": { + "x": 28, + "y": 53, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "route_after_destel" + }, + { + "description": "Foxy (in lower half of mimics room in King Nole's Labyrinth)", + "entity": { + "mapId": 383, + "position": { + "x": 26, + "y": 26, + "z": 2 + }, + "orientation": "nw" + }, + "nodeId": "king_nole_labyrinth_pre_door" + } +] diff --git a/worlds/landstalker/data/item_source.py b/worlds/landstalker/data/item_source.py new file mode 100644 index 000000000000..e0a2d701f4bf --- /dev/null +++ b/worlds/landstalker/data/item_source.py @@ -0,0 +1,2017 @@ +ITEM_SOURCES_JSON = [ + { + "name": "Swamp Shrine (0F): chest in room to the right", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 0 + }, + { + "name": "Swamp Shrine (0F): chest in carpet room", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 1 + }, + { + "name": "Swamp Shrine (0F): chest in left hallway (accessed by falling from upstairs)", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 2 + }, + { + "name": "Swamp Shrine (0F): falling chest after beating orc", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 3 + }, + { + "name": "Swamp Shrine (0F): chest in room visible from second entrance", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 4 + }, + { + "name": "Swamp Shrine (1F): lower chest in wooden bridges room", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 5 + }, + { + "name": "Swamp Shrine (2F): upper chest in wooden bridges room", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 6 + }, + { + "name": "Swamp Shrine (2F): chest on spiked floor room balcony", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 7 + }, + { + "name": "Swamp Shrine (3F): chest in boss arena", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 8 + }, + { + "name": "Mercator Dungeon (-1F): chest on elevated path near entrance", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "hidden in the depths of Mercator" + ], + "chestId": 9 + }, + { + "name": "Mercator Dungeon (-1F): chest in Moralis's cell", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "hidden in the depths of Mercator" + ], + "chestId": 10 + }, + { + "name": "Mercator Dungeon (-1F): left chest in undeground double chest room", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "hidden in the depths of Mercator" + ], + "chestId": 11 + }, + { + "name": "Mercator Dungeon (-1F): right chest in undeground double chest room", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "hidden in the depths of Mercator" + ], + "chestId": 12 + }, + { + "name": "Mercator: castle kitchen chest", + "type": "chest", + "nodeId": "mercator", + "chestId": 13 + }, + { + "name": "Mercator: chest in special shop backroom", + "type": "chest", + "nodeId": "mercator", + "chestId": 14 + }, + { + "name": "Mercator Dungeon (1F): left chest in tower double chest room", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "inside a tower" + ], + "chestId": 15 + }, + { + "name": "Mercator Dungeon (1F): right chest in tower double chest room", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "inside a tower" + ], + "chestId": 16 + }, + { + "name": "Mercator: chest in castle tower (ladder revealed by slashing armor)", + "type": "chest", + "nodeId": "mercator", + "hints": [ + "inside a tower" + ], + "chestId": 17 + }, + { + "name": "Mercator Dungeon (4F): chest in topmost tower room", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "inside a tower" + ], + "chestId": 18 + }, + { + "name": "King Nole's Palace: chest at entrance", + "type": "chest", + "nodeId": "king_nole_palace", + "chestId": 19 + }, + { + "name": "King Nole's Palace: chest along central pit", + "type": "chest", + "nodeId": "king_nole_palace", + "chestId": 20 + }, + { + "name": "King Nole's Palace: chest in floating button room", + "type": "chest", + "nodeId": "king_nole_palace", + "chestId": 21 + }, + { + "name": "King Nole's Cave: chest in second room", + "type": "chest", + "nodeId": "king_nole_cave", + "chestId": 22 + }, + { + "name": "King Nole's Cave: first chest in third room", + "type": "chest", + "nodeId": "king_nole_cave", + "chestId": 24 + }, + { + "name": "King Nole's Cave: second chest in third room", + "type": "chest", + "nodeId": "king_nole_cave", + "chestId": 25 + }, + { + "name": "King Nole's Cave: chest in isolated room", + "type": "chest", + "nodeId": "king_nole_cave", + "chestId": 28 + }, + { + "name": "King Nole's Cave: chest in crate room", + "type": "chest", + "nodeId": "king_nole_cave", + "chestId": 29 + }, + { + "name": "King Nole's Cave: boulder chase hallway chest", + "type": "chest", + "nodeId": "king_nole_cave", + "chestId": 31 + }, + { + "name": "Waterfall Shrine: chest under entrance hallway", + "type": "chest", + "nodeId": "waterfall_shrine", + "chestId": 33 + }, + { + "name": "Waterfall Shrine: chest near Prospero", + "type": "chest", + "nodeId": "waterfall_shrine", + "chestId": 34 + }, + { + "name": "Waterfall Shrine: chest on right branch of biggest room", + "type": "chest", + "nodeId": "waterfall_shrine", + "chestId": 35 + }, + { + "name": "Waterfall Shrine: upstairs chest", + "type": "chest", + "nodeId": "waterfall_shrine", + "chestId": 36 + }, + { + "name": "Thieves Hideout: chest under water in entrance room", + "type": "chest", + "nodeId": "thieves_hideout_pre_key", + "chestId": 38 + }, + { + "name": "Thieves Hideout (back): right chest after teal knight mini-boss", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 41 + }, + { + "name": "Thieves Hideout (back): left chest after teal knight mini-boss", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 42 + }, + { + "name": "Thieves Hideout: left chest in Pockets cell", + "type": "chest", + "nodeId": "thieves_hideout_pre_key", + "chestId": 43 + }, + { + "name": "Thieves Hideout: right chest in Pockets cell", + "type": "chest", + "nodeId": "thieves_hideout_pre_key", + "chestId": 44 + }, + { + "name": "Thieves Hideout (back): second chest in hallway after quick climb trial", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 45 + }, + { + "name": "Thieves Hideout (back): first chest in hallway after quick climb trial", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 46 + }, + { + "name": "Thieves Hideout (back): chest in moving platforms room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 47 + }, + { + "name": "Thieves Hideout (back): chest in falling platforms room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 48 + }, + { + "name": "Thieves Hideout (back): reward chest after moving balls room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 49 + }, + { + "name": "Thieves Hideout: rolling boulder chest near entrance", + "type": "chest", + "nodeId": "thieves_hideout_pre_key", + "chestId": 50 + }, + { + "name": "Thieves Hideout: left chest in room on the way to goddess statue", + "type": "chest", + "nodeId": "thieves_hideout_pre_key", + "chestId": 52 + }, + { + "name": "Thieves Hideout: right chest in room on the way to goddess statue", + "type": "chest", + "nodeId": "thieves_hideout_pre_key", + "chestId": 53 + }, + { + "name": "Thieves Hideout (back): left chest in room before boss", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 54 + }, + { + "name": "Thieves Hideout (back): right chest in room before boss", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 55 + }, + { + "name": "Thieves Hideout (back): chest #1 in boss reward room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 56 + }, + { + "name": "Thieves Hideout (back): chest #2 in boss reward room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 57 + }, + { + "name": "Thieves Hideout (back): chest #3 in boss reward room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 58 + }, + { + "name": "Thieves Hideout (back): chest #4 in boss reward room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 59 + }, + { + "name": "Thieves Hideout (back): chest #5 in boss reward room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 60 + }, + { + "name": "Verla Mines: right chest in double chest room near entrance", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 66 + }, + { + "name": "Verla Mines: left chest in double chest room near entrance", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 67 + }, + { + "name": "Verla Mines: chest on jar staircase room balcony", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 68 + }, + { + "name": "Verla Mines: Dex reward chest", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 69 + }, + { + "name": "Verla Mines: Slasher reward chest", + "type": "chest", + "nodeId": "verla_mines", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 70 + }, + { + "name": "Verla Mines: left chest in 3-chests room near Slasher", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 71 + }, + { + "name": "Verla Mines: middle chest in 3-chests room near Slasher", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 72 + }, + { + "name": "Verla Mines: right chest in 3-chests room near Slasher", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 73 + }, + { + "name": "Verla Mines: right chest in button room near elevator shaft leading to Marley", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 74 + }, + { + "name": "Verla Mines: left chest in button room near elevator shaft leading to Marley", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 75 + }, + { + "name": "Verla Mines: chest in hidden room accessed by walking on lava", + "type": "chest", + "nodeId": "verla_mines_behind_lava", + "hints": [ + "in a very hot place" + ], + "chestId": 76 + }, + { + "name": "Destel Well (0F): 4 crates puzzle room chest", + "type": "chest", + "nodeId": "destel_well", + "chestId": 77 + }, + { + "name": "Destel Well (1F): chest on small stairs", + "type": "chest", + "nodeId": "destel_well", + "chestId": 78 + }, + { + "name": "Destel Well (1F): chest on narrow floating ground", + "type": "chest", + "nodeId": "destel_well", + "chestId": 79 + }, + { + "name": "Destel Well (1F): chest in spiky hallway", + "type": "chest", + "nodeId": "destel_well", + "chestId": 80 + }, + { + "name": "Destel Well (2F): chest in ghosts room", + "type": "chest", + "nodeId": "destel_well", + "chestId": 81 + }, + { + "name": "Destel Well (2F): chest in falling platforms room", + "type": "chest", + "nodeId": "destel_well", + "chestId": 82 + }, + { + "name": "Destel Well (2F): right chest in Pockets room", + "type": "chest", + "nodeId": "destel_well", + "chestId": 83 + }, + { + "name": "Destel Well (2F): left chest in Pockets room", + "type": "chest", + "nodeId": "destel_well", + "chestId": 84 + }, + { + "name": "Destel Well (3F): chest in first trapped arena", + "type": "chest", + "nodeId": "destel_well", + "chestId": 85 + }, + { + "name": "Destel Well (3F): chest in trapped giants room", + "type": "chest", + "nodeId": "destel_well", + "chestId": 86 + }, + { + "name": "Destel Well (3F): chest in second trapped arena", + "type": "chest", + "nodeId": "destel_well", + "chestId": 87 + }, + { + "name": "Destel Well (4F): top chest in room before boss", + "type": "chest", + "nodeId": "destel_well", + "chestId": 88 + }, + { + "name": "Destel Well (4F): left chest in room before boss", + "type": "chest", + "nodeId": "destel_well", + "chestId": 89 + }, + { + "name": "Destel Well (4F): bottom chest in room before boss", + "type": "chest", + "nodeId": "destel_well", + "chestId": 90 + }, + { + "name": "Destel Well (4F): right chest in room before boss", + "type": "chest", + "nodeId": "destel_well", + "chestId": 91 + }, + { + "name": "Lake Shrine (-1F): chest in crate room near green golem spinner", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 92 + }, + { + "name": "Lake Shrine (-1F): chest in hallway with hole leading downstairs", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 93 + }, + { + "name": "Lake Shrine (-1F): chest in spikeballs hallway near green golem spinner", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 94 + }, + { + "name": "Lake Shrine (-1F): reward chest for golem hopping puzzle", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 95 + }, + { + "name": "Lake Shrine (-2F): chest on room corner accessed by falling from above", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 96 + }, + { + "name": "Lake Shrine (-2F): lower chest in throne room", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 97 + }, + { + "name": "Lake Shrine (-2F): upper chest in throne room", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 98 + }, + { + "name": "Lake Shrine (-3F): chest on floating platform in white golems room", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 99 + }, + { + "name": "Lake Shrine (-3F): chest near Sword of Ice", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 100 + }, + { + "name": "Lake Shrine (-3F): chest in snake trapping puzzle room", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 101 + }, + { + "name": "Lake Shrine (-3F): chest on cube accessed by falling from upstairs", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 102 + }, + { + "name": "Lake Shrine (-3F): chest in watery archway room", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 103 + }, + { + "name": "Lake Shrine (-3F): left reward chest in boss room", + "type": "chest", + "nodeId": "lake_shrine", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 104 + }, + { + "name": "Lake Shrine (-3F): middle reward chest in boss room", + "type": "chest", + "nodeId": "lake_shrine", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 105 + }, + { + "name": "Lake Shrine (-3F): right reward chest in boss room", + "type": "chest", + "nodeId": "lake_shrine", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 106 + }, + { + "name": "Lake Shrine (-3F): chest near golden golems spinner", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 107 + }, + { + "name": "King Nole's Labyrinth (0F): chest in exterior room", + "type": "chest", + "nodeId": "king_nole_labyrinth_exterior", + "chestId": 108 + }, + { + "name": "King Nole's Labyrinth (0F): left chest in room after key door", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 109 + }, + { + "name": "King Nole's Labyrinth (0F): right chest in room after key door", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 110 + }, + { + "name": "King Nole's Labyrinth (-1F): chest in maze room with healing tile", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 111 + }, + { + "name": "King Nole's Labyrinth (0F): chest in spike balls room", + "type": "chest", + "nodeId": "king_nole_labyrinth_pre_door", + "chestId": 112 + }, + { + "name": "King Nole's Labyrinth (-1F): right chest in 3-chest dark room (left side)", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 113 + }, + { + "name": "King Nole's Labyrinth (-1F): chest in 3-chest dark room (right side)", + "type": "chest", + "nodeId": "king_nole_labyrinth_pre_door", + "chestId": 114 + }, + { + "name": "King Nole's Labyrinth (-1F): left chest in 3-chest dark room (left side)", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 115 + }, + { + "name": "King Nole's Labyrinth (-1F): chest in maze room with two buttons", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 116 + }, + { + "name": "King Nole's Labyrinth (-1F): upper chest in lantern room", + "type": "chest", + "nodeId": "king_nole_labyrinth_pre_door", + "chestId": 117 + }, + { + "name": "King Nole's Labyrinth (-1F): lower chest in lantern room", + "type": "chest", + "nodeId": "king_nole_labyrinth_pre_door", + "chestId": 118 + }, + { + "name": "King Nole's Labyrinth (-1F): chest in ice shortcut room", + "type": "chest", + "nodeId": "king_nole_labyrinth_pre_door", + "chestId": 119 + }, + { + "name": "King Nole's Labyrinth (-2F): chest in save room", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 120 + }, + { + "name": "King Nole's Labyrinth (-1F): chest in room with button and crates stairway", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 121 + }, + { + "name": "King Nole's Labyrinth (-3F): first chest before Firedemon", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "hints": [ + "in a very hot place" + ], + "chestId": 122 + }, + { + "name": "King Nole's Labyrinth (-3F): second chest before Firedemon", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "hints": [ + "in a very hot place" + ], + "chestId": 123 + }, + { + "name": "King Nole's Labyrinth (-3F): reward chest for beating Firedemon", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "hints": [ + "kept by a threatening guardian", + "in a very hot place" + ], + "chestId": 124 + }, + { + "name": "King Nole's Labyrinth (-2F): chest in four buttons room", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 125 + }, + { + "name": "King Nole's Labyrinth (-3F): first chest after falling from raft", + "type": "chest", + "nodeId": "king_nole_labyrinth_raft", + "chestId": 126 + }, + { + "name": "King Nole's Labyrinth (-3F): left chest in room before Spinner", + "type": "chest", + "nodeId": "king_nole_labyrinth_raft", + "chestId": 127 + }, + { + "name": "King Nole's Labyrinth (-3F): right chest in room before Spinner", + "type": "chest", + "nodeId": "king_nole_labyrinth_raft", + "chestId": 128 + }, + { + "name": "King Nole's Labyrinth (-3F): reward chest for beating Spinner", + "type": "chest", + "nodeId": "king_nole_labyrinth_raft", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 129 + }, + { + "name": "King Nole's Labyrinth (-3F): chest in room after Spinner", + "type": "chest", + "nodeId": "king_nole_labyrinth_raft", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 130 + }, + { + "name": "King Nole's Labyrinth (-3F): chest in room before Miro", + "type": "chest", + "nodeId": "king_nole_labyrinth_path_to_palace", + "hints": [ + "close to a waterfall" + ], + "chestId": 131 + }, + { + "name": "King Nole's Labyrinth (-3F): reward chest for beating Miro", + "type": "chest", + "nodeId": "king_nole_labyrinth_path_to_palace", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 132 + }, + { + "name": "King Nole's Labyrinth (-3F): chest in hands room", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 133 + }, + { + "name": "Route between Gumi and Ryuma: chest on the way to Swordsman Kado", + "type": "chest", + "nodeId": "route_gumi_ryuma", + "chestId": 134 + }, + { + "name": "Route between Massan and Gumi: chest on cliff", + "type": "chest", + "nodeId": "route_massan_gumi", + "hints": [ + "near a swamp" + ], + "chestId": 135 + }, + { + "name": "Route between Mercator and Verla: chest on cliff next to tree", + "type": "chest", + "nodeId": "mir_tower_sector", + "chestId": 136 + }, + { + "name": "Route between Mercator and Verla: chest on cliff next to blocked cave", + "type": "chest", + "nodeId": "mir_tower_sector", + "chestId": 137 + }, + { + "name": "Route between Mercator and Verla: chest near Twinkle village", + "type": "chest", + "nodeId": "mir_tower_sector", + "chestId": 138 + }, + { + "name": "Verla Shore: chest on corner cliff after Verla tunnel", + "type": "chest", + "nodeId": "verla_shore", + "chestId": 139 + }, + { + "name": "Verla Shore: chest on highest cliff after Verla tunnel (accessible through Verla mines)", + "type": "chest", + "nodeId": "verla_shore_cliff", + "chestId": 140 + }, + { + "name": "Route to Mir Tower: chest on cliff accessed by pressing hidden switch", + "type": "chest", + "nodeId": "mir_tower_sector", + "chestId": 141 + }, + { + "name": "Route to Mir Tower: chest behind first sacred tree", + "type": "chest", + "nodeId": "mir_tower_sector_tree_ledge", + "chestId": 142 + }, + { + "name": "Verla Shore: chest behind cabin", + "type": "chest", + "nodeId": "verla_shore", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 143 + }, + { + "name": "Route to Destel: chest in map right after Verla mines exit", + "type": "chest", + "nodeId": "route_verla_destel", + "chestId": 144 + }, + { + "name": "Route to Destel: chest in small platform elevator map", + "type": "chest", + "nodeId": "route_verla_destel", + "chestId": 145 + }, + { + "name": "Route to Mir Tower: chest behind second sacred tree", + "type": "chest", + "nodeId": "mir_tower_sector_tree_coast", + "chestId": 146 + }, + { + "name": "Route to Destel: hidden chest in map right before Destel", + "type": "chest", + "nodeId": "route_verla_destel", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 147 + }, + { + "name": "Mountainous Area: chest near teleport tree", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 148 + }, + { + "name": "Mountainous Area: chest on right side of map before the bridge", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 149 + }, + { + "name": "Mountainous Area: hidden chest in L-shaped path", + "type": "chest", + "nodeId": "mountainous_area", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 150 + }, + { + "name": "Mountainous Area: hidden chest in uppermost path", + "type": "chest", + "nodeId": "mountainous_area", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 151 + }, + { + "name": "Mountainous Area: isolated chest on cliff in bridge map", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 152 + }, + { + "name": "Mountainous Area: left chest on wall in bridge map", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 153 + }, + { + "name": "Mountainous Area: right chest on wall in bridge map", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 154 + }, + { + "name": "Mountainous Area: right chest in map before Zak arena", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 155 + }, + { + "name": "Mountainous Area: left chest in map before Zak arena", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 156 + }, + { + "name": "Route after Destel: chest on tiny cliff", + "type": "chest", + "nodeId": "route_after_destel", + "chestId": 157 + }, + { + "name": "Route after Destel: hidden chest in map after seeing Duke raft", + "type": "chest", + "nodeId": "route_after_destel", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 158 + }, + { + "name": "Route after Destel: visible chest in map after seeing Duke raft", + "type": "chest", + "nodeId": "route_after_destel", + "chestId": 159 + }, + { + "name": "Mountainous Area: chest hidden under rocky arch", + "type": "chest", + "nodeId": "mountainous_area", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 160 + }, + { + "name": "Route to Lake Shrine: chest on long cliff in crossroads map", + "type": "chest", + "nodeId": "route_lake_shrine", + "chestId": 161 + }, + { + "name": "Route to Lake Shrine: chest on middle cliff in crossroads map (reached from Mountainous Area)", + "type": "chest", + "nodeId": "route_lake_shrine_cliff", + "chestId": 162 + }, + { + "name": "Mountainous Area: chest in map in front of bridge statue", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 163 + }, + { + "name": "Route to Lake Shrine: right chest in volcano", + "type": "chest", + "nodeId": "route_lake_shrine", + "chestId": 164 + }, + { + "name": "Route to Lake Shrine: left chest in volcano", + "type": "chest", + "nodeId": "route_lake_shrine", + "chestId": 165 + }, + { + "name": "Mountainous Area Cave: chest in small hidden room", + "type": "chest", + "nodeId": "mountainous_area", + "hints": [ + "in a small cave", + "in a well-hidden chest", + "in a cave in the mountains" + ], + "chestId": 166 + }, + { + "name": "Mountainous Area Cave: chest in small visible room", + "type": "chest", + "nodeId": "mountainous_area", + "hints": [ + "in a small cave", + "in a cave in the mountains" + ], + "chestId": 167 + }, + { + "name": "Greenmaze: chest on path to Cutter", + "type": "chest", + "nodeId": "greenmaze_cutter", + "chestId": 168 + }, + { + "name": "Greenmaze: chest on cliff near the swamp", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "chestId": 169 + }, + { + "name": "Greenmaze: chest between Sunstone and Massan shortcut", + "type": "chest", + "nodeId": "greenmaze_post_whistle", + "chestId": 170 + }, + { + "name": "Greenmaze: chest in mages room", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "chestId": 171 + }, + { + "name": "Greenmaze: left chest in elbow cave", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "chestId": 172 + }, + { + "name": "Greenmaze: right chest in elbow cave", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "chestId": 173 + }, + { + "name": "Greenmaze: chest in waterfall cave", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "hints": [ + "close to a waterfall" + ], + "chestId": 174 + }, + { + "name": "Greenmaze: left chest in hidden room behind waterfall", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "hints": [ + "close to a waterfall" + ], + "chestId": 175 + }, + { + "name": "Greenmaze: right chest in hidden room behind waterfall", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "hints": [ + "close to a waterfall" + ], + "chestId": 176 + }, + { + "name": "Massan: chest triggered by dog statue", + "type": "chest", + "nodeId": "massan", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 177 + }, + { + "name": "Massan: chest in house nearest to elder house", + "type": "chest", + "nodeId": "massan", + "chestId": 178 + }, + { + "name": "Massan: chest in middle house", + "type": "chest", + "nodeId": "massan", + "chestId": 179 + }, + { + "name": "Massan: chest in house farthest from elder house", + "type": "chest", + "nodeId": "massan", + "chestId": 180 + }, + { + "name": "Gumi: chest on top of bed in house", + "type": "chest", + "nodeId": "gumi", + "chestId": 181 + }, + { + "name": "Gumi: chest in elder house after saving Fara", + "type": "chest", + "nodeId": "gumi_after_swamp_shrine", + "chestId": 182 + }, + { + "name": "Ryuma: chest in mayor's house", + "type": "chest", + "nodeId": "ryuma", + "chestId": 183 + }, + { + "name": "Ryuma: chest in repaired lighthouse", + "type": "chest", + "nodeId": "ryuma_lighthouse_repaired", + "chestId": 184 + }, + { + "name": "Crypt: chest in main room", + "type": "chest", + "nodeId": "crypt", + "chestId": 185 + }, + { + "name": "Crypt: reward chest", + "type": "chest", + "nodeId": "crypt", + "chestId": 186 + }, + { + "name": "Mercator: hidden casino chest", + "type": "chest", + "nodeId": "mercator_casino", + "hints": [ + "hidden in the depths of Mercator", + "in a well-hidden chest" + ], + "chestId": 191 + }, + { + "name": "Mercator: chest in Greenpea's house", + "type": "chest", + "nodeId": "mercator", + "chestId": 192 + }, + { + "name": "Mercator: chest in grandma's house (pot shelving trial)", + "type": "chest", + "nodeId": "mercator", + "chestId": 193 + }, + { + "name": "Verla: chest in well after beating Marley", + "type": "chest", + "nodeId": "verla_after_mines", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 194 + }, + { + "name": "Destel: chest in inn next to innkeeper", + "type": "chest", + "nodeId": "destel", + "chestId": 196 + }, + { + "name": "Mir Tower: timed jump trial chest", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 197 + }, + { + "name": "Mir Tower: chest after mimic room", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 198 + }, + { + "name": "Mir Tower: mimic room chest #1", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 199 + }, + { + "name": "Mir Tower: mimic room chest #2", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 200 + }, + { + "name": "Mir Tower: mimic room chest #3", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 201 + }, + { + "name": "Mir Tower: mimic room chest #4", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 202 + }, + { + "name": "Mir Tower: chest in mushroom pit room", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 203 + }, + { + "name": "Mir Tower: chest in room next to mummy switch room", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 204 + }, + { + "name": "Mir Tower: chest in library accessible from teleporter maze", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "chestId": 205 + }, + { + "name": "Mir Tower: hidden chest in room before library", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 206 + }, + { + "name": "Mir Tower: chest in falling spikeballs room", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "chestId": 207 + }, + { + "name": "Mir Tower: chest in timed challenge room", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "chestId": 208 + }, + { + "name": "Mir Tower: chest in room where Miro closes the door", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "chestId": 209 + }, + { + "name": "Mir Tower: chest after room where Miro closes the door", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "chestId": 210 + }, + { + "name": "Mir Tower: reward chest", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 211 + }, + { + "name": "Mir Tower: right chest in reward room", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 212 + }, + { + "name": "Mir Tower: left chest in reward room", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 213 + }, + { + "name": "Mir Tower: chest behind wall accessible after beating Mir", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 214 + }, + { + "name": "Witch Helga's Hut: end chest", + "type": "chest", + "nodeId": "helga_hut", + "chestId": 215 + }, + { + "name": "Massan Cave: right chest", + "type": "chest", + "nodeId": "massan_cave", + "chestId": 216 + }, + { + "name": "Massan Cave: left chest", + "type": "chest", + "nodeId": "massan_cave", + "chestId": 217 + }, + { + "name": "Tibor: reward chest after boss", + "type": "chest", + "nodeId": "tibor", + "chestId": 218 + }, + { + "name": "Tibor: chest in spike balls room", + "type": "chest", + "nodeId": "tibor", + "chestId": 219 + }, + { + "name": "Tibor: left chest on 2 chest group", + "type": "chest", + "nodeId": "tibor", + "chestId": 220 + }, + { + "name": "Tibor: right chest on 2 chest group", + "type": "chest", + "nodeId": "tibor", + "chestId": 221 + }, + { + "name": "Gumi: item on furniture in elder's house", + "type": "ground", + "nodeId": "gumi", + "entity": {"mapId": 605, "entityId": 2}, + "groundItemId": 1 + }, + { + "name": "Greenmaze: item behind trees requiring Cutter", + "type": "ground", + "nodeId": "greenmaze_post_whistle", + "entity": {"mapId": 564, "entityId": 0}, + "groundItemId": 2 + }, + { + "name": "Verla Mines: item in the corner of lava filled room", + "type": "ground", + "nodeId": "verla_mines", + "hints": [ + "in a very hot place" + ], + "entity": {"mapId": 263, "entityId": 7}, + "groundItemId": 3 + }, + { + "name": "Lake Shrine (-3F): item on ground at the SE exit of the golden golems roundabout", + "type": "ground", + "nodeId": "lake_shrine", + "entity": {"mapId": 333, "entityId": 0}, + "groundItemId": 4 + }, + { + "name": "King Nole's Labyrinth (-3F): item on ground behind waterfall after beating Spinner", + "type": "ground", + "nodeId": "king_nole_labyrinth_raft", + "hints": [ + "kept by a threatening guardian" + ], + "entity": {"mapId": 411, "entityId": 0}, + "groundItemId": 5 + }, + { + "name": "Destel Well: item on platform revealed after beating Quake", + "type": "ground", + "nodeId": "destel_well", + "hints": [ + "kept by a threatening guardian" + ], + "entity": {"mapId": 288, "entityId": 0}, + "groundItemId": 6 + }, + { + "name": "King Nole's Labyrinth (-1F): item on ground in ninjas room", + "type": "ground", + "nodeId": "king_nole_labyrinth_post_door", + "entity": {"mapId": 374, "entityId": 3}, + "groundItemId": 7 + }, + { + "name": "Massan Cave: item on ground in treasure room", + "type": "ground", + "nodeId": "massan_cave", + "entity": {"mapId": 807, "entityId": 4}, + "groundItemId": 8 + }, + { + "name": "King Nole's Labyrinth (-3F): item on floating hands", + "type": "ground", + "nodeId": "king_nole_labyrinth_post_door", + "entity": {"mapId": 418, "entityId": 0}, + "groundItemId": 9 + }, + { + "name": "Lake Shrine (-3F): isolated item on ground requiring raised platform to reach", + "type": "ground", + "nodeId": "lake_shrine", + "entities": [ + {"mapId": 344, "entityId": 0}, + {"mapId": 345, "entityId": 0} + ], + "groundItemId": 10 + }, + { + "name": "King Nole's Labyrinth (-2F): item on ground after falling from exterior room", + "type": "ground", + "nodeId": "king_nole_labyrinth_fall_from_exterior", + "entity": {"mapId": 363, "entityId": 0}, + "groundItemId": 11 + }, + { + "name": "Route after Destel: item on ground on the cliff", + "type": "ground", + "nodeId": "route_after_destel", + "entity": {"mapId": 483, "entityId": 0}, + "groundItemId": 12 + }, + { + "name": "Mountainous Area cave: item on ground behind hidden path", + "type": "ground", + "nodeId": "mountainous_area", + "hints": [ + "in a small cave", + "in a cave in the mountains" + ], + "entity": {"mapId": 553, "entityId": 0}, + "groundItemId": 13 + }, + { + "name": "Witch Helga's Hut: item on furniture", + "type": "ground", + "nodeId": "helga_hut", + "entity": {"mapId": 479, "entityId": 1}, + "groundItemId": 14 + }, + { + "name": "King Nole's Labyrinth (-3F): item on ground climbing back from Firedemon", + "type": "ground", + "nodeId": "king_nole_labyrinth_post_door", + "hints": [ + "in a very hot place" + ], + "entity": {"mapId": 399, "entityId": 0}, + "groundItemId": 15 + }, + { + "name": "Mercator: falling item in castle court", + "type": "ground", + "nodeId": "mercator", + "entity": {"mapId": 32, "entityId": 2}, + "groundItemId": 16 + }, + { + "name": "Lake Shrine (-2F): north item on ground in quadruple items room", + "type": "ground", + "nodeId": "lake_shrine", + "entity": {"mapId": 318, "entityId": 0}, + "groundItemId": 17 + }, + { + "name": "Lake Shrine (-2F): south item on ground in quadruple items room", + "type": "ground", + "nodeId": "lake_shrine", + "entity": {"mapId": 318, "entityId": 1}, + "groundItemId": 18 + }, + { + "name": "Lake Shrine (-2F): west item on ground in quadruple items room", + "type": "ground", + "nodeId": "lake_shrine", + "entity": {"mapId": 318, "entityId": 2}, + "groundItemId": 19 + }, + { + "name": "Lake Shrine (-2F): east item on ground in quadruple items room", + "type": "ground", + "nodeId": "lake_shrine", + "entity": {"mapId": 318, "entityId": 3}, + "groundItemId": 20 + }, + { + "name": "Twinkle Village: first item on ground", + "type": "ground", + "nodeId": "twinkle_village", + "entity": {"mapId": 462, "entityId": 5}, + "groundItemId": 21 + }, + { + "name": "Twinkle Village: second item on ground", + "type": "ground", + "nodeId": "twinkle_village", + "entity": {"mapId": 462, "entityId": 4}, + "groundItemId": 22 + }, + { + "name": "Twinkle Village: third item on ground", + "type": "ground", + "nodeId": "twinkle_village", + "entity": {"mapId": 462, "entityId": 3}, + "groundItemId": 23 + }, + { + "name": "Mir Tower: Priest room item #1", + "type": "ground", + "nodeId": "mir_tower_post_garlic", + "entity": {"mapId": 775, "entityId": 7}, + "groundItemId": 24 + }, + { + "name": "Mir Tower: Priest room item #2", + "type": "ground", + "nodeId": "mir_tower_post_garlic", + "entity": {"mapId": 775, "entityId": 6}, + "groundItemId": 25 + }, + { + "name": "Mir Tower: Priest room item #3", + "type": "ground", + "nodeId": "mir_tower_post_garlic", + "entity": {"mapId": 775, "entityId": 1}, + "groundItemId": 26 + }, + { + "name": "King Nole's Labyrinth (-2F): Left item dropped by sacred tree", + "type": "ground", + "nodeId": "king_nole_labyrinth_sacred_tree", + "entity": {"mapId": 415, "entityId": 2}, + "groundItemId": 27 + }, + { + "name": "King Nole's Labyrinth (-2F): Right item dropped by sacred tree", + "type": "ground", + "nodeId": "king_nole_labyrinth_sacred_tree", + "entity": {"mapId": 415, "entityId": 1}, + "groundItemId": 28 + }, + { + "name": "King Nole's Labyrinth (-3F): First item on ground before Firedemon", + "type": "ground", + "nodeId": "king_nole_labyrinth_post_door", + "hints": [ + "in a very hot place" + ], + "entity": {"mapId": 400, "entityId": 0}, + "groundItemId": 29 + }, + { + "name": "Massan: Shop item #1", + "type": "shop", + "nodeId": "massan", + "entity": {"mapId": 596, "entityId": 1}, + "shopItemId": 1 + }, + { + "name": "Massan: Shop item #2", + "type": "shop", + "nodeId": "massan", + "entity": {"mapId": 596, "entityId": 2}, + "shopItemId": 2 + }, + { + "name": "Massan: Shop item #3", + "type": "shop", + "nodeId": "massan", + "entity": {"mapId": 596, "entityId": 3}, + "shopItemId": 3 + }, + { + "name": "Gumi: Inn item #1", + "type": "shop", + "nodeId": "gumi", + "entity": {"mapId": 608, "entityId": 4}, + "shopItemId": 4 + }, + { + "name": "Gumi: Inn item #2", + "type": "shop", + "nodeId": "gumi", + "entity": {"mapId": 608, "entityId": 2}, + "shopItemId": 5 + }, + { + "name": "Ryuma: Shop item #1", + "type": "shop", + "nodeId": "ryuma_after_thieves_hideout", + "entity": {"mapId": 615, "entityId": 2}, + "shopItemId": 6 + }, + { + "name": "Ryuma: Shop item #2", + "type": "shop", + "nodeId": "ryuma_after_thieves_hideout", + "entity": {"mapId": 615, "entityId": 3}, + "shopItemId": 7 + }, + { + "name": "Ryuma: Shop item #3", + "type": "shop", + "nodeId": "ryuma_after_thieves_hideout", + "entity": {"mapId": 615, "entityId": 4}, + "shopItemId": 8 + }, + { + "name": "Ryuma: Shop item #4", + "type": "shop", + "nodeId": "ryuma_after_thieves_hideout", + "entity": {"mapId": 615, "entityId": 5}, + "shopItemId": 9 + }, + { + "name": "Ryuma: Shop item #5", + "type": "shop", + "nodeId": "ryuma_after_thieves_hideout", + "entity": {"mapId": 615, "entityId": 6}, + "shopItemId": 10 + }, + { + "name": "Ryuma: Inn item", + "type": "shop", + "nodeId": "ryuma", + "entity": {"mapId": 624, "entityId": 3}, + "shopItemId": 11 + }, + { + "name": "Mercator: Shop item #1", + "type": "shop", + "nodeId": "mercator", + "entity": {"mapId": 679, "entityId": 1}, + "shopItemId": 12 + }, + { + "name": "Mercator: Shop item #2", + "type": "shop", + "nodeId": "mercator", + "entity": {"mapId": 679, "entityId": 2}, + "shopItemId": 13 + }, + { + "name": "Mercator: Shop item #3", + "type": "shop", + "nodeId": "mercator", + "entity": {"mapId": 679, "entityId": 3}, + "shopItemId": 14 + }, + { + "name": "Mercator: Shop item #4", + "type": "shop", + "nodeId": "mercator", + "entity": {"mapId": 679, "entityId": 4}, + "shopItemId": 15 + }, + { + "name": "Mercator: Shop item #5", + "type": "shop", + "nodeId": "mercator", + "entity": {"mapId": 679, "entityId": 5}, + "shopItemId": 16 + }, + { + "name": "Mercator: Shop item #6", + "type": "shop", + "nodeId": "mercator", + "entity": {"mapId": 679, "entityId": 6}, + "shopItemId": 17 + }, + { + "name": "Mercator: Special shop item #1", + "type": "shop", + "nodeId": "mercator_special_shop", + "entity": {"mapId": 696, "entityId": 1}, + "shopItemId": 18 + }, + { + "name": "Mercator: Special shop item #2", + "type": "shop", + "nodeId": "mercator_special_shop", + "entity": {"mapId": 696, "entityId": 2}, + "shopItemId": 19 + }, + { + "name": "Mercator: Special shop item #3", + "type": "shop", + "nodeId": "mercator_special_shop", + "entity": {"mapId": 696, "entityId": 3}, + "shopItemId": 20 + }, + { + "name": "Mercator: Special shop item #4", + "type": "shop", + "nodeId": "mercator_special_shop", + "entity": {"mapId": 696, "entityId": 4}, + "shopItemId": 21 + }, + { + "name": "Mercator: Docks shop item #1", + "type": "shop", + "nodeId": "mercator_repaired_docks", + "entities": [ + {"mapId": 644, "entityId": 3}, + {"mapId": 643, "entityId": 9} + ], + "shopItemId": 22 + }, + { + "name": "Mercator: Docks shop item #2", + "type": "shop", + "nodeId": "mercator_repaired_docks", + "entities": [ + {"mapId": 644, "entityId": 4}, + {"mapId": 643, "entityId": 10} + ], + "shopItemId": 23 + }, + { + "name": "Mercator: Docks shop item #3", + "type": "shop", + "nodeId": "mercator_repaired_docks", + "entities": [ + {"mapId": 644, "entityId": 5}, + {"mapId": 643, "entityId": 11} + ], + "shopItemId": 24 + }, + { + "name": "Verla: Shop item #1", + "type": "shop", + "nodeId": "verla", + "entities": [ + {"mapId": 719, "entityId": 0}, + {"mapId": 720, "entityId": 1} + ], + "shopItemId": 25 + }, + { + "name": "Verla: Shop item #2", + "type": "shop", + "nodeId": "verla", + "entities": [ + {"mapId": 719, "entityId": 1}, + {"mapId": 720, "entityId": 2} + ], + "shopItemId": 26 + }, + { + "name": "Verla: Shop item #3", + "type": "shop", + "nodeId": "verla", + "entities": [ + {"mapId": 719, "entityId": 2}, + {"mapId": 720, "entityId": 3} + ], + "shopItemId": 27 + }, + { + "name": "Verla: Shop item #4", + "type": "shop", + "nodeId": "verla", + "entities": [ + {"mapId": 719, "entityId": 4}, + {"mapId": 720, "entityId": 4} + ], + "shopItemId": 28 + }, + { + "name": "Verla: Shop item #5 (extra item after saving town)", + "type": "shop", + "nodeId": "verla_after_mines", + "entity": {"mapId": 720, "entityId": 5}, + "shopItemId": 29 + }, + { + "name": "Route from Verla to Destel: Kelketo shop item #1", + "type": "shop", + "nodeId": "route_verla_destel", + "entity": {"mapId": 517, "entityId": 1}, + "shopItemId": 30 + }, + { + "name": "Route from Verla to Destel: Kelketo shop item #2", + "type": "shop", + "nodeId": "route_verla_destel", + "entity": {"mapId": 517, "entityId": 2}, + "shopItemId": 31 + }, + { + "name": "Route from Verla to Destel: Kelketo shop item #3", + "type": "shop", + "nodeId": "route_verla_destel", + "entity": {"mapId": 517, "entityId": 3}, + "shopItemId": 32 + }, + { + "name": "Route from Verla to Destel: Kelketo shop item #4", + "type": "shop", + "nodeId": "route_verla_destel", + "entity": {"mapId": 517, "entityId": 4}, + "shopItemId": 33 + }, + { + "name": "Route from Verla to Destel: Kelketo shop item #5", + "type": "shop", + "nodeId": "route_verla_destel", + "entity": {"mapId": 517, "entityId": 5}, + "shopItemId": 34 + }, + { + "name": "Destel: Inn item", + "type": "shop", + "nodeId": "destel", + "entity": {"mapId": 729, "entityId": 2}, + "shopItemId": 35 + }, + { + "name": "Destel: Shop item #1", + "type": "shop", + "nodeId": "destel", + "entity": {"mapId": 733, "entityId": 1}, + "shopItemId": 36 + }, + { + "name": "Destel: Shop item #2", + "type": "shop", + "nodeId": "destel", + "entity": {"mapId": 733, "entityId": 2}, + "shopItemId": 37 + }, + { + "name": "Destel: Shop item #3", + "type": "shop", + "nodeId": "destel", + "entity": {"mapId": 733, "entityId": 3}, + "shopItemId": 38 + }, + { + "name": "Destel: Shop item #4", + "type": "shop", + "nodeId": "destel", + "entity": {"mapId": 733, "entityId": 4}, + "shopItemId": 39 + }, + { + "name": "Destel: Shop item #5", + "type": "shop", + "nodeId": "destel", + "entity": {"mapId": 733, "entityId": 5}, + "shopItemId": 40 + }, + { + "name": "Route to Lake Shrine: Greedly's shop item #1", + "type": "shop", + "nodeId": "route_lake_shrine", + "entity": {"mapId": 526, "entityId": 0}, + "shopItemId": 41 + }, + { + "name": "Route to Lake Shrine: Greedly's shop item #2", + "type": "shop", + "nodeId": "route_lake_shrine", + "entity": {"mapId": 526, "entityId": 2}, + "shopItemId": 42 + }, + { + "name": "Route to Lake Shrine: Greedly's shop item #3", + "type": "shop", + "nodeId": "route_lake_shrine", + "entity": {"mapId": 526, "entityId": 3}, + "shopItemId": 43 + }, + { + "name": "Route to Lake Shrine: Greedly's shop item #4", + "type": "shop", + "nodeId": "route_lake_shrine", + "entity": {"mapId": 526, "entityId": 4}, + "shopItemId": 44 + }, + { + "name": "Kazalt: Shop item #1", + "type": "shop", + "nodeId": "kazalt", + "entity": {"mapId": 747, "entityId": 0}, + "shopItemId": 45 + }, + { + "name": "Kazalt: Shop item #2", + "type": "shop", + "nodeId": "kazalt", + "entity": {"mapId": 747, "entityId": 2}, + "shopItemId": 46 + }, + { + "name": "Kazalt: Shop item #3", + "type": "shop", + "nodeId": "kazalt", + "entity": {"mapId": 747, "entityId": 3}, + "shopItemId": 47 + }, + { + "name": "Kazalt: Shop item #4", + "type": "shop", + "nodeId": "kazalt", + "entity": {"mapId": 747, "entityId": 4}, + "shopItemId": 48 + }, + { + "name": "Kazalt: Shop item #5", + "type": "shop", + "nodeId": "kazalt", + "entity": {"mapId": 747, "entityId": 5}, + "shopItemId": 49 + }, + { + "name": "Massan: Elder reward after freeing Fara in Swamp Shrine", + "type": "reward", + "nodeId": "massan_after_swamp_shrine", + "address": 162337, + "flag": {"byte": "0x1004", "bit": 2}, + "rewardId": 0 + }, + { + "name": "Lake Shrine: Mir reward after beating Duke", + "type": "reward", + "nodeId": "lake_shrine", + "address": 166463, + "flag": {"byte": "0x1003", "bit": 0}, + "rewardId": 1 + }, + { + "name": "Greenmaze: Cutter reward for saving Einstein", + "type": "reward", + "nodeId": "greenmaze_cutter", + "address": 166021, + "flag": {"byte": "0x1024", "bit": 4}, + "rewardId": 2 + }, + { + "name": "Mountainous Area: Zak reward after fighting", + "type": "reward", + "nodeId": "mountainous_area", + "hints": [ + "kept by a threatening guardian" + ], + "address": 166515, + "flag": {"byte": "0x1027", "bit": 0}, + "rewardId": 3 + }, + { + "name": "Route between Gumi and Ryuma: Swordsman Kado reward", + "type": "reward", + "nodeId": "route_gumi_ryuma", + "address": 166219, + "flag": {"byte": "0x101B", "bit": 7}, + "rewardId": 4 + }, + { + "name": "Greenmaze: dwarf hidden in the trees", + "type": "reward", + "nodeId": "greenmaze_pre_whistle", + "address": 166111, + "flag": {"byte": "0x1022", "bit": 7}, + "rewardId": 5 + }, + { + "name": "Mercator: Arthur reward (in castle throne room)", + "type": "reward", + "nodeId": "mercator", + "address": 164191, + "flag": {"byte": "0x101B", "bit": 6}, + "rewardId": 6 + }, + { + "name": "Mercator: Fahl's dojo challenge reward", + "type": "reward", + "nodeId": "mercator", + "address": 165029, + "flag": {"byte": "0x101C", "bit": 4}, + "rewardId": 7 + }, + { + "name": "Ryuma: Mayor's first reward", + "type": "reward", + "nodeId": "ryuma_after_thieves_hideout", + "address": 164731, + "flag": {"byte": "0x1004", "bit": 3}, + "rewardId": 8 + }, + { + "name": "Ryuma: Mayor's second reward", + "type": "reward", + "nodeId": "ryuma_after_thieves_hideout", + "address": 164735, + "flag": {"byte": "0x1004", "bit": 3}, + "rewardId": 9 + } +] diff --git a/worlds/landstalker/data/world_node.py b/worlds/landstalker/data/world_node.py new file mode 100644 index 000000000000..f786f9613fba --- /dev/null +++ b/worlds/landstalker/data/world_node.py @@ -0,0 +1,411 @@ +WORLD_NODES_JSON = { + "massan": { + "name": "Massan", + "hints": [ + "in a village", + "in a region inhabited by bears", + "in the village of Massan" + ] + }, + "massan_cave": { + "name": "Massan Cave", + "hints": [ + "in a large cave", + "in a region inhabited by bears", + "in Massan cave" + ] + }, + "route_massan_gumi": { + "name": "Route between Massan and Gumi", + "hints": [ + "on a route", + "in a region inhabited by bears", + "between Massan and Gumi" + ] + }, + "waterfall_shrine": { + "name": "Waterfall Shrine", + "hints": [ + "in a shrine", + "close to a waterfall", + "in a region inhabited by bears", + "in Waterfall Shrine" + ] + }, + "swamp_shrine": { + "name": "Swamp Shrine", + "hints": [ + "in a shrine", + "near a swamp", + "in a region inhabited by bears", + "in Swamp Shrine" + ] + }, + "massan_after_swamp_shrine": { + "name": "Massan (after Swamp Shrine)", + "hints": [ + "in a village", + "in a region inhabited by bears", + "in the village of Massan" + ] + }, + "gumi_after_swamp_shrine": { + "name": "Gumi (after Swamp Shrine)", + "hints": [ + "in a village", + "in a region inhabited by bears", + "in the village of Gumi" + ] + }, + "gumi": { + "name": "Gumi", + "hints": [ + "in a village", + "in a region inhabited by bears", + "in the village of Gumi" + ] + }, + "route_gumi_ryuma": { + "name": "Route from Gumi to Ryuma", + "hints": [ + "on a route", + "in a region inhabited by bears", + "between Gumi and Ryuma" + ] + }, + "tibor": { + "name": "Tibor", + "hints": [ + "among the trees", + "inside the elder tree called Tibor" + ] + }, + "ryuma": { + "name": "Ryuma", + "hints": [ + "in a town", + "in the town of Ryuma" + ] + }, + "ryuma_after_thieves_hideout": { + "name": "Ryuma (after Thieves Hideout)", + "hints": [ + "in a town", + "in the town of Ryuma" + ] + }, + "ryuma_lighthouse_repaired": { + "name": "Ryuma (repaired lighthouse)", + "hints": [ + "in a town", + "in the town of Ryuma" + ] + }, + "thieves_hideout_pre_key": { + "name": "Thieves Hideout (before keydoor)", + "hints": [ + "close to a waterfall", + "in a large cave", + "in the Thieves' Hideout" + ] + }, + "thieves_hideout_post_key": { + "name": "Thieves Hideout (after keydoor)", + "hints": [ + "close to a waterfall", + "in a large cave", + "in the Thieves' Hideout" + ] + }, + "helga_hut": { + "name": "Witch Helga's Hut", + "hints": [ + "near a swamp", + "in the hut of a witch called Helga" + ] + }, + "mercator": { + "name": "Mercator", + "hints": [ + "in a town", + "in the town of Mercator" + ] + }, + "mercator_repaired_docks": { + "name": "Mercator (docks with repaired lighthouse)", + "hints": [ + "in a town", + "in the town of Mercator" + ] + }, + "mercator_casino": { + "name": "Mercator casino" + }, + "mercator_dungeon": { + "name": "Mercator Dungeon" + }, + "crypt": { + "name": "Crypt", + "hints": [ + "hidden in the depths of Mercator", + "in Mercator crypt" + ] + }, + "mercator_special_shop": { + "name": "Mercator special shop", + "hints": [ + "in a town", + "in the town of Mercator" + ] + }, + "mir_tower_sector": { + "name": "Mir Tower sector", + "hints": [ + "on a route", + "near Mir Tower" + ] + }, + "mir_tower_sector_tree_ledge": { + "name": "Mir Tower sector (ledge behind sacred tree)", + "hints": [ + "on a route", + "among the trees", + "near Mir Tower" + ] + }, + "mir_tower_sector_tree_coast": { + "name": "Mir Tower sector (coast behind sacred tree)", + "hints": [ + "on a route", + "among the trees", + "near Mir Tower" + ] + }, + "twinkle_village": { + "name": "Twinkle village", + "hints": [ + "in a village", + "in Twinkle village" + ] + }, + "mir_tower_pre_garlic": { + "name": "Mir Tower (pre-garlic)", + "hints": [ + "inside a tower", + "in Mir Tower" + ] + }, + "mir_tower_post_garlic": { + "name": "Mir Tower (post-garlic)", + "hints": [ + "inside a tower", + "in Mir Tower" + ] + }, + "greenmaze_pre_whistle": { + "name": "Greenmaze (pre-whistle)", + "hints": [ + "among the trees", + "in the infamous Greenmaze" + ] + }, + "greenmaze_cutter": { + "name": "Greenmaze (Cutter hidden sector)", + "hints": [ + "among the trees", + "in the infamous Greenmaze" + ] + }, + "greenmaze_post_whistle": { + "name": "Greenmaze (post-whistle)", + "hints": [ + "among the trees", + "in the infamous Greenmaze" + ] + }, + "verla_shore": { + "name": "Verla shore", + "hints": [ + "on a route", + "near the town of Verla" + ] + }, + "verla_shore_cliff": { + "name": "Verla shore cliff (accessible from Verla Mines)", + "hints": [ + "on a route", + "near the town of Verla" + ] + }, + "verla": { + "name": "Verla", + "hints": [ + "in a town", + "in the town of Verla" + ] + }, + "verla_after_mines": { + "name": "Verla (after mines)", + "hints": [ + "in a town", + "in the town of Verla" + ] + }, + "verla_mines": { + "name": "Verla Mines", + "hints": [ + "in Verla Mines" + ] + }, + "verla_mines_behind_lava": { + "name": "Verla Mines (behind lava)", + "hints": [ + "in Verla Mines" + ] + }, + "route_verla_destel": { + "name": "Route between Verla and Destel", + "hints": [ + "on a route", + "in Destel region", + "between Verla and Destel" + ] + }, + "destel": { + "name": "Destel", + "hints": [ + "in a village", + "in Destel region", + "in the village of Destel" + ] + }, + "route_after_destel": { + "name": "Route after Destel", + "hints": [ + "on a route", + "near a lake", + "in Destel region", + "on the route to the lake after Destel" + ] + }, + "destel_well": { + "name": "Destel Well", + "hints": [ + "in Destel region", + "in a large cave", + "in Destel Well" + ] + }, + "route_lake_shrine": { + "name": "Route to Lake Shrine", + "hints": [ + "on a route", + "near a lake", + "on the mountainous path to Lake Shrine" + ] + }, + "route_lake_shrine_cliff": { + "name": "Route to Lake Shrine cliff", + "hints": [ + "on a route", + "near a lake", + "on the mountainous path to Lake Shrine" + ] + }, + "lake_shrine": { + "name": "Lake Shrine", + "hints": [ + "in a shrine", + "near a lake", + "in Lake Shrine" + ] + }, + "mountainous_area": { + "name": "Mountainous Area", + "hints": [ + "in a mountainous area" + ] + }, + "king_nole_cave": { + "name": "King Nole's Cave", + "hints": [ + "in a large cave", + "in King Nole's cave" + ] + }, + "kazalt": { + "name": "Kazalt", + "hints": [ + "in King Nole's domain", + "in Kazalt" + ] + }, + "king_nole_labyrinth_pre_door": { + "name": "King Nole's Labyrinth (before door)", + "hints": [ + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_post_door": { + "name": "King Nole's Labyrinth (after door)", + "hints": [ + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_exterior": { + "name": "King Nole's Labyrinth (exterior)", + "hints": [ + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_fall_from_exterior": { + "name": "King Nole's Labyrinth (fall from exterior)", + "hints": [ + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_raft_entrance": { + "name": "King Nole's Labyrinth (raft entrance)", + "hints": [ + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_raft": { + "name": "King Nole's Labyrinth (raft)", + "hints": [ + "close to a waterfall", + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_sacred_tree": { + "name": "King Nole's Labyrinth (sacred tree)", + "hints": [ + "among the trees", + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_path_to_palace": { + "name": "King Nole's Labyrinth (path to palace)", + "hints": [ + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_palace": { + "name": "King Nole's Palace", + "hints": [ + "in King Nole's domain", + "in King Nole's palace" + ] + }, + "end": { + "name": "The End" + } +} diff --git a/worlds/landstalker/data/world_path.py b/worlds/landstalker/data/world_path.py new file mode 100644 index 000000000000..f7baba358a48 --- /dev/null +++ b/worlds/landstalker/data/world_path.py @@ -0,0 +1,446 @@ +WORLD_PATHS_JSON = [ + { + "fromId": "massan", + "toId": "massan_cave", + "twoWay": True, + "requiredItems": [ + "Axe Magic" + ] + }, + { + "fromId": "massan", + "toId": "massan_after_swamp_shrine", + "requiredNodes": [ + "swamp_shrine" + ] + }, + { + "fromId": "massan", + "toId": "route_massan_gumi", + "twoWay": True + }, + { + "fromId": "route_massan_gumi", + "toId": "waterfall_shrine", + "twoWay": True + }, + { + "fromId": "route_massan_gumi", + "toId": "swamp_shrine", + "twoWay": True, + "weight": 2, + "requiredItems": [ + "Idol Stone" + ] + }, + { + "fromId": "route_massan_gumi", + "toId": "gumi", + "twoWay": True + }, + { + "fromId": "gumi", + "toId": "gumi_after_swamp_shrine", + "requiredNodes": [ + "swamp_shrine" + ] + }, + { + "fromId": "gumi", + "toId": "route_gumi_ryuma" + }, + { + "fromId": "route_gumi_ryuma", + "toId": "ryuma", + "twoWay": True + }, + { + "fromId": "ryuma", + "toId": "ryuma_after_thieves_hideout", + "requiredNodes": [ + "thieves_hideout_post_key" + ] + }, + { + "fromId": "ryuma", + "toId": "ryuma_lighthouse_repaired", + "twoWay": True, + "requiredItems": [ + "Sun Stone" + ] + }, + { + "fromId": "ryuma", + "toId": "thieves_hideout_pre_key", + "twoWay": True + }, + { + "fromId": "thieves_hideout_pre_key", + "toId": "thieves_hideout_post_key", + "requiredItems": [ + "Key" + ] + }, + { + "fromId": "thieves_hideout_post_key", + "toId": "thieves_hideout_pre_key" + }, + { + "fromId": "route_gumi_ryuma", + "toId": "tibor", + "twoWay": True + }, + { + "fromId": "route_gumi_ryuma", + "toId": "helga_hut", + "twoWay": True, + "requiredItems": [ + "Einstein Whistle" + ], + "requiredNodes": [ + "massan" + ] + }, + { + "fromId": "route_gumi_ryuma", + "toId": "mercator", + "twoWay": True, + "weight": 2, + "requiredItems": [ + "Safety Pass" + ] + }, + { + "fromId": "mercator", + "toId": "mercator_dungeon", + "twoWay": True + }, + { + "fromId": "mercator", + "toId": "crypt", + "twoWay": True + }, + { + "fromId": "mercator", + "toId": "mercator_special_shop", + "twoWay": True, + "requiredItems": [ + "Buyer Card" + ] + }, + { + "fromId": "mercator", + "toId": "mercator_casino", + "twoWay": True, + "requiredItems": [ + "Casino Ticket" + ] + }, + { + "fromId": "mercator", + "toId": "mir_tower_sector", + "twoWay": True + }, + { + "fromId": "mir_tower_sector", + "toId": "twinkle_village", + "twoWay": True + }, + { + "fromId": "mir_tower_sector", + "toId": "mir_tower_sector_tree_ledge", + "twoWay": True, + "requiredItems": [ + "Axe Magic" + ] + }, + { + "fromId": "mir_tower_sector", + "toId": "mir_tower_sector_tree_coast", + "twoWay": True, + "requiredItems": [ + "Axe Magic" + ] + }, + { + "fromId": "mir_tower_sector", + "toId": "mir_tower_pre_garlic", + "requiredItems": [ + "Armlet" + ] + }, + { + "fromId": "mir_tower_pre_garlic", + "toId": "mir_tower_sector" + }, + { + "fromId": "mir_tower_pre_garlic", + "toId": "mir_tower_post_garlic", + "requiredItems": [ + "Garlic" + ] + }, + { + "fromId": "mir_tower_post_garlic", + "toId": "mir_tower_pre_garlic" + }, + { + "fromId": "mir_tower_post_garlic", + "toId": "mir_tower_sector" + }, + { + "fromId": "mercator", + "toId": "greenmaze_pre_whistle", + "weight": 2, + "requiredItems": [ + "Key" + ] + }, + { + "fromId": "greenmaze_pre_whistle", + "toId": "greenmaze_post_whistle", + "requiredItems": [ + "Einstein Whistle" + ] + }, + { + "fromId": "greenmaze_pre_whistle", + "toId": "greenmaze_cutter", + "requiredItems": [ + "EkeEke" + ], + "twoWay": True + }, + { + "fromId": "greenmaze_post_whistle", + "toId": "route_massan_gumi" + }, + { + "fromId": "mercator", + "toId": "mercator_repaired_docks", + "requiredNodes": [ + "ryuma_lighthouse_repaired" + ] + }, + { + "fromId": "mercator_repaired_docks", + "toId": "verla_shore" + }, + { + "fromId": "verla_shore", + "toId": "verla", + "twoWay": True + }, + { + "fromId": "verla", + "toId": "verla_after_mines", + "requiredNodes": [ + "verla_mines" + ], + "twoWay": True + }, + { + "fromId": "verla_shore", + "toId": "verla_mines", + "twoWay": True + }, + { + "fromId": "verla_mines", + "toId": "verla_shore_cliff", + "twoWay": True + }, + { + "fromId": "verla_shore_cliff", + "toId": "verla_shore" + }, + { + "fromId": "verla_shore", + "toId": "mir_tower_sector", + "requiredNodes": [ + "verla_mines" + ], + "twoWay": True + }, + { + "fromId": "verla_mines", + "toId": "route_verla_destel" + }, + { + "fromId": "verla_mines", + "toId": "verla_mines_behind_lava", + "twoWay": True, + "requiredItems": [ + "Fireproof" + ] + }, + { + "fromId": "route_verla_destel", + "toId": "destel", + "twoWay": True + }, + { + "fromId": "destel", + "toId": "route_after_destel", + "twoWay": True + }, + { + "fromId": "destel", + "toId": "destel_well", + "twoWay": True + }, + { + "fromId": "destel_well", + "toId": "route_lake_shrine", + "twoWay": True + }, + { + "fromId": "route_lake_shrine", + "toId": "lake_shrine", + "itemsPlacedWhenCrossing": [ + "Sword of Gaia" + ] + }, + { + "fromId": "lake_shrine", + "toId": "route_lake_shrine" + }, + { + "fromId": "lake_shrine", + "toId": "mir_tower_sector" + }, + { + "fromId": "greenmaze_pre_whistle", + "toId": "mountainous_area", + "twoWay": True, + "requiredItems": [ + "Axe Magic" + ] + }, + { + "fromId": "mountainous_area", + "toId": "route_lake_shrine_cliff", + "twoWay": True, + "requiredItems": [ + "Axe Magic" + ] + }, + { + "fromId": "route_lake_shrine_cliff", + "toId": "route_lake_shrine" + }, + { + "fromId": "mountainous_area", + "toId": "king_nole_cave", + "twoWay": True, + "weight": 2, + "requiredItems": [ + "Gola's Eye" + ] + }, + { + "fromId": "king_nole_cave", + "toId": "mercator" + }, + { + "fromId": "king_nole_cave", + "toId": "kazalt", + "itemsPlacedWhenCrossing": [ + "Lithograph" + ] + }, + { + "fromId": "kazalt", + "toId": "king_nole_cave" + }, + { + "fromId": "kazalt", + "toId": "king_nole_labyrinth_pre_door", + "twoWay": True + }, + { + "fromId": "king_nole_labyrinth_pre_door", + "toId": "king_nole_labyrinth_post_door", + "requiredItems": [ + "Key" + ] + }, + { + "fromId": "king_nole_labyrinth_post_door", + "toId": "king_nole_labyrinth_pre_door" + }, + { + "fromId": "king_nole_labyrinth_pre_door", + "toId": "king_nole_labyrinth_exterior", + "requiredItems": [ + "Iron Boots" + ] + }, + { + "fromId": "king_nole_labyrinth_exterior", + "toId": "king_nole_labyrinth_fall_from_exterior", + "requiredItems": [ + "Axe Magic" + ] + }, + { + "fromId": "king_nole_labyrinth_fall_from_exterior", + "toId": "king_nole_labyrinth_pre_door" + }, + { + "fromId": "king_nole_labyrinth_post_door", + "toId": "king_nole_labyrinth_raft_entrance", + "requiredItems": [ + "Snow Spikes" + ] + }, + { + "fromId": "king_nole_labyrinth_raft_entrance", + "toId": "king_nole_labyrinth_post_door" + }, + { + "fromId": "king_nole_labyrinth_raft_entrance", + "toId": "king_nole_labyrinth_raft", + "requiredItems": [ + "Logs" + ] + }, + { + "fromId": "king_nole_labyrinth_raft", + "toId": "king_nole_labyrinth_raft_entrance" + }, + { + "fromId": "king_nole_labyrinth_post_door", + "toId": "king_nole_labyrinth_path_to_palace", + "requiredItems": [ + "Snow Spikes" + ] + }, + { + "fromId": "king_nole_labyrinth_path_to_palace", + "toId": "king_nole_labyrinth_post_door" + }, + { + "fromId": "king_nole_labyrinth_post_door", + "toId": "king_nole_labyrinth_sacred_tree", + "requiredItems": [ + "Axe Magic" + ], + "requiredNodes": [ + "king_nole_labyrinth_raft_entrance" + ] + }, + { + "fromId": "king_nole_labyrinth_path_to_palace", + "toId": "king_nole_palace", + "twoWay": True + }, + { + "fromId": "king_nole_palace", + "toId": "end", + "requiredItems": [ + "Gola's Fang", + "Gola's Horn", + "Gola's Nail" + ] + } +] \ No newline at end of file diff --git a/worlds/landstalker/data/world_region.py b/worlds/landstalker/data/world_region.py new file mode 100644 index 000000000000..3365a9dfa9e2 --- /dev/null +++ b/worlds/landstalker/data/world_region.py @@ -0,0 +1,299 @@ +WORLD_REGIONS_JSON = [ + { + "name": "Massan", + "hintName": "in the village of Massan", + "nodeIds": [ + "massan", + "massan_after_swamp_shrine" + ] + }, + { + "name": "Massan Cave", + "hintName": "in the cave near Massan", + "nodeIds": [ + "massan_cave" + ], + "darkMapIds": [ + 803, 804, 805, 806, 807 + ] + }, + { + "name": "Route between Massan and Gumi", + "canBeHintedAsRequired": False, + "nodeIds": [ + "route_massan_gumi" + ] + }, + { + "name": "Waterfall Shrine", + "hintName": "in the waterfall shrine", + "nodeIds": [ + "waterfall_shrine" + ], + "darkMapIds": [ + 174, 175, 176, 177, 178, 179, 180, 181, 182 + ] + }, + { + "name": "Swamp Shrine", + "hintName": "in the swamp shrine", + "canBeHintedAsRequired": False, + "nodeIds": [ + "swamp_shrine" + ], + "darkMapIds": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 30 + ] + }, + { + "name": "Gumi", + "hintName": "in the village of Gumi", + "nodeIds": [ + "gumi", + "gumi_after_swamp_shrine" + ] + }, + { + "name": "Route between Gumi and Ryuma", + "canBeHintedAsRequired": False, + "nodeIds": [ + "route_gumi_ryuma" + ] + }, + { + "name": "Tibor", + "hintName": "inside Tibor", + "nodeIds": [ + "tibor" + ], + "darkMapIds": [ + 808, 809, 810, 811, 812, 813, 814, 815 + ] + }, + { + "name": "Ryuma", + "hintName": "in the town of Ryuma", + "nodeIds": [ + "ryuma", + "ryuma_after_thieves_hideout", + "ryuma_lighthouse_repaired" + ] + }, + { + "name": "Thieves Hideout", + "hintName": "in the thieves' hideout", + "nodeIds": [ + "thieves_hideout_pre_key", + "thieves_hideout_post_key" + ], + "darkMapIds": [ + 185, 186, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, + 203, 204, 205, 206, 207, 208, 210, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222 + ] + }, + { + "name": "Witch Helga's Hut", + "hintName": "in witch Helga's hut", + "nodeIds": [ + "helga_hut" + ] + }, + { + "name": "Mercator", + "hintName": "in the town of Mercator", + "nodeIds": [ + "mercator", + "mercator_repaired_docks", + "mercator_casino", + "mercator_special_shop" + ] + }, + { + "name": "Crypt", + "hintName": "in the crypt of Mercator", + "nodeIds": [ + "crypt" + ], + "darkMapIds": [ + 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659 + ] + }, + { + "name": "Mercator Dungeon", + "hintName": "in the dungeon of Mercator", + "nodeIds": [ + "mercator_dungeon" + ], + "darkMapIds": [ + 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 76, 80, 81, 82, 91, 92 + ] + }, + { + "name": "Mir Tower sector", + "hintName": "near Mir Tower", + "canBeHintedAsRequired": False, + "nodeIds": [ + "mir_tower_sector", + "mir_tower_sector_tree_ledge", + "mir_tower_sector_tree_coast", + "twinkle_village" + ] + }, + { + "name": "Mir Tower", + "hintName": "inside Mir Tower", + "canBeHintedAsRequired": False, + "nodeIds": [ + "mir_tower_pre_garlic", + "mir_tower_post_garlic" + ], + "darkMapIds": [ + 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, + 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784 + ] + }, + { + "name": "Greenmaze", + "hintName": "in Greenmaze", + "nodeIds": [ + "greenmaze_pre_whistle", + "greenmaze_post_whistle" + ] + }, + { + "name": "Verla Shore", + "canBeHintedAsRequired": False, + "nodeIds": [ + "verla_shore", + "verla_shore_cliff" + ] + }, + { + "name": "Verla", + "hintName": "in the town of Verla", + "nodeIds": [ + "verla", + "verla_after_mines" + ] + }, + { + "name": "Verla Mines", + "hintName": "in the mines near Verla", + "nodeIds": [ + "verla_mines", + "verla_mines_behind_lava" + ], + "darkMapIds": [ + 227, 228, 229, 230, 231, 232, 233, 234, 235, 237, 239, 240, 241, 242, 243, 244, 246, + 247, 248, 250, 253, 254, 255, 256, 258, 259, 266, 268, 269, 471 + ] + }, + { + "name": "Route between Verla and Destel", + "canBeHintedAsRequired": False, + "nodeIds": [ + "route_verla_destel" + ] + }, + { + "name": "Destel", + "hintName": "in the village of Destel", + "nodeIds": [ + "destel" + ] + }, + { + "name": "Route after Destel", + "canBeHintedAsRequired": False, + "nodeIds": [ + "route_after_destel" + ] + }, + { + "name": "Destel Well", + "hintName": "in Destel well", + "nodeIds": [ + "destel_well" + ], + "darkMapIds": [ + 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290 + ] + }, + { + "name": "Route to Lake Shrine", + "canBeHintedAsRequired": False, + "nodeIds": [ + "route_lake_shrine", + "route_lake_shrine_cliff" + ] + }, + { + "name": "Lake Shrine", + "hintName": "in the lake shrine", + "nodeIds": [ + "lake_shrine" + ], + "darkMapIds": [ + 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, + 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, + 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, + 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354 + ] + }, + { + "name": "Mountainous Area", + "hintName": "in the mountainous area", + "nodeIds": [ + "mountainous_area" + ] + }, + { + "name": "King Nole's Cave", + "hintName": "in King Nole's cave", + "nodeIds": [ + "king_nole_cave" + ], + "darkMapIds": [ + 145, 147, 150, 152, 154, 155, 156, 158, 160, 161, 162, 164, 166, 170, 171, 172 + ] + }, + { + "name": "Kazalt", + "hintName": "in the hidden town of Kazalt", + "nodeIds": [ + "kazalt" + ] + }, + { + "name": "King Nole's Labyrinth", + "hintName": "in King Nole's labyrinth", + "nodeIds": [ + "king_nole_labyrinth_pre_door", + "king_nole_labyrinth_post_door", + "king_nole_labyrinth_exterior", + "king_nole_labyrinth_fall_from_exterior", + "king_nole_labyrinth_path_to_palace", + "king_nole_labyrinth_raft_entrance", + "king_nole_labyrinth_raft", + "king_nole_labyrinth_sacred_tree" + ], + "darkMapIds": [ + 355, 356, 357, 358, 359, 360, 361, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, + 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, + 390, 391, 392, 393, 394, 395, 396, 397, 398, 405, 406, 408, 409, 410, 411, 412, 413, + 414, 415, 416, 417, 418, 419, 420, 422, 423 + ] + }, + { + "name": "King Nole's Palace", + "hintName": "in King Nole's palace", + "nodeIds": [ + "king_nole_palace", + "end" + ], + "darkMapIds": [ + 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, + 131, 132, 133, 134, 135, 136, 137, 138 + ] + } +] \ No newline at end of file diff --git a/worlds/landstalker/data/world_teleport_tree.py b/worlds/landstalker/data/world_teleport_tree.py new file mode 100644 index 000000000000..830f5547201e --- /dev/null +++ b/worlds/landstalker/data/world_teleport_tree.py @@ -0,0 +1,62 @@ +WORLD_TELEPORT_TREES_JSON = [ + [ + { + "name": "Massan tree", + "treeMapId": 512, + "nodeId": "route_massan_gumi" + }, + { + "name": "Tibor tree", + "treeMapId": 534, + "nodeId": "route_gumi_ryuma" + } + ], + [ + { + "name": "Mercator front gate tree", + "treeMapId": 539, + "nodeId": "route_gumi_ryuma" + }, + { + "name": "Verla shore tree", + "treeMapId": 537, + "nodeId": "verla_shore" + } + ], + [ + { + "name": "Destel sector tree", + "treeMapId": 536, + "nodeId": "route_after_destel" + }, + { + "name": "Lake Shrine sector tree", + "treeMapId": 513, + "nodeId": "route_lake_shrine" + } + ], + [ + { + "name": "Mir Tower sector tree", + "treeMapId": 538, + "nodeId": "mir_tower_sector" + }, + { + "name": "Mountainous area tree", + "treeMapId": 535, + "nodeId": "mountainous_area" + } + ], + [ + { + "name": "Greenmaze entrance tree", + "treeMapId": 510, + "nodeId": "greenmaze_pre_whistle" + }, + { + "name": "Greenmaze end tree", + "treeMapId": 511, + "nodeId": "greenmaze_post_whistle" + } + ] +] \ No newline at end of file diff --git a/worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md b/worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md new file mode 100644 index 000000000000..90a79f8bd986 --- /dev/null +++ b/worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md @@ -0,0 +1,60 @@ +# Landstalker: The Treasures of King Nole + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains most of the options you need to +configure and export a config file. + +## What does randomization do to this game? + +All items are shuffled while keeping a logic to make every seed completable. + +Some key items could be obtained in a very different order compared to the vanilla game, leading to very unusual situations. + +The world is made as open as possible while keeping the original locks behind the same items & triggers as vanilla +when that makes sense logic-wise. This puts the emphasis on exploration and gameplay by removing all the scenario +and story-related triggers, giving a wide open world to explore. + +## What items and locations get shuffled? + +All items and locations are shuffled. This includes **chests**, items on **ground**, in **shops**, and given by **NPCs**. + +It's also worth noting that all of these items are shuffled among all worlds, meaning every item can be sent to you +by other players. + +## What are the main differences compared to the vanilla game? + +The **Key** is now a unique item and can open several doors without being consumed, making it a standard progression item. +All key doors are gone, except three of them : + - the Mercator castle backdoor (giving access to Greenmaze sector) + - Thieves Hideout middle door (cutting the level in half) + - King Nole's Labyrinth door near entrance + +--- + +The secondary shop of Mercator requiring to do the traders sidequest in the original game is now unlocked by having +**Buyer Card** in your inventory. + +You will need as many **jewels** as specified in the settings to use the teleporter to go to Kazalt and the final dungeon. +If you find and use the **Lithograph**, it will tell you in which world are each one of your jewels. + +Each seed, there is a random dungeon which is chosen to be the "dark dungeon" where you won't see anything unless you +have the **Lantern** in your inventory. Unlike vanilla, King Nole's Labyrinth no longer has the few dark rooms the lantern +was originally intended for. + +The **Statue of Jypta** is introduced as a real item (instead of just being an intro gimmick) and gives you gold over +time while you're walking, the same way Healing Boots heal you when you walk. + + +## What do I need to know for my first seed? + +It's advised you keep Massan as your starting region for your first seed, since taking another starting region might +be significantly harder, both combat-wise and logic-wise. + +Having fully open & shuffled teleportation trees is an interesting way to play, but is discouraged for beginners +as well since it can force you to go in late-game zones with few Life Stocks. + +Overall, the default settings are good for a beginner-friendly seed, and if you don't feel too confident, you can also +lower the combat difficulty to make it more forgiving. + +*Have fun on your adventure!* diff --git a/worlds/landstalker/docs/landstalker_setup_en.md b/worlds/landstalker/docs/landstalker_setup_en.md new file mode 100644 index 000000000000..9f453c146de3 --- /dev/null +++ b/worlds/landstalker/docs/landstalker_setup_en.md @@ -0,0 +1,119 @@ +# Landstalker Setup Guide + +## Required Software + +- [Landstalker Archipelago Client](https://github.com/Dinopony/randstalker-archipelago/releases) (only available on Windows) +- A compatible emulator to run the game + - [RetroArch](https://retroarch.com?page=platforms) with the Genesis Plus GX core + - [Bizhawk 2.9.1 (x64)](https://tasvideos.org/BizHawk/ReleaseHistory) with the Genesis Plus GX core +- Your legally obtained Landstalker US ROM file (which can be acquired on [Steam](https://store.steampowered.com/app/71118/Landstalker_The_Treasures_of_King_Nole/)) + +## Installation Instructions + +- Unzip the Landstalker Archipelago Client archive into its own folder +- Put your Landstalker ROM (`LandStalker_USA.SGD` on the Steam release) inside this folder +- To launch the client, launch `randstalker_archipelago.exe` inside that folder + +Be aware that you might get antivirus warnings about the client program because one of its main features is to spy +on another process's memory (your emulator). This is something antiviruses obviously dislike, and sometimes mistake +for malicious software. + +If you're not trusting the program, you can check its [source code](https://github.com/Dinopony/randstalker-archipelago/) +or test it on a service like Virustotal. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The [Player Settings Page](../player-settings) on the website allows you to easily configure your personal settings +and export a config file from them. + +## How-to-play + +### Connecting to the Archipelago Server + +Once the game has been created, you need to connect to the server using the Landstalker Archipelago Client. + +To do so, run `randstalker_archipelago.exe` inside the folder you created while installing the software. + +A window will open with a few settings to enter: +- **Host**: Put the server address and port in this field (e.g. `archipelago.gg:12345`) +- **Slot name**: Put the player name you specified in your YAML config file in this field. +- **Password**: If the server has a password, put it there. + +![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_ap.png) + +Once all those fields were filled appropriately, click on the `Connect to Archipelago` button below to try connecting to +the Archipelago server. + +If this didn't work, double-check your credentials. An error message should be displayed on the console log to the +right that might help you find the cause of the issue. + +### ROM Generation + +When you connected to the Archipelago server, the client fetched all the required data from the server to be able to +build a randomized ROM. + +You should see a window with settings to fill: +- **Input ROM file**: This is the path to your original ROM file for the game. If you are using the Steam release ROM + and placed it inside the client's folder as mentioned above, you don't need to change anything. +- **Output ROM directory**: This is where the randomized ROMs will be put. No need to change this unless you want them + to be created in a very specific folder. + +![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_rom.png) + +There also a few cosmetic options you can fill before clicking the `Build ROM` button which should create your +randomized seed if everything went right. + +If it didn't, double-check your `Input ROM file` and `Output ROM path`, then retry building the ROM by clicking +the same button again. + +### Connecting to the emulator + +Now that you're connected to the Archipelago server and have a randomized ROM, all we need is to get the client +connected to the emulator. This way, the client will be able to see what's happening while you play and give you in-game +the items you have received from other players. + +You should see the following window: + +![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_emu.png) + +As written, you have to open the newly generated ROM inside either Retroarch or Bizhawk using the Genesis Plus GX core. +Be careful to select that core, because any other core (e.g. BlastEm) won't work. + +The easiest way to do so is to: +- open the emu of your choice +- if you're using Retroarch and it's your first time, download the Genesis Plus GX core through Retroarch user interface +- click the `Show ROM file in explorer` button +- drag-and-drop the shown ROM file on the emulator window +- press Start to reach file select screen (to ensure game RAM is properly set-up) + +Then, you can click on the `Connect to emulator` button below and it should work. + +If this didn't work, try the following: +- ensure you have loaded your ROM and reached the save select screen +- ensure you are using Genesis Plus GX and not another core (e.g. BlastEm will not work) +- try launching the client in Administrator Mode (right-click on `randstalker_archipelago.exe`, then + `Run as administrator`) +- if all else fails, try using one of those specific emulator versions: + - RetroArch 1.9.0 and Genesis Plus GX 1.7.4 + - Bizhawk 2.9.1 (x64) + +### Play the game + +If all indicators are green and show "Connected," you're good to go! Play the game and enjoy the wonders of isometric +perspective. + +The client is packaged with both an **automatic item tracker** and an **automatic map tracker** for your comfort. + +If you don't know all checks in the game, don't be afraid: you can click the `Where is it?` button that will show +you a screenshot of where the location actually is. + +![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_client.png) + +Have fun! \ No newline at end of file diff --git a/worlds/landstalker/docs/ls_guide_ap.png b/worlds/landstalker/docs/ls_guide_ap.png new file mode 100644 index 0000000000000000000000000000000000000000..674938ce6707d4f1384a7d1771b66c4a39e5e428 GIT binary patch literal 2283 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 literal 0 HcmV?d00001 diff --git a/worlds/landstalker/docs/ls_guide_emu.png b/worlds/landstalker/docs/ls_guide_emu.png new file mode 100644 index 0000000000000000000000000000000000000000..ff9218de12bafba18009b57efe85b9003930998a GIT binary patch literal 2598 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 literal 0 HcmV?d00001 diff --git a/worlds/landstalker/docs/ls_guide_rom.png b/worlds/landstalker/docs/ls_guide_rom.png new file mode 100644 index 0000000000000000000000000000000000000000..c57554ab43d875c7ee0110990572daa4ae1e6d6b GIT binary patch literal 3951 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 literal 0 HcmV?d00001 From 5475b04b902bf678927e1c22bf64d9a9c80b309e Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 25 Nov 2023 07:27:54 -0800 Subject: [PATCH 093/104] Pokemon Emerald: Bump apworld version number (#2504) --- worlds/pokemon_emerald/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_emerald/README.md b/worlds/pokemon_emerald/README.md index 61aee774525f..2c1e9e356046 100644 --- a/worlds/pokemon_emerald/README.md +++ b/worlds/pokemon_emerald/README.md @@ -1,6 +1,6 @@ # Pokemon Emerald -Version 1.2.0 +Version 1.2.1 This README contains general info useful for understanding the world. Pretty much all the long lists of locations, regions, and items are stored in `data/` and (mostly) loaded in by `data.py`. Access rules are in `rules.py`. Check From 6718fa4e3b7fd1978921604ac5b6aac35051cb20 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 25 Nov 2023 07:38:12 -0800 Subject: [PATCH 094/104] Installer: Add `_bizhawk.apworld` to installer deleted files (#2477) --- inno_setup.iss | 1 + 1 file changed, 1 insertion(+) diff --git a/inno_setup.iss b/inno_setup.iss index 10d699ad7065..be5de320a1c6 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -80,6 +80,7 @@ Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringC Type: dirifempty; Name: "{app}" [InstallDelete] +Type: files; Name: "{app}\lib\worlds\_bizhawk.apworld" Type: files; Name: "{app}\ArchipelagoLttPClient.exe" Type: files; Name: "{app}\ArchipelagoPokemonClient.exe" Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua" From fe6a70a1de997d094bd70153b9e1a6887d301807 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 25 Nov 2023 10:48:13 -0600 Subject: [PATCH 095/104] Docs: add documentation for options comparison (#2505) --- docs/options api.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/options api.md b/docs/options api.md index 80d0737e3a7f..48a3f763fa92 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -77,7 +77,33 @@ or if I need a boolean object, such as in my slot_data I can access it as: ```python start_with_sword = bool(self.options.starting_sword.value) ``` - +All numeric options (i.e. Toggle, Choice, Range) can be compared to integers, strings that match their attributes, +strings that match the option attributes after "option_" is stripped, and the attributes themselves. +```python +# options.py +class Logic(Choice): + option_normal = 0 + option_hard = 1 + option_challenging = 2 + option_extreme = 3 + option_insane = 4 + alias_extra_hard = 2 + crazy = 4 # won't be listed as an option and only exists as an attribute on the class + +# __init__.py +from .options import Logic + +if self.options.logic: + do_things_for_all_non_normal_logic() +if self.options.logic == 1: + do_hard_things() +elif self.options.logic == "challenging": + do_challenging_things() +elif self.options.logic == Logic.option_extreme: + do_extreme_things() +elif self.options.logic == "crazy": + do_insane_things() +``` ## Generic Option Classes These options are generically available to every game automatically, but can be overridden for slightly different behavior, if desired. See `worlds/soe/Options.py` for an example. From cfe357eb7197a127f1a5dc62ccc9eaa87e6c2668 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 25 Nov 2023 15:07:02 -0600 Subject: [PATCH 096/104] The Messenger, LADX: use collect and remove as intended (#2093) Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> --- worlds/ladx/__init__.py | 31 ++++++++++++----------------- worlds/ladx/test/testShop.py | 38 ++++++++++++++++++++++++++++++++++++ worlds/messenger/__init__.py | 19 ++++++++++-------- 3 files changed, 61 insertions(+), 27 deletions(-) create mode 100644 worlds/ladx/test/testShop.py diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index eaaea5be2f67..181cc053222d 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -1,32 +1,29 @@ import binascii -import bsdiff4 import os import pkgutil -import settings -import typing import tempfile +import typing +import bsdiff4 +import settings from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial from Fill import fill_restrictive from worlds.AutoWorld import WebWorld, World - from .Common import * -from .Items import (DungeonItemData, DungeonItemType, LinksAwakeningItem, TradeItemData, - ladxr_item_to_la_item_name, links_awakening_items, - links_awakening_items_by_name, ItemName) +from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData, + ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name) from .LADXR import generator from .LADXR.itempool import ItemPool as LADXRItemPool +from .LADXR.locations.constants import CHEST_ITEMS +from .LADXR.locations.instrument import Instrument from .LADXR.logic import Logic as LAXDRLogic from .LADXR.main import get_parser from .LADXR.settings import Settings as LADXRSettings from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup -from .LADXR.locations.instrument import Instrument -from .LADXR.locations.constants import CHEST_ITEMS from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, create_regions_from_ladxr, get_locations_to_id) -from .Options import links_awakening_options, DungeonItemShuffle - +from .Options import DungeonItemShuffle, links_awakening_options from .Rom import LADXDeltaPatch DEVELOPER_MODE = False @@ -511,16 +508,12 @@ def modify_multidata(self, multidata: dict): def collect(self, state, item: Item) -> bool: change = super().collect(state, item) - if change: - rupees = self.rupees.get(item.name, 0) - state.prog_items[item.player]["RUPEES"] += rupees - + if change and item.name in self.rupees: + state.prog_items[self.player]["RUPEES"] += self.rupees[item.name] return change def remove(self, state, item: Item) -> bool: change = super().remove(state, item) - if change: - rupees = self.rupees.get(item.name, 0) - state.prog_items[item.player]["RUPEES"] -= rupees - + if change and item.name in self.rupees: + state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name] return change diff --git a/worlds/ladx/test/testShop.py b/worlds/ladx/test/testShop.py new file mode 100644 index 000000000000..91d504d521b4 --- /dev/null +++ b/worlds/ladx/test/testShop.py @@ -0,0 +1,38 @@ +from typing import Optional + +from Fill import distribute_planned +from test.general import setup_solo_multiworld +from worlds.AutoWorld import call_all +from . import LADXTestBase +from .. import LinksAwakeningWorld + + +class PlandoTest(LADXTestBase): + options = { + "plando_items": [{ + "items": { + "Progressive Sword": 2, + }, + "locations": [ + "Shop 200 Item (Mabe Village)", + "Shop 980 Item (Mabe Village)", + ], + }], + } + + def world_setup(self, seed: Optional[int] = None) -> None: + self.multiworld = setup_solo_multiworld( + LinksAwakeningWorld, + ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic") + ) + self.multiworld.plando_items[1] = self.options["plando_items"] + distribute_planned(self.multiworld) + call_all(self.multiworld, "pre_fill") + + def test_planned(self): + """Tests plandoing swords in the shop.""" + location_names = ["Shop 200 Item (Mabe Village)", "Shop 980 Item (Mabe Village)"] + locations = [self.multiworld.get_location(loc, 1) for loc in location_names] + for loc in locations: + self.assertEqual("Progressive Sword", loc.item.name) + self.assertFalse(loc.can_reach(self.multiworld.state)) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index f12687361b70..d569dd754278 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -176,11 +176,14 @@ def create_item(self, name: str) -> MessengerItem: self.total_shards += count return MessengerItem(name, self.player, item_id, override_prog, count) - def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]: - if item.advancement and "Time Shard" in item.name: - shard_count = int(item.name.strip("Time Shard ()")) - if remove: - shard_count = -shard_count - state.prog_items[self.player]["Shards"] += shard_count - - return super().collect_item(state, item, remove) + def collect(self, state: "CollectionState", item: "Item") -> bool: + change = super().collect(state, item) + if change and "Time Shard" in item.name: + state.prog_items[self.player]["Shards"] += int(item.name.strip("Time Shard ()")) + return change + + def remove(self, state: "CollectionState", item: "Item") -> bool: + change = super().remove(state, item) + if change and "Time Shard" in item.name: + state.prog_items[self.player]["Shards"] -= int(item.name.strip("Time Shard ()")) + return change From 7a4620925957b2da909d9510358d6773efab3bf3 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Sat, 25 Nov 2023 23:12:38 -0500 Subject: [PATCH 097/104] SA2B: Add AP 0.4.4 Game Chao Names (#2510) --- worlds/sa2b/AestheticData.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/sa2b/AestheticData.py b/worlds/sa2b/AestheticData.py index f3699e81e0c4..077f35fc01b0 100644 --- a/worlds/sa2b/AestheticData.py +++ b/worlds/sa2b/AestheticData.py @@ -146,6 +146,10 @@ "Rin", "Doomguy", "Guide", + "May", + "Hubert", + "Corvus", + "Nigel", ] totally_real_item_names = [ From eec35ab1c3c4b6d51d04b12c812ed6abe8e84f09 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 25 Nov 2023 20:13:08 -0800 Subject: [PATCH 098/104] Pokemon Emerald: Fix tracker flags being reset in menus (#2511) --- worlds/pokemon_emerald/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index 5420b15fbe95..d8b4b8d5878f 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -254,7 +254,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: "key": f"pokemon_emerald_events_{ctx.team}_{ctx.slot}", "default": 0, "want_reply": False, - "operations": [{"operation": "replace", "value": event_bitfield}] + "operations": [{"operation": "or", "value": event_bitfield}] }]) self.local_set_events = local_set_events @@ -269,7 +269,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: "key": f"pokemon_emerald_keys_{ctx.team}_{ctx.slot}", "default": 0, "want_reply": False, - "operations": [{"operation": "replace", "value": key_bitfield}] + "operations": [{"operation": "or", "value": key_bitfield}] }]) self.local_found_key_items = local_found_key_items except bizhawk.RequestFailedError: From 65f47be511ace6d6f34495df58ad46d02c450f9d Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Sun, 26 Nov 2023 14:13:59 +1000 Subject: [PATCH 099/104] Muse Dash: Presets and Song Updates (#2512) --- worlds/musedash/MuseDashData.txt | 8 +++++++- worlds/musedash/Presets.py | 31 +++++++++++++++++++++++++++++++ worlds/musedash/__init__.py | 2 ++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 worlds/musedash/Presets.py diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 5b3ef40e5421..54a0124474c6 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -495,4 +495,10 @@ Gullinkambi|67-1|Happy Otaku Pack Vol.18|True|4|7|10| RakiRaki Rebuilders!!!|67-2|Happy Otaku Pack Vol.18|True|5|7|10| Laniakea|67-3|Happy Otaku Pack Vol.18|False|5|8|10| OTTAMA GAZER|67-4|Happy Otaku Pack Vol.18|True|5|8|10| -Sleep Tight feat.Macoto|67-5|Happy Otaku Pack Vol.18|True|3|5|8| \ No newline at end of file +Sleep Tight feat.Macoto|67-5|Happy Otaku Pack Vol.18|True|3|5|8| +New York Back Raise|68-0|Gambler's Tricks|True|6|8|10| +slic.hertz|68-1|Gambler's Tricks|True|5|7|9| +Fuzzy-Navel|68-2|Gambler's Tricks|True|6|8|10|11 +Swing Edge|68-3|Gambler's Tricks|True|4|8|10| +Twisted Escape|68-4|Gambler's Tricks|True|5|8|10|11 +Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10| \ No newline at end of file diff --git a/worlds/musedash/Presets.py b/worlds/musedash/Presets.py new file mode 100644 index 000000000000..64591118021e --- /dev/null +++ b/worlds/musedash/Presets.py @@ -0,0 +1,31 @@ +from typing import Any, Dict + +MuseDashPresets: Dict[str, Dict[str, Any]] = { + # An option to support Short Sync games. 40 songs. + "No DLC - Short": { + "allow_just_as_planned_dlc_songs": False, + "starting_song_count": 5, + "additional_song_count": 34, + "additional_item_percentage": 80, + "music_sheet_count_percentage": 20, + "music_sheet_win_count_percentage": 90, + }, + # An option to support Short Sync games but adds variety. 40 songs. + "DLC - Short": { + "allow_just_as_planned_dlc_songs": True, + "starting_song_count": 5, + "additional_song_count": 34, + "additional_item_percentage": 80, + "music_sheet_count_percentage": 20, + "music_sheet_win_count_percentage": 90, + }, + # An option to support Longer Sync/Async games. 100 songs. + "DLC - Long": { + "allow_just_as_planned_dlc_songs": True, + "starting_song_count": 8, + "additional_song_count": 91, + "additional_item_percentage": 80, + "music_sheet_count_percentage": 20, + "music_sheet_win_count_percentage": 90, + }, +} diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index 9a0e473494ad..a68fd2853def 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -8,6 +8,7 @@ from .Items import MuseDashSongItem, MuseDashFixedItem from .Locations import MuseDashLocation from .MuseDashCollection import MuseDashCollections +from .Presets import MuseDashPresets class MuseDashWebWorld(WebWorld): @@ -33,6 +34,7 @@ class MuseDashWebWorld(WebWorld): ) tutorials = [setup_en, setup_es] + options_presets = MuseDashPresets class MuseDashWorld(World): From f54f8622bb6edd38324a0eadc42b2daa7261f415 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sun, 26 Nov 2023 11:17:59 -0500 Subject: [PATCH 100/104] Final Fantasy Mystic Quest: Implement new game (#1909) FFMQR by @wildham0 Uses an API created by wildham for Map Shuffle, Crest Shuffle and Battlefield Reward Shuffle, using a similar method of obtaining data from an external website to Super Metroid's Varia Preset option. Generates a .apmq file which the user must bring to the FFMQR website https://www.ffmqrando.net/Archipelago to patch their rom. It is not an actual patch file but contains item placement and options data for the FFMQR website to generate a patched rom with for AP. Some of the AP options may seem unusual, using Choice instead of Range where it may seem more appropriate, but these are options that are passed to FFMQR and I can only be as flexible as it is. @wildham0 deserves the bulk of the credit for not only creating FFMQR in the first place but all the ASM work on the rom needed to make this possible, work on FFMQR to allow patching with the .apmq files, and creating the API that meant I did not have to recreate his map shuffle from scratch. --- README.md | 1 + WebHostLib/downloads.py | 2 + WebHostLib/templates/macros.html | 3 + docs/CODEOWNERS | 3 + worlds/ffmq/Client.py | 119 + worlds/ffmq/Items.py | 297 ++ worlds/ffmq/LICENSE | 22 + worlds/ffmq/Options.py | 258 ++ worlds/ffmq/Output.py | 113 + worlds/ffmq/Regions.py | 251 + worlds/ffmq/__init__.py | 217 + worlds/ffmq/data/entrances.yaml | 2425 ++++++++++ worlds/ffmq/data/rooms.yaml | 4026 +++++++++++++++++ worlds/ffmq/data/settings.yaml | 140 + .../docs/en_Final Fantasy Mystic Quest.md | 33 + worlds/ffmq/docs/setup_en.md | 162 + 16 files changed, 8072 insertions(+) create mode 100644 worlds/ffmq/Client.py create mode 100644 worlds/ffmq/Items.py create mode 100644 worlds/ffmq/LICENSE create mode 100644 worlds/ffmq/Options.py create mode 100644 worlds/ffmq/Output.py create mode 100644 worlds/ffmq/Regions.py create mode 100644 worlds/ffmq/__init__.py create mode 100644 worlds/ffmq/data/entrances.yaml create mode 100644 worlds/ffmq/data/rooms.yaml create mode 100644 worlds/ffmq/data/settings.yaml create mode 100644 worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md create mode 100644 worlds/ffmq/docs/setup_en.md diff --git a/README.md b/README.md index 3508dd16095c..a1e03293d587 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Currently, the following games are supported: * Shivers * Heretic * Landstalker: The Treasures of King Nole +* Final Fantasy Mystic Quest For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index 5cf503be1b2b..a09ca7017181 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -90,6 +90,8 @@ def download_slot_file(room_id, player_id: int): fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json" elif slot_data.game == "Kingdom Hearts 2": fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.zip" + elif slot_data.game == "Final Fantasy Mystic Quest": + fname = f"AP+{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmq" else: return "Game download not supported." return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname) diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 746399da74a6..0722ee317466 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -50,6 +50,9 @@ {% elif patch.game == "Dark Souls III" %} Download JSON File... + {% elif patch.game == "Final Fantasy Mystic Quest" %} + + Download APMQ File... {% else %} No file to download for this game. {% endif %} diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 0764fa927464..e221371b2417 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -55,6 +55,9 @@ # Final Fantasy /worlds/ff1/ @jtoyoda +# Final Fantasy Mystic Quest +/worlds/ffmq/ @Alchav @wildham0 + # Heretic /worlds/heretic/ @Daivuk diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py new file mode 100644 index 000000000000..c53f275017af --- /dev/null +++ b/worlds/ffmq/Client.py @@ -0,0 +1,119 @@ + +from NetUtils import ClientStatus, color +from worlds.AutoSNIClient import SNIClient +from .Regions import offset +import logging + +snes_logger = logging.getLogger("SNES") + +ROM_NAME = (0x7FC0, 0x7FD4 + 1 - 0x7FC0) + +READ_DATA_START = 0xF50EA8 +READ_DATA_END = 0xF50FE7 + 1 + +GAME_FLAGS = (0xF50EA8, 64) +COMPLETED_GAME = (0xF50F22, 1) +BATTLEFIELD_DATA = (0xF50FD4, 20) + +RECEIVED_DATA = (0xE01FF0, 3) + +ITEM_CODE_START = 0x420000 + +IN_GAME_FLAG = (4 * 8) + 2 + +NPC_CHECKS = { + 4325676: ((6 * 8) + 4, False), # Old Man Level Forest + 4325677: ((3 * 8) + 6, True), # Kaeli Level Forest + 4325678: ((25 * 8) + 1, True), # Tristam + 4325680: ((26 * 8) + 0, True), # Aquaria Vendor Girl + 4325681: ((29 * 8) + 2, True), # Phoebe Wintry Cave + 4325682: ((25 * 8) + 6, False), # Mysterious Man (Life Temple) + 4325683: ((29 * 8) + 3, True), # Reuben Mine + 4325684: ((29 * 8) + 7, True), # Spencer + 4325685: ((29 * 8) + 6, False), # Venus Chest + 4325686: ((29 * 8) + 1, True), # Fireburg Tristam + 4325687: ((26 * 8) + 1, True), # Fireburg Vendor Girl + 4325688: ((14 * 8) + 4, True), # MegaGrenade Dude + 4325689: ((29 * 8) + 5, False), # Tristam's Chest + 4325690: ((29 * 8) + 4, True), # Arion + 4325691: ((29 * 8) + 0, True), # Windia Kaeli + 4325692: ((26 * 8) + 2, True), # Windia Vendor Girl + +} + + +def get_flag(data, flag): + byte = int(flag / 8) + bit = int(0x80 / (2 ** (flag % 8))) + return (data[byte] & bit) > 0 + + +class FFMQClient(SNIClient): + game = "Final Fantasy Mystic Quest" + + async def validate_rom(self, ctx): + from SNIClient import snes_read + rom_name = await snes_read(ctx, *ROM_NAME) + if rom_name is None: + return False + if rom_name[:2] != b"MQ": + return False + + ctx.rom = rom_name + ctx.game = self.game + ctx.items_handling = 0b001 + return True + + async def game_watcher(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + check_1 = await snes_read(ctx, 0xF53749, 1) + received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) + data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) + check_2 = await snes_read(ctx, 0xF53749, 1) + if check_1 == b'\x00' or check_2 == b'\x00': + return + + def get_range(data_range): + return data[data_range[0] - READ_DATA_START:data_range[0] + data_range[1] - READ_DATA_START] + completed_game = get_range(COMPLETED_GAME) + battlefield_data = get_range(BATTLEFIELD_DATA) + game_flags = get_range(GAME_FLAGS) + + if game_flags is None: + return + if not get_flag(game_flags, IN_GAME_FLAG): + return + + if not ctx.finished_game: + if completed_game[0] & 0x80 and game_flags[30] & 0x18: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + + old_locations_checked = ctx.locations_checked.copy() + + for container in range(256): + if get_flag(game_flags, (0x20 * 8) + container): + ctx.locations_checked.add(offset["Chest"] + container) + + for location, data in NPC_CHECKS.items(): + if get_flag(game_flags, data[0]) is data[1]: + ctx.locations_checked.add(location) + + for battlefield in range(20): + if battlefield_data[battlefield] == 0: + ctx.locations_checked.add(offset["BattlefieldItem"] + battlefield + 1) + + if old_locations_checked != ctx.locations_checked: + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": ctx.locations_checked}]) + + if received[0] == 0: + received_index = int.from_bytes(received[1:], "big") + if received_index < len(ctx.items_received): + item = ctx.items_received[received_index] + received_index += 1 + code = (item.item - ITEM_CODE_START) + 1 + if code > 256: + code -= 256 + snes_buffered_write(ctx, RECEIVED_DATA[0], bytes([code, *received_index.to_bytes(2, "big")])) + await snes_flush_writes(ctx) diff --git a/worlds/ffmq/Items.py b/worlds/ffmq/Items.py new file mode 100644 index 000000000000..7660bd5d52f3 --- /dev/null +++ b/worlds/ffmq/Items.py @@ -0,0 +1,297 @@ +from BaseClasses import ItemClassification, Item + +fillers = {"Cure Potion": 61, "Heal Potion": 52, "Refresher": 17, "Seed": 2, "Bomb Refill": 19, + "Projectile Refill": 50} + + +class ItemData: + def __init__(self, item_id, classification, groups=(), data_name=None): + self.groups = groups + self.classification = classification + self.id = None + if item_id is not None: + self.id = item_id + 0x420000 + self.data_name = data_name + + +item_table = { + "Elixir": ItemData(0, ItemClassification.progression, ["Key Items"]), + "Tree Wither": ItemData(1, ItemClassification.progression, ["Key Items"]), + "Wakewater": ItemData(2, ItemClassification.progression, ["Key Items"]), + "Venus Key": ItemData(3, ItemClassification.progression, ["Key Items"]), + "Multi Key": ItemData(4, ItemClassification.progression, ["Key Items"]), + "Mask": ItemData(5, ItemClassification.progression, ["Key Items"]), + "Magic Mirror": ItemData(6, ItemClassification.progression, ["Key Items"]), + "Thunder Rock": ItemData(7, ItemClassification.progression, ["Key Items"]), + "Captain's Cap": ItemData(8, ItemClassification.progression_skip_balancing, ["Key Items"]), + "Libra Crest": ItemData(9, ItemClassification.progression, ["Key Items"]), + "Gemini Crest": ItemData(10, ItemClassification.progression, ["Key Items"]), + "Mobius Crest": ItemData(11, ItemClassification.progression, ["Key Items"]), + "Sand Coin": ItemData(12, ItemClassification.progression, ["Key Items", "Coins"]), + "River Coin": ItemData(13, ItemClassification.progression, ["Key Items", "Coins"]), + "Sun Coin": ItemData(14, ItemClassification.progression, ["Key Items", "Coins"]), + "Sky Coin": ItemData(15, ItemClassification.progression_skip_balancing, ["Key Items", "Coins"]), + "Sky Fragment": ItemData(15 + 256, ItemClassification.progression_skip_balancing, ["Key Items"]), + "Cure Potion": ItemData(16, ItemClassification.filler, ["Consumables"]), + "Heal Potion": ItemData(17, ItemClassification.filler, ["Consumables"]), + "Seed": ItemData(18, ItemClassification.filler, ["Consumables"]), + "Refresher": ItemData(19, ItemClassification.filler, ["Consumables"]), + "Exit Book": ItemData(20, ItemClassification.useful, ["Spells"]), + "Cure Book": ItemData(21, ItemClassification.useful, ["Spells"]), + "Heal Book": ItemData(22, ItemClassification.useful, ["Spells"]), + "Life Book": ItemData(23, ItemClassification.useful, ["Spells"]), + "Quake Book": ItemData(24, ItemClassification.useful, ["Spells"]), + "Blizzard Book": ItemData(25, ItemClassification.useful, ["Spells"]), + "Fire Book": ItemData(26, ItemClassification.useful, ["Spells"]), + "Aero Book": ItemData(27, ItemClassification.useful, ["Spells"]), + "Thunder Seal": ItemData(28, ItemClassification.useful, ["Spells"]), + "White Seal": ItemData(29, ItemClassification.useful, ["Spells"]), + "Meteor Seal": ItemData(30, ItemClassification.useful, ["Spells"]), + "Flare Seal": ItemData(31, ItemClassification.useful, ["Spells"]), + "Progressive Sword": ItemData(32 + 256, ItemClassification.progression, ["Weapons", "Swords"]), + "Steel Sword": ItemData(32, ItemClassification.progression, ["Weapons", "Swords"]), + "Knight Sword": ItemData(33, ItemClassification.progression_skip_balancing, ["Weapons", "Swords"]), + "Excalibur": ItemData(34, ItemClassification.progression_skip_balancing, ["Weapons", "Swords"]), + "Progressive Axe": ItemData(35 + 256, ItemClassification.progression, ["Weapons", "Axes"]), + "Axe": ItemData(35, ItemClassification.progression, ["Weapons", "Axes"]), + "Battle Axe": ItemData(36, ItemClassification.progression_skip_balancing, ["Weapons", "Axes"]), + "Giant's Axe": ItemData(37, ItemClassification.progression_skip_balancing, ["Weapons", "Axes"]), + "Progressive Claw": ItemData(38 + 256, ItemClassification.progression, ["Weapons", "Axes"]), + "Cat Claw": ItemData(38, ItemClassification.progression, ["Weapons", "Claws"]), + "Charm Claw": ItemData(39, ItemClassification.progression_skip_balancing, ["Weapons", "Claws"]), + "Dragon Claw": ItemData(40, ItemClassification.progression, ["Weapons", "Claws"]), + "Progressive Bomb": ItemData(41 + 256, ItemClassification.progression, ["Weapons", "Bombs"]), + "Bomb": ItemData(41, ItemClassification.progression, ["Weapons", "Bombs"]), + "Jumbo Bomb": ItemData(42, ItemClassification.progression_skip_balancing, ["Weapons", "Bombs"]), + "Mega Grenade": ItemData(43, ItemClassification.progression, ["Weapons", "Bombs"]), + # Ally-only equipment does nothing when received, no reason to put them in the datapackage + #"Morning Star": ItemData(44, ItemClassification.progression, ["Weapons"]), + #"Bow Of Grace": ItemData(45, ItemClassification.progression, ["Weapons"]), + #"Ninja Star": ItemData(46, ItemClassification.progression, ["Weapons"]), + + "Progressive Helm": ItemData(47 + 256, ItemClassification.useful, ["Helms"]), + "Steel Helm": ItemData(47, ItemClassification.useful, ["Helms"]), + "Moon Helm": ItemData(48, ItemClassification.useful, ["Helms"]), + "Apollo Helm": ItemData(49, ItemClassification.useful, ["Helms"]), + "Progressive Armor": ItemData(50 + 256, ItemClassification.useful, ["Armors"]), + "Steel Armor": ItemData(50, ItemClassification.useful, ["Armors"]), + "Noble Armor": ItemData(51, ItemClassification.useful, ["Armors"]), + "Gaia's Armor": ItemData(52, ItemClassification.useful, ["Armors"]), + #"Replica Armor": ItemData(53, ItemClassification.progression, ["Armors"]), + #"Mystic Robes": ItemData(54, ItemClassification.progression, ["Armors"]), + #"Flame Armor": ItemData(55, ItemClassification.progression, ["Armors"]), + #"Black Robe": ItemData(56, ItemClassification.progression, ["Armors"]), + "Progressive Shield": ItemData(57 + 256, ItemClassification.useful, ["Shields"]), + "Steel Shield": ItemData(57, ItemClassification.useful, ["Shields"]), + "Venus Shield": ItemData(58, ItemClassification.useful, ["Shields"]), + "Aegis Shield": ItemData(59, ItemClassification.useful, ["Shields"]), + #"Ether Shield": ItemData(60, ItemClassification.progression, ["Shields"]), + "Progressive Accessory": ItemData(61 + 256, ItemClassification.useful, ["Accessories"]), + "Charm": ItemData(61, ItemClassification.useful, ["Accessories"]), + "Magic Ring": ItemData(62, ItemClassification.useful, ["Accessories"]), + "Cupid Locket": ItemData(63, ItemClassification.useful, ["Accessories"]), + + # these are understood by FFMQR and I could place these if I want, but it's easier to just let FFMQR + # place them. I want an option to make shuffle battlefield rewards NOT color-code the battlefields, + # and then I would make the non-item reward battlefields into AP checks and these would be put into those as + # the item for AP. But there is no such option right now. + # "54 XP": ItemData(96, ItemClassification.filler, data_name="Xp54"), + # "99 XP": ItemData(97, ItemClassification.filler, data_name="Xp99"), + # "540 XP": ItemData(98, ItemClassification.filler, data_name="Xp540"), + # "744 XP": ItemData(99, ItemClassification.filler, data_name="Xp744"), + # "816 XP": ItemData(100, ItemClassification.filler, data_name="Xp816"), + # "1068 XP": ItemData(101, ItemClassification.filler, data_name="Xp1068"), + # "1200 XP": ItemData(102, ItemClassification.filler, data_name="Xp1200"), + # "2700 XP": ItemData(103, ItemClassification.filler, data_name="Xp2700"), + # "2808 XP": ItemData(104, ItemClassification.filler, data_name="Xp2808"), + # "150 Gp": ItemData(105, ItemClassification.filler, data_name="Gp150"), + # "300 Gp": ItemData(106, ItemClassification.filler, data_name="Gp300"), + # "600 Gp": ItemData(107, ItemClassification.filler, data_name="Gp600"), + # "900 Gp": ItemData(108, ItemClassification.filler, data_name="Gp900"), + # "1200 Gp": ItemData(109, ItemClassification.filler, data_name="Gp1200"), + + + "Bomb Refill": ItemData(221, ItemClassification.filler, ["Refills"]), + "Projectile Refill": ItemData(222, ItemClassification.filler, ["Refills"]), + #"None": ItemData(255, ItemClassification.progression, []), + + "Kaeli 1": ItemData(None, ItemClassification.progression), + "Kaeli 2": ItemData(None, ItemClassification.progression), + "Tristam": ItemData(None, ItemClassification.progression), + "Phoebe 1": ItemData(None, ItemClassification.progression), + "Reuben 1": ItemData(None, ItemClassification.progression), + "Reuben Dad Saved": ItemData(None, ItemClassification.progression), + "Otto": ItemData(None, ItemClassification.progression), + "Captain Mac": ItemData(None, ItemClassification.progression), + "Ship Steering Wheel": ItemData(None, ItemClassification.progression), + "Minotaur": ItemData(None, ItemClassification.progression), + "Flamerus Rex": ItemData(None, ItemClassification.progression), + "Phanquid": ItemData(None, ItemClassification.progression), + "Freezer Crab": ItemData(None, ItemClassification.progression), + "Ice Golem": ItemData(None, ItemClassification.progression), + "Jinn": ItemData(None, ItemClassification.progression), + "Medusa": ItemData(None, ItemClassification.progression), + "Dualhead Hydra": ItemData(None, ItemClassification.progression), + "Gidrah": ItemData(None, ItemClassification.progression), + "Dullahan": ItemData(None, ItemClassification.progression), + "Pazuzu": ItemData(None, ItemClassification.progression), + "Aquaria Plaza": ItemData(None, ItemClassification.progression), + "Summer Aquaria": ItemData(None, ItemClassification.progression), + "Reuben Mine": ItemData(None, ItemClassification.progression), + "Alive Forest": ItemData(None, ItemClassification.progression), + "Rainbow Bridge": ItemData(None, ItemClassification.progression), + "Collapse Spencer's Cave": ItemData(None, ItemClassification.progression), + "Ship Liberated": ItemData(None, ItemClassification.progression), + "Ship Loaned": ItemData(None, ItemClassification.progression), + "Ship Dock Access": ItemData(None, ItemClassification.progression), + "Stone Golem": ItemData(None, ItemClassification.progression), + "Twinhead Wyvern": ItemData(None, ItemClassification.progression), + "Zuh": ItemData(None, ItemClassification.progression), + + "Libra Temple Crest Tile": ItemData(None, ItemClassification.progression), + "Life Temple Crest Tile": ItemData(None, ItemClassification.progression), + "Aquaria Vendor Crest Tile": ItemData(None, ItemClassification.progression), + "Fireburg Vendor Crest Tile": ItemData(None, ItemClassification.progression), + "Fireburg Grenademan Crest Tile": ItemData(None, ItemClassification.progression), + "Sealed Temple Crest Tile": ItemData(None, ItemClassification.progression), + "Wintry Temple Crest Tile": ItemData(None, ItemClassification.progression), + "Kaidge Temple Crest Tile": ItemData(None, ItemClassification.progression), + "Light Temple Crest Tile": ItemData(None, ItemClassification.progression), + "Windia Kids Crest Tile": ItemData(None, ItemClassification.progression), + "Windia Dock Crest Tile": ItemData(None, ItemClassification.progression), + "Ship Dock Crest Tile": ItemData(None, ItemClassification.progression), + "Alive Forest Libra Crest Tile": ItemData(None, ItemClassification.progression), + "Alive Forest Gemini Crest Tile": ItemData(None, ItemClassification.progression), + "Alive Forest Mobius Crest Tile": ItemData(None, ItemClassification.progression), + "Wood House Libra Crest Tile": ItemData(None, ItemClassification.progression), + "Wood House Gemini Crest Tile": ItemData(None, ItemClassification.progression), + "Wood House Mobius Crest Tile": ItemData(None, ItemClassification.progression), + "Barrel Pushed": ItemData(None, ItemClassification.progression), + "Long Spine Bombed": ItemData(None, ItemClassification.progression), + "Short Spine Bombed": ItemData(None, ItemClassification.progression), + "Skull 1 Bombed": ItemData(None, ItemClassification.progression), + "Skull 2 Bombed": ItemData(None, ItemClassification.progression), + "Ice Pyramid 1F Statue": ItemData(None, ItemClassification.progression), + "Ice Pyramid 3F Statue": ItemData(None, ItemClassification.progression), + "Ice Pyramid 4F Statue": ItemData(None, ItemClassification.progression), + "Ice Pyramid 5F Statue": ItemData(None, ItemClassification.progression), + "Spencer Cave Libra Block Bombed": ItemData(None, ItemClassification.progression), + "Lava Dome Plate": ItemData(None, ItemClassification.progression), + "Pazuzu 2F Lock": ItemData(None, ItemClassification.progression), + "Pazuzu 4F Lock": ItemData(None, ItemClassification.progression), + "Pazuzu 6F Lock": ItemData(None, ItemClassification.progression), + "Pazuzu 1F": ItemData(None, ItemClassification.progression), + "Pazuzu 2F": ItemData(None, ItemClassification.progression), + "Pazuzu 3F": ItemData(None, ItemClassification.progression), + "Pazuzu 4F": ItemData(None, ItemClassification.progression), + "Pazuzu 5F": ItemData(None, ItemClassification.progression), + "Pazuzu 6F": ItemData(None, ItemClassification.progression), + "Dark King": ItemData(None, ItemClassification.progression), + #"Barred": ItemData(None, ItemClassification.progression), + +} + +prog_map = { + "Swords": "Progressive Sword", + "Axes": "Progressive Axe", + "Claws": "Progressive Claw", + "Bombs": "Progressive Bomb", + "Shields": "Progressive Shield", + "Armors": "Progressive Armor", + "Helms": "Progressive Helm", + "Accessories": "Progressive Accessory", +} + + +def yaml_item(text): + if text == "CaptainCap": + return "Captain's Cap" + elif text == "WakeWater": + return "Wakewater" + return "".join( + [(" " + c if (c.isupper() or c.isnumeric()) and not (text[i - 1].isnumeric() and c == "F") else c) for + i, c in enumerate(text)]).strip() + + +item_groups = {} +for item, data in item_table.items(): + for group in data.groups: + item_groups[group] = item_groups.get(group, []) + [item] + + +def create_items(self) -> None: + items = [] + starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ") + if self.multiworld.progressive_gear[self.player]: + for item_group in prog_map: + if starting_weapon in self.item_name_groups[item_group]: + starting_weapon = prog_map[item_group] + break + self.multiworld.push_precollected(self.create_item(starting_weapon)) + self.multiworld.push_precollected(self.create_item("Steel Armor")) + if self.multiworld.sky_coin_mode[self.player] == "start_with": + self.multiworld.push_precollected(self.create_item("Sky Coin")) + + precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]} + + def add_item(item_name): + if item_name in ["Steel Armor", "Sky Fragment"] or "Progressive" in item_name: + return + if item_name.lower().replace(" ", "_") == self.multiworld.starting_weapon[self.player].current_key: + return + if self.multiworld.progressive_gear[self.player]: + for item_group in prog_map: + if item_name in self.item_name_groups[item_group]: + item_name = prog_map[item_group] + break + if item_name == "Sky Coin": + if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + for _ in range(40): + items.append(self.create_item("Sky Fragment")) + return + elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals": + items.append(self.create_filler()) + return + if item_name in precollected_item_names: + items.append(self.create_filler()) + return + i = self.create_item(item_name) + if self.multiworld.logic[self.player] != "friendly" and item_name in ("Magic Mirror", "Mask"): + i.classification = ItemClassification.useful + if (self.multiworld.logic[self.player] == "expert" and self.multiworld.map_shuffle[self.player] == "none" and + item_name == "Exit Book"): + i.classification = ItemClassification.progression + items.append(i) + + for item_group in ("Key Items", "Spells", "Armors", "Helms", "Shields", "Accessories", "Weapons"): + for item in self.item_name_groups[item_group]: + add_item(item) + + if self.multiworld.brown_boxes[self.player] == "include": + filler_items = [] + for item, count in fillers.items(): + filler_items += [self.create_item(item) for _ in range(count)] + if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + self.multiworld.random.shuffle(filler_items) + filler_items = filler_items[39:] + items += filler_items + + self.multiworld.itempool += items + + if len(self.multiworld.player_ids) > 1: + early_choices = ["Sand Coin", "River Coin"] + early_item = self.multiworld.random.choice(early_choices) + self.multiworld.early_items[self.player][early_item] = 1 + + +class FFMQItem(Item): + game = "Final Fantasy Mystic Quest" + type = None + + def __init__(self, name, player: int = None): + item_data = item_table[name] + super(FFMQItem, self).__init__( + name, + item_data.classification, + item_data.id, player + ) \ No newline at end of file diff --git a/worlds/ffmq/LICENSE b/worlds/ffmq/LICENSE new file mode 100644 index 000000000000..46ad1c007466 --- /dev/null +++ b/worlds/ffmq/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2023 Alex "Alchav" Avery +Copyright (c) 2023 wildham + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py new file mode 100644 index 000000000000..2746bb197743 --- /dev/null +++ b/worlds/ffmq/Options.py @@ -0,0 +1,258 @@ +from Options import Choice, FreeText, Toggle + + +class Logic(Choice): + """Placement logic sets the rules that will be applied when placing items. Friendly: Required Items to clear a + dungeon will never be placed in that dungeon to avoid the need to revisit it. Also, the Magic Mirror and the Mask + will always be available before Ice Pyramid and Volcano, respectively. Note: If Dungeons are shuffled, Friendly + logic will only ensure the availability of the Mirror and the Mask. Standard: Items are randomly placed and logic + merely verifies that they're all accessible. As for Region access, only the Coins are considered. Expert: Same as + Standard, but Items Placement logic also includes other routes than Coins: the Crests Teleporters, the + Fireburg-Aquaria Lava bridge and the Sealed Temple Exit trick.""" + option_friendly = 0 + option_standard = 1 + option_expert = 2 + default = 1 + display_name = "Logic" + + +class BrownBoxes(Choice): + """Include the 201 brown box locations from the original game. Brown Boxes are all the boxes that contained a + consumable in the original game. If shuffle is chosen, the consumables contained will be shuffled but the brown + boxes will not be Archipelago location checks.""" + option_exclude = 0 + option_include = 1 + option_shuffle = 2 + default = 1 + display_name = "Brown Boxes" + + +class SkyCoinMode(Choice): + """Configure how the Sky Coin is acquired. With standard, the Sky Coin will be placed randomly. With Start With, the + Sky Coin will be in your inventory at the start of the game. With Save The Crystals, the Sky Coin will be acquired + once you save all 4 crystals. With Shattered Sky Coin, the Sky Coin is split in 40 fragments; you can enter Doom + Castle once the required amount is found. Shattered Sky Coin will force brown box locations to be included.""" + option_standard = 0 + option_start_with = 1 + option_save_the_crystals = 2 + option_shattered_sky_coin = 3 + default = 0 + display_name = "Sky Coin Mode" + + +class ShatteredSkyCoinQuantity(Choice): + """Configure the number of the 40 Sky Coin Fragments required to enter the Doom Castle. Only has an effect if + Sky Coin Mode is set to shattered. Low: 16. Mid: 24. High: 32. Random Narrow: random between 16 and 32. + Random Wide: random between 10 and 38.""" + option_low_16 = 0 + option_mid_24 = 1 + option_high_32 = 2 + option_random_narrow = 3 + option_random_wide = 4 + default = 1 + display_name = "Shattered Sky Coin" + + +class StartingWeapon(Choice): + """Choose your starting weapon.""" + display_name = "Starting Weapon" + option_steel_sword = 0 + option_axe = 1 + option_cat_claw = 2 + option_bomb = 3 + default = "random" + + +class ProgressiveGear(Toggle): + """Pieces of gear are always acquired from weakest to strongest in a set.""" + display_name = "Progressive Gear" + + +class EnemiesDensity(Choice): + """Set how many of the original enemies are on each map.""" + display_name = "Enemies Density" + option_all = 0 + option_three_quarter = 1 + option_half = 2 + option_quarter = 3 + option_none = 4 + + +class EnemyScaling(Choice): + """Superclass for enemy scaling options.""" + option_quarter = 0 + option_half = 1 + option_three_quarter = 2 + option_normal = 3 + option_one_and_quarter = 4 + option_one_and_half = 5 + option_double = 6 + option_double_and_half = 7 + option_triple = 8 + + +class EnemiesScalingLower(EnemyScaling): + """Randomly adjust enemies stats by the selected range percentage. Include mini-bosses' weaker clones.""" + display_name = "Enemies Scaling Lower" + default = 0 + + +class EnemiesScalingUpper(EnemyScaling): + """Randomly adjust enemies stats by the selected range percentage. Include mini-bosses' weaker clones.""" + display_name = "Enemies Scaling Upper" + default = 4 + + +class BossesScalingLower(EnemyScaling): + """Randomly adjust bosses stats by the selected range percentage. Include Mini-Bosses, Bosses, Bosses' refights and + the Dark King.""" + display_name = "Bosses Scaling Lower" + default = 0 + + +class BossesScalingUpper(EnemyScaling): + """Randomly adjust bosses stats by the selected range percentage. Include Mini-Bosses, Bosses, Bosses' refights and + the Dark King.""" + display_name = "Bosses Scaling Upper" + default = 4 + + +class EnemizerAttacks(Choice): + """Shuffles enemy attacks. Standard: No shuffle. Safe: Randomize every attack but leave out self-destruct and Dark + King attacks. Chaos: Randomize and include self-destruct and Dark King attacks. Self Destruct: Every enemy + self-destructs. Simple Shuffle: Instead of randomizing, shuffle one monster's attacks to another. Dark King is left + vanilla.""" + display_name = "Enemizer Attacks" + option_normal = 0 + option_safe = 1 + option_chaos = 2 + option_self_destruct = 3 + option_simple_shuffle = 4 + default = 0 + + +class ShuffleEnemiesPositions(Toggle): + """Instead of their original position in a given map, enemies are randomly placed.""" + display_name = "Shuffle Enemies' Positions" + default = 1 + + +class ProgressiveFormations(Choice): + """Enemies' formations are selected by regions, with the weakest formations always selected in Foresta and the + strongest in Windia. Disabled: Standard formations are used. Regions Strict: Formations will come exclusively + from the current region, whatever the map is. Regions Keep Type: Formations will keep the original formation type + and match with the nearest power level.""" + display_name = "Progressive Formations" + option_disabled = 0 + option_regions_strict = 1 + option_regions_keep_type = 2 + + +class DoomCastle(Choice): + """Configure how you reach the Dark King. With Standard, you need to defeat all four bosses and their floors to + reach the Dark King. With Boss Rush, only the bosses are blocking your way in the corridor to the Dark King's room. + With Dark King Only, the way to the Dark King is free of any obstacle.""" + display_name = "Doom Castle" + option_standard = 0 + option_boss_rush = 1 + option_dark_king_only = 2 + + +class DoomCastleShortcut(Toggle): + """Create a shortcut granting access from the start to Doom Castle at Focus Tower's entrance. + Also modify the Desert floor, so it can be navigated without the Mega Grenades and the Dragon Claw.""" + display_name = "Doom Castle Shortcut" + + +class TweakFrustratingDungeons(Toggle): + """Make some small changes to a few of the most annoying dungeons. Ice Pyramid: Add 3 shortcuts on the 1st floor. + Giant Tree: Add shortcuts on the 1st and 4th floors and curtail mushrooms population. + Pazuzu's Tower: Staircases are devoid of enemies (regardless of Enemies Density settings).""" + display_name = "Tweak Frustrating Dungeons" + + +class MapShuffle(Choice): + """None: No shuffle. Overworld: Only shuffle the Overworld locations. Dungeons: Only shuffle the dungeons' floors + amongst themselves. Temples and Towns aren't included. Overworld And Dungeons: Shuffle the Overworld and dungeons + at the same time. Everything: Shuffle the Overworld, dungeons, temples and towns all amongst each others. + When dungeons are shuffled, defeating Pazuzu won't teleport you to the 7th floor, you have to get there normally to + save the Crystal and get Pazuzu's Chest.""" + display_name = "Map Shuffle" + option_none = 0 + option_overworld = 1 + option_dungeons = 2 + option_overworld_and_dungeons = 3 + option_everything = 4 + default = 0 + + +class CrestShuffle(Toggle): + """Shuffle the Crest tiles amongst themselves.""" + display_name = "Crest Shuffle" + + +class MapShuffleSeed(FreeText): + """If this is a number, it will be used as a set seed number for Map, Crest, and Battlefield Reward shuffles. + If this is "random" the seed will be chosen randomly. If it is any other text, it will be used as a seed group name. + All players using the same seed group name will get the same shuffle results, as long as their Map Shuffle, + Crest Shuffle, and Shuffle Battlefield Rewards settings are the same.""" + display_name = "Map Shuffle Seed" + default = "random" + + +class LevelingCurve(Choice): + """Adjust the level gain rate.""" + display_name = "Leveling Curve" + option_half = 0 + option_normal = 1 + option_one_and_half = 2 + option_double = 3 + option_double_and_half = 4 + option_triple = 5 + option_quadruple = 6 + default = 4 + + +class ShuffleBattlefieldRewards(Toggle): + """Shuffle the type of reward (Item, XP, GP) given by battlefields and color code them by reward type. + Blue: Give an item. Grey: Give XP. Green: Give GP.""" + display_name = "Shuffle Battlefield Rewards" + + +class BattlefieldsBattlesQuantities(Choice): + """Adjust the number of battles that need to be fought to get a battlefield's reward.""" + display_name = "Battlefields Battles Quantity" + option_ten = 0 + option_seven = 1 + option_five = 2 + option_three = 3 + option_one = 4 + option_random_one_through_five = 5 + option_random_one_through_ten = 6 + + +option_definitions = { + "logic": Logic, + "brown_boxes": BrownBoxes, + "sky_coin_mode": SkyCoinMode, + "shattered_sky_coin_quantity": ShatteredSkyCoinQuantity, + "starting_weapon": StartingWeapon, + "progressive_gear": ProgressiveGear, + "enemies_density": EnemiesDensity, + "enemies_scaling_lower": EnemiesScalingLower, + "enemies_scaling_upper": EnemiesScalingUpper, + "bosses_scaling_lower": BossesScalingLower, + "bosses_scaling_upper": BossesScalingUpper, + "enemizer_attacks": EnemizerAttacks, + "shuffle_enemies_position": ShuffleEnemiesPositions, + "progressive_formations": ProgressiveFormations, + "doom_castle_mode": DoomCastle, + "doom_castle_shortcut": DoomCastleShortcut, + "tweak_frustrating_dungeons": TweakFrustratingDungeons, + "map_shuffle": MapShuffle, + "crest_shuffle": CrestShuffle, + "shuffle_battlefield_rewards": ShuffleBattlefieldRewards, + "map_shuffle_seed": MapShuffleSeed, + "leveling_curve": LevelingCurve, + "battlefields_battles_quantities": BattlefieldsBattlesQuantities, +} diff --git a/worlds/ffmq/Output.py b/worlds/ffmq/Output.py new file mode 100644 index 000000000000..c4c4605c8512 --- /dev/null +++ b/worlds/ffmq/Output.py @@ -0,0 +1,113 @@ +import yaml +import os +import zipfile +from copy import deepcopy +from .Regions import object_id_table +from Main import __version__ +from worlds.Files import APContainer +import pkgutil + +settings_template = yaml.load(pkgutil.get_data(__name__, "data/settings.yaml"), yaml.Loader) + + +def generate_output(self, output_directory): + def output_item_name(item): + if item.player == self.player: + if item.code > 0x420000 + 256: + item_name = self.item_id_to_name[item.code - 256] + else: + item_name = item.name + item_name = "".join(item_name.split("'")) + item_name = "".join(item_name.split(" ")) + else: + if item.advancement or item.useful or (item.trap and + self.multiworld.per_slot_randoms[self.player].randint(0, 1)): + item_name = "APItem" + else: + item_name = "APItemFiller" + return item_name + + item_placement = [] + for location in self.multiworld.get_locations(self.player): + if location.type != "Trigger": + item_placement.append({"object_id": object_id_table[location.name], "type": location.type, "content": + output_item_name(location.item), "player": self.multiworld.player_name[location.item.player], + "item_name": location.item.name}) + + def cc(option): + return option.current_key.title().replace("_", "").replace("OverworldAndDungeons", "OverworldDungeons") + + def tf(option): + return True if option else False + + options = deepcopy(settings_template) + options["name"] = self.multiworld.player_name[self.player] + + option_writes = { + "enemies_density": cc(self.multiworld.enemies_density[self.player]), + "chests_shuffle": "Include", + "shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle", + "npcs_shuffle": "Include", + "battlefields_shuffle": "Include", + "logic_options": cc(self.multiworld.logic[self.player]), + "shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]), + "enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]), + "enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]), + "bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]), + "bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]), + "enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]), + "leveling_curve": cc(self.multiworld.leveling_curve[self.player]), + "battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if + self.multiworld.battlefields_battles_quantities[self.player].value < 5 else + "RandomLow" if + self.multiworld.battlefields_battles_quantities[self.player].value == 5 else + "RandomHigh", + "shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]), + "random_starting_weapon": True, + "progressive_gear": tf(self.multiworld.progressive_gear[self.player]), + "tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]), + "doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]), + "doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]), + "sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]), + "sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]), + "enable_spoilers": False, + "progressive_formations": cc(self.multiworld.progressive_formations[self.player]), + "map_shuffling": cc(self.multiworld.map_shuffle[self.player]), + "crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]), + } + for option, data in option_writes.items(): + options["Final Fantasy Mystic Quest"][option][data] = 1 + + rom_name = f'MQ{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21] + self.rom_name = bytearray(rom_name, + 'utf8') + self.rom_name_available_event.set() + + setup = {"version": "1.4", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed": + hex(self.multiworld.per_slot_randoms[self.player].randint(0, 0xFFFFFFFF)).split("0x")[1].upper()} + + starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]] + if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + starting_items.append("SkyCoin") + + file_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.apmq") + + APMQ = APMQFile(file_path, player=self.player, player_name=self.multiworld.player_name[self.player]) + with zipfile.ZipFile(file_path, mode="w", compression=zipfile.ZIP_DEFLATED, + compresslevel=9) as zf: + zf.writestr("itemplacement.yaml", yaml.dump(item_placement)) + zf.writestr("flagset.yaml", yaml.dump(options)) + zf.writestr("startingitems.yaml", yaml.dump(starting_items)) + zf.writestr("setup.yaml", yaml.dump(setup)) + zf.writestr("rooms.yaml", yaml.dump(self.rooms)) + + APMQ.write_contents(zf) + + +class APMQFile(APContainer): + game = "Final Fantasy Mystic Quest" + + def get_manifest(self): + manifest = super().get_manifest() + manifest["patch_file_ending"] = ".apmq" + return manifest \ No newline at end of file diff --git a/worlds/ffmq/Regions.py b/worlds/ffmq/Regions.py new file mode 100644 index 000000000000..aac8289a3600 --- /dev/null +++ b/worlds/ffmq/Regions.py @@ -0,0 +1,251 @@ +from BaseClasses import Region, MultiWorld, Entrance, Location, LocationProgressType, ItemClassification +from worlds.generic.Rules import add_rule +from .Items import item_groups, yaml_item +import pkgutil +import yaml + +rooms = yaml.load(pkgutil.get_data(__name__, "data/rooms.yaml"), yaml.Loader) +entrance_names = {entrance["id"]: entrance["name"] for entrance in yaml.load(pkgutil.get_data(__name__, "data/entrances.yaml"), yaml.Loader)} + +object_id_table = {} +object_type_table = {} +offset = {"Chest": 0x420000, "Box": 0x420000, "NPC": 0x420000 + 300, "BattlefieldItem": 0x420000 + 350} +for room in rooms: + for object in room["game_objects"]: + if "Hero Chest" in object["name"] or object["type"] == "Trigger": + continue + if object["type"] in ("BattlefieldItem", "BattlefieldXp", "BattlefieldGp"): + object_type_table[object["name"]] = "BattlefieldItem" + elif object["type"] in ("Chest", "NPC", "Box"): + object_type_table[object["name"]] = object["type"] + object_id_table[object["name"]] = object["object_id"] + +location_table = {loc_name: offset[object_type_table[loc_name]] + obj_id for loc_name, obj_id in + object_id_table.items()} + +weapons = ("Claw", "Bomb", "Sword", "Axe") +crest_warps = [51, 52, 53, 76, 96, 108, 158, 171, 175, 191, 275, 276, 277, 308, 334, 336, 396, 397] + + +def process_rules(spot, access): + for weapon in weapons: + if weapon in access: + add_rule(spot, lambda state, w=weapon: state.has_any(item_groups[w + "s"], spot.player)) + access = [yaml_item(rule) for rule in access if rule not in weapons] + add_rule(spot, lambda state: state.has_all(access, spot.player)) + + +def create_region(world: MultiWorld, player: int, name: str, room_id=None, locations=None, links=None): + if links is None: + links = [] + ret = Region(name, player, world) + if locations: + for location in locations: + location.parent_region = ret + ret.locations.append(location) + ret.links = links + ret.id = room_id + return ret + + +def get_entrance_to(entrance_to): + for room in rooms: + if room["id"] == entrance_to["target_room"]: + for link in room["links"]: + if link["target_room"] == entrance_to["room"]: + return link + else: + raise Exception(f"Did not find entrance {entrance_to}") + + +def create_regions(self): + + menu_region = create_region(self.multiworld, self.player, "Menu") + self.multiworld.regions.append(menu_region) + + for room in self.rooms: + self.multiworld.regions.append(create_region(self.multiworld, self.player, room["name"], room["id"], + [FFMQLocation(self.player, object["name"], location_table[object["name"]] if object["name"] in + location_table else None, object["type"], object["access"], + self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for + object in room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in + ("BattlefieldGp", "BattlefieldXp") and (object["type"] != "Box" or + self.multiworld.brown_boxes[self.player] == "include")], room["links"])) + + dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player) + dark_king = FFMQLocation(self.player, "Dark King", None, "Trigger", []) + dark_king.parent_region = dark_king_room + dark_king.place_locked_item(self.create_item("Dark King")) + dark_king_room.locations.append(dark_king) + + connection = Entrance(self.player, f"Enter Overworld", menu_region) + connection.connect(self.multiworld.get_region("Overworld", self.player)) + menu_region.exits.append(connection) + + for region in self.multiworld.get_regions(self.player): + for link in region.links: + for connect_room in self.multiworld.get_regions(self.player): + if connect_room.id == link["target_room"]: + connection = Entrance(self.player, entrance_names[link["entrance"]] if "entrance" in link and + link["entrance"] != -1 else f"{region.name} to {connect_room.name}", region) + if "entrance" in link and link["entrance"] != -1: + spoiler = False + if link["entrance"] in crest_warps: + if self.multiworld.crest_shuffle[self.player]: + spoiler = True + elif self.multiworld.map_shuffle[self.player] == "everything": + spoiler = True + elif "Subregion" in region.name and self.multiworld.map_shuffle[self.player] not in ("dungeons", + "none"): + spoiler = True + elif "Subregion" not in region.name and self.multiworld.map_shuffle[self.player] not in ("none", + "overworld"): + spoiler = True + + if spoiler: + self.multiworld.spoiler.set_entrance(entrance_names[link["entrance"]], connect_room.name, + 'both', self.player) + if link["access"]: + process_rules(connection, link["access"]) + region.exits.append(connection) + connection.connect(connect_room) + break + +non_dead_end_crest_rooms = [ + 'Libra Temple', 'Aquaria Gemini Room', "GrenadeMan's Mobius Room", 'Fireburg Gemini Room', + 'Sealed Temple', 'Alive Forest', 'Kaidge Temple Upper Ledge', + 'Windia Kid House Basement', 'Windia Old People House Basement' +] + +non_dead_end_crest_warps = [ + 'Libra Temple - Libra Tile Script', 'Aquaria Gemini Room - Gemini Script', + 'GrenadeMan Mobius Room - Mobius Teleporter Script', 'Fireburg Gemini Room - Gemini Teleporter Script', + 'Sealed Temple - Gemini Tile Script', 'Alive Forest - Libra Teleporter Script', + 'Alive Forest - Gemini Teleporter Script', 'Alive Forest - Mobius Teleporter Script', + 'Kaidge Temple - Mobius Teleporter Script', 'Windia Kid House Basement - Mobius Teleporter', + 'Windia Old People House Basement - Mobius Teleporter Script', +] + + +vendor_locations = ["Aquaria - Vendor", "Fireburg - Vendor", "Windia - Vendor"] + + +def set_rules(self) -> None: + self.multiworld.completion_condition[self.player] = lambda state: state.has("Dark King", self.player) + + def hard_boss_logic(state): + return state.has_all(["River Coin", "Sand Coin"], self.player) + + add_rule(self.multiworld.get_location("Pazuzu 1F", self.player), hard_boss_logic) + add_rule(self.multiworld.get_location("Gidrah", self.player), hard_boss_logic) + add_rule(self.multiworld.get_location("Dullahan", self.player), hard_boss_logic) + + if self.multiworld.map_shuffle[self.player]: + for boss in ("Freezer Crab", "Ice Golem", "Jinn", "Medusa", "Dualhead Hydra"): + loc = self.multiworld.get_location(boss, self.player) + checked_regions = {loc.parent_region} + + def check_foresta(region): + if region.name == "Subregion Foresta": + add_rule(loc, hard_boss_logic) + return True + elif "Subregion" in region.name: + return True + for entrance in region.entrances: + if entrance.parent_region not in checked_regions: + checked_regions.add(entrance.parent_region) + if check_foresta(entrance.parent_region): + return True + check_foresta(loc.parent_region) + + if self.multiworld.logic[self.player] == "friendly": + process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player), + ["MagicMirror"]) + process_rules(self.multiworld.get_entrance("Overworld - Volcano", self.player), + ["Mask"]) + if self.multiworld.map_shuffle[self.player] in ("none", "overworld"): + process_rules(self.multiworld.get_entrance("Overworld - Bone Dungeon", self.player), + ["Bomb"]) + process_rules(self.multiworld.get_entrance("Overworld - Wintry Cave", self.player), + ["Bomb", "Claw"]) + process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player), + ["Bomb", "Claw"]) + process_rules(self.multiworld.get_entrance("Overworld - Mine", self.player), + ["MegaGrenade", "Claw", "Reuben1"]) + process_rules(self.multiworld.get_entrance("Overworld - Lava Dome", self.player), + ["MegaGrenade"]) + process_rules(self.multiworld.get_entrance("Overworld - Giant Tree", self.player), + ["DragonClaw", "Axe"]) + process_rules(self.multiworld.get_entrance("Overworld - Mount Gale", self.player), + ["DragonClaw"]) + process_rules(self.multiworld.get_entrance("Overworld - Pazuzu Tower", self.player), + ["DragonClaw", "Bomb"]) + process_rules(self.multiworld.get_entrance("Overworld - Mac Ship", self.player), + ["DragonClaw", "CaptainCap"]) + process_rules(self.multiworld.get_entrance("Overworld - Mac Ship Doom", self.player), + ["DragonClaw", "CaptainCap"]) + + if self.multiworld.logic[self.player] == "expert": + if self.multiworld.map_shuffle[self.player] == "none" and not self.multiworld.crest_shuffle[self.player]: + inner_room = self.multiworld.get_region("Wintry Temple Inner Room", self.player) + connection = Entrance(self.player, "Sealed Temple Exit Trick", inner_room) + connection.connect(self.multiworld.get_region("Wintry Temple Outer Room", self.player)) + connection.access_rule = lambda state: state.has("Exit Book", self.player) + inner_room.exits.append(connection) + else: + for crest_warp in non_dead_end_crest_warps: + entrance = self.multiworld.get_entrance(crest_warp, self.player) + if entrance.connected_region.name in non_dead_end_crest_rooms: + entrance.access_rule = lambda state: False + + if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + logic_coins = [16, 24, 32, 32, 38][self.multiworld.shattered_sky_coin_quantity[self.player].value] + self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ + lambda state: state.has("Sky Fragment", self.player, logic_coins) + elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals": + self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ + lambda state: state.has_all(["Flamerus Rex", "Dualhead Hydra", "Ice Golem", "Pazuzu"], self.player) + elif self.multiworld.sky_coin_mode[self.player] in ("standard", "start_with"): + self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ + lambda state: state.has("Sky Coin", self.player) + + +def stage_set_rules(multiworld): + # If there's no enemies, there's no repeatable income sources + no_enemies_players = [player for player in multiworld.get_game_players("Final Fantasy Mystic Quest") + if multiworld.enemies_density[player] == "none"] + if (len([item for item in multiworld.itempool if item.classification in (ItemClassification.filler, + ItemClassification.trap)]) > len([player for player in no_enemies_players if + multiworld.accessibility[player] == "minimal"]) * 3): + for player in no_enemies_players: + for location in vendor_locations: + if multiworld.accessibility[player] == "locations": + print("exclude") + multiworld.get_location(location, player).progress_type = LocationProgressType.EXCLUDED + else: + print("unreachable") + multiworld.get_location(location, player).access_rule = lambda state: False + else: + # There are not enough junk items to fill non-minimal players' vendors. Just set an item rule not allowing + # advancement items so that useful items can be placed. + print("no advancement") + for player in no_enemies_players: + for location in vendor_locations: + multiworld.get_location(location, player).item_rule = lambda item: not item.advancement + + + + +class FFMQLocation(Location): + game = "Final Fantasy Mystic Quest" + + def __init__(self, player, name, address, loc_type, access=None, event=None): + super(FFMQLocation, self).__init__( + player, name, + address + ) + self.type = loc_type + if access: + process_rules(self, access) + if event: + self.place_locked_item(event) diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py new file mode 100644 index 000000000000..b6f19a77fb53 --- /dev/null +++ b/worlds/ffmq/__init__.py @@ -0,0 +1,217 @@ +import Utils +import settings +import base64 +import threading +import requests +import yaml +from worlds.AutoWorld import World, WebWorld +from BaseClasses import Tutorial +from .Regions import create_regions, location_table, set_rules, stage_set_rules, rooms, non_dead_end_crest_rooms,\ + non_dead_end_crest_warps +from .Items import item_table, item_groups, create_items, FFMQItem, fillers +from .Output import generate_output +from .Options import option_definitions +from .Client import FFMQClient + + +# removed until lists are supported +# class FFMQSettings(settings.Group): +# class APIUrls(list): +# """A list of API URLs to get map shuffle, crest shuffle, and battlefield reward shuffle data from.""" +# api_urls: APIUrls = [ +# "https://api.ffmqrando.net/", +# "http://ffmqr.jalchavware.com:5271/" +# ] + + +class FFMQWebWorld(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to playing Final Fantasy Mystic Quest with Archipelago.", + "English", + "setup_en.md", + "setup/en", + ["Alchav"] + )] + + +class FFMQWorld(World): + """Final Fantasy: Mystic Quest is a simple, humorous RPG for the Super Nintendo. You travel across four continents, + linked in the middle of the world by the Focus Tower, which has been locked by four magical coins. Make your way to + the bottom of the Focus Tower, then straight up through the top!""" + # -Giga Otomia + + game = "Final Fantasy Mystic Quest" + + item_name_to_id = {name: data.id for name, data in item_table.items() if data.id is not None} + location_name_to_id = location_table + option_definitions = option_definitions + + topology_present = True + + item_name_groups = item_groups + + generate_output = generate_output + create_items = create_items + create_regions = create_regions + set_rules = set_rules + stage_set_rules = stage_set_rules + + data_version = 1 + + web = FFMQWebWorld() + # settings: FFMQSettings + + def __init__(self, world, player: int): + self.rom_name_available_event = threading.Event() + self.rom_name = None + self.rooms = None + super().__init__(world, player) + + def generate_early(self): + if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + self.multiworld.brown_boxes[self.player].value = 1 + if self.multiworld.enemies_scaling_lower[self.player].value > \ + self.multiworld.enemies_scaling_upper[self.player].value: + (self.multiworld.enemies_scaling_lower[self.player].value, + self.multiworld.enemies_scaling_upper[self.player].value) =\ + (self.multiworld.enemies_scaling_upper[self.player].value, + self.multiworld.enemies_scaling_lower[self.player].value) + if self.multiworld.bosses_scaling_lower[self.player].value > \ + self.multiworld.bosses_scaling_upper[self.player].value: + (self.multiworld.bosses_scaling_lower[self.player].value, + self.multiworld.bosses_scaling_upper[self.player].value) =\ + (self.multiworld.bosses_scaling_upper[self.player].value, + self.multiworld.bosses_scaling_lower[self.player].value) + + @classmethod + def stage_generate_early(cls, multiworld): + + # api_urls = Utils.get_options()["ffmq_options"].get("api_urls", None) + api_urls = [ + "https://api.ffmqrando.net/", + "http://ffmqr.jalchavware.com:5271/" + ] + + rooms_data = {} + + for world in multiworld.get_game_worlds("Final Fantasy Mystic Quest"): + if (world.multiworld.map_shuffle[world.player] or world.multiworld.crest_shuffle[world.player] or + world.multiworld.crest_shuffle[world.player]): + if world.multiworld.map_shuffle_seed[world.player].value.isdigit(): + multiworld.random.seed(int(world.multiworld.map_shuffle_seed[world.player].value)) + elif world.multiworld.map_shuffle_seed[world.player].value != "random": + multiworld.random.seed(int(hash(world.multiworld.map_shuffle_seed[world.player].value)) + + int(world.multiworld.seed)) + + seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper() + map_shuffle = multiworld.map_shuffle[world.player].value + crest_shuffle = multiworld.crest_shuffle[world.player].current_key + battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key + + query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}" + + if query in rooms_data: + world.rooms = rooms_data[query] + continue + + if not api_urls: + raise Exception("No FFMQR API URLs specified in host.yaml") + + errors = [] + for api_url in api_urls.copy(): + try: + response = requests.get(f"{api_url}GenerateRooms?{query}") + except (ConnectionError, requests.exceptions.HTTPError, requests.exceptions.ConnectionError, + requests.exceptions.RequestException) as err: + api_urls.remove(api_url) + errors.append([api_url, err]) + else: + if response.ok: + world.rooms = rooms_data[query] = yaml.load(response.text, yaml.Loader) + break + else: + api_urls.remove(api_url) + errors.append([api_url, response]) + else: + error_text = f"Failed to fetch map shuffle data for FFMQ player {world.player}" + for error in errors: + error_text += f"\n{error[0]} - got error {error[1].status_code} {error[1].reason} {error[1].text}" + raise Exception(error_text) + api_urls.append(api_urls.pop(0)) + else: + world.rooms = rooms + + def create_item(self, name: str): + return FFMQItem(name, self.player) + + def collect_item(self, state, item, remove=False): + if "Progressive" in item.name: + i = item.code - 256 + if state.has(self.item_id_to_name[i], self.player): + if state.has(self.item_id_to_name[i+1], self.player): + return self.item_id_to_name[i+2] + return self.item_id_to_name[i+1] + return self.item_id_to_name[i] + return item.name if item.advancement else None + + def modify_multidata(self, multidata): + # wait for self.rom_name to be available. + self.rom_name_available_event.wait() + rom_name = getattr(self, "rom_name", None) + # we skip in case of error, so that the original error in the output thread is the one that gets raised + if rom_name: + new_name = base64.b64encode(bytes(self.rom_name)).decode() + payload = multidata["connect_names"][self.multiworld.player_name[self.player]] + multidata["connect_names"][new_name] = payload + + def get_filler_item_name(self): + r = self.multiworld.random.randint(0, 201) + for item, count in fillers.items(): + r -= count + r -= fillers[item] + if r <= 0: + return item + + def extend_hint_information(self, hint_data): + hint_data[self.player] = {} + if self.multiworld.map_shuffle[self.player]: + single_location_regions = ["Subregion Volcano Battlefield", "Subregion Mac's Ship", "Subregion Doom Castle"] + for subregion in ["Subregion Foresta", "Subregion Aquaria", "Subregion Frozen Fields", "Subregion Fireburg", + "Subregion Volcano Battlefield", "Subregion Windia", "Subregion Mac's Ship", + "Subregion Doom Castle"]: + region = self.multiworld.get_region(subregion, self.player) + for location in region.locations: + if location.address and self.multiworld.map_shuffle[self.player] != "dungeons": + hint_data[self.player][location.address] = (subregion.split("Subregion ")[-1] + + (" Region" if subregion not in + single_location_regions else "")) + for overworld_spot in region.exits: + if ("Subregion" in overworld_spot.connected_region.name or + overworld_spot.name == "Overworld - Mac Ship Doom" or "Focus Tower" in overworld_spot.name + or "Doom Castle" in overworld_spot.name or overworld_spot.name == "Overworld - Giant Tree"): + continue + exits = list(overworld_spot.connected_region.exits) + [overworld_spot] + checked_regions = set() + while exits: + exit_check = exits.pop() + if (exit_check.connected_region not in checked_regions and "Subregion" not in + exit_check.connected_region.name): + checked_regions.add(exit_check.connected_region) + exits.extend(exit_check.connected_region.exits) + for location in exit_check.connected_region.locations: + if location.address: + hint = [] + if self.multiworld.map_shuffle[self.player] != "dungeons": + hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not + in single_location_regions else ""))) + if self.multiworld.map_shuffle[self.player] != "overworld" and subregion not in \ + ("Subregion Mac's Ship", "Subregion Doom Castle"): + hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu", + "Pazuzu's")) + hint = " - ".join(hint) + if location.address in hint_data[self.player]: + hint_data[self.player][location.address] += f"/{hint}" + else: + hint_data[self.player][location.address] = hint + diff --git a/worlds/ffmq/data/entrances.yaml b/worlds/ffmq/data/entrances.yaml new file mode 100644 index 000000000000..15bcd02bf623 --- /dev/null +++ b/worlds/ffmq/data/entrances.yaml @@ -0,0 +1,2425 @@ +- name: Doom Castle - Sand Floor - To Sky Door - Sand Floor + id: 0 + area: 7 + coordinates: [24, 19] + teleporter: [0, 0] +- name: Doom Castle - Sand Floor - Main Entrance - Sand Floor + id: 1 + area: 7 + coordinates: [19, 43] + teleporter: [1, 6] +- name: Doom Castle - Aero Room - Aero Room Entrance + id: 2 + area: 7 + coordinates: [27, 39] + teleporter: [1, 0] +- name: Focus Tower B1 - Main Loop - South Entrance + id: 3 + area: 8 + coordinates: [43, 60] + teleporter: [2, 6] +- name: Focus Tower B1 - Main Loop - To Focus Tower 1F - Main Hall + id: 4 + area: 8 + coordinates: [37, 41] + teleporter: [4, 0] +- name: Focus Tower B1 - Aero Corridor - To Focus Tower 1F - Sun Coin Room + id: 5 + area: 8 + coordinates: [59, 35] + teleporter: [5, 0] +- name: Focus Tower B1 - Aero Corridor - To Sand Floor - Aero Chest + id: 6 + area: 8 + coordinates: [57, 59] + teleporter: [8, 0] +- name: Focus Tower B1 - Inner Loop - To Focus Tower 1F - Sky Door + id: 7 + area: 8 + coordinates: [51, 49] + teleporter: [6, 0] +- name: Focus Tower B1 - Inner Loop - To Doom Castle Sand Floor + id: 8 + area: 8 + coordinates: [51, 45] + teleporter: [7, 0] +- name: Focus Tower 1F - Focus Tower West Entrance + id: 9 + area: 9 + coordinates: [25, 29] + teleporter: [3, 6] +- name: Focus Tower 1F - To Focus Tower 2F - From SandCoin + id: 10 + area: 9 + coordinates: [16, 4] + teleporter: [10, 0] +- name: Focus Tower 1F - To Focus Tower B1 - Main Hall + id: 11 + area: 9 + coordinates: [4, 23] + teleporter: [11, 0] +- name: Focus Tower 1F - To Focus Tower B1 - To Aero Chest + id: 12 + area: 9 + coordinates: [26, 17] + teleporter: [12, 0] +- name: Focus Tower 1F - Sky Door + id: 13 + area: 9 + coordinates: [16, 24] + teleporter: [13, 0] +- name: Focus Tower 1F - To Focus Tower 2F - From RiverCoin + id: 14 + area: 9 + coordinates: [16, 10] + teleporter: [14, 0] +- name: Focus Tower 1F - To Focus Tower B1 - From Sky Door + id: 15 + area: 9 + coordinates: [16, 29] + teleporter: [15, 0] +- name: Focus Tower 2F - Sand Coin Passage - North Entrance + id: 16 + area: 10 + coordinates: [49, 30] + teleporter: [4, 6] +- name: Focus Tower 2F - Sand Coin Passage - To Focus Tower 1F - To SandCoin + id: 17 + area: 10 + coordinates: [47, 33] + teleporter: [17, 0] +- name: Focus Tower 2F - River Coin Passage - To Focus Tower 1F - To RiverCoin + id: 18 + area: 10 + coordinates: [47, 41] + teleporter: [18, 0] +- name: Focus Tower 2F - River Coin Passage - To Focus Tower 3F - Lower Floor + id: 19 + area: 10 + coordinates: [38, 40] + teleporter: [20, 0] +- name: Focus Tower 2F - Venus Chest Room - To Focus Tower 3F - Upper Floor + id: 20 + area: 10 + coordinates: [56, 40] + teleporter: [19, 0] +- name: Focus Tower 2F - Venus Chest Room - Pillar Script + id: 21 + area: 10 + coordinates: [48, 53] + teleporter: [13, 8] +- name: Focus Tower 3F - Lower Floor - To Fireburg Entrance + id: 22 + area: 11 + coordinates: [11, 39] + teleporter: [6, 6] +- name: Focus Tower 3F - Lower Floor - To Focus Tower 2F - Jump on Pillar + id: 23 + area: 11 + coordinates: [6, 47] + teleporter: [24, 0] +- name: Focus Tower 3F - Upper Floor - To Aquaria Entrance + id: 24 + area: 11 + coordinates: [21, 38] + teleporter: [5, 6] +- name: Focus Tower 3F - Upper Floor - To Focus Tower 2F - Venus Chest Room + id: 25 + area: 11 + coordinates: [24, 47] + teleporter: [23, 0] +- name: Level Forest - Boulder Script + id: 26 + area: 14 + coordinates: [52, 15] + teleporter: [0, 8] +- name: Level Forest - Rotten Tree Script + id: 27 + area: 14 + coordinates: [47, 6] + teleporter: [2, 8] +- name: Level Forest - Exit Level Forest 1 + id: 28 + area: 14 + coordinates: [46, 25] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 2 + id: 29 + area: 14 + coordinates: [46, 26] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 3 + id: 30 + area: 14 + coordinates: [47, 25] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 4 + id: 31 + area: 14 + coordinates: [47, 26] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 5 + id: 32 + area: 14 + coordinates: [60, 14] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 6 + id: 33 + area: 14 + coordinates: [61, 14] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 7 + id: 34 + area: 14 + coordinates: [46, 4] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 8 + id: 35 + area: 14 + coordinates: [46, 3] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 9 + id: 36 + area: 14 + coordinates: [47, 4] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest A + id: 37 + area: 14 + coordinates: [47, 3] + teleporter: [25, 0] +- name: Foresta - Exit Foresta 1 + id: 38 + area: 15 + coordinates: [10, 25] + teleporter: [31, 0] +- name: Foresta - Exit Foresta 2 + id: 39 + area: 15 + coordinates: [10, 26] + teleporter: [31, 0] +- name: Foresta - Exit Foresta 3 + id: 40 + area: 15 + coordinates: [11, 25] + teleporter: [31, 0] +- name: Foresta - Exit Foresta 4 + id: 41 + area: 15 + coordinates: [11, 26] + teleporter: [31, 0] +- name: Foresta - Old Man House - Front Door + id: 42 + area: 15 + coordinates: [25, 17] + teleporter: [32, 4] +- name: Foresta - Old Man House - Back Door + id: 43 + area: 15 + coordinates: [25, 14] + teleporter: [33, 0] +- name: Foresta - Kaeli's House + id: 44 + area: 15 + coordinates: [7, 21] + teleporter: [0, 5] +- name: Foresta - Rest House + id: 45 + area: 15 + coordinates: [23, 23] + teleporter: [1, 5] +- name: Kaeli's House - Kaeli's House Entrance + id: 46 + area: 16 + coordinates: [11, 20] + teleporter: [86, 3] +- name: Foresta Houses - Old Man's House - Old Man Front Exit + id: 47 + area: 17 + coordinates: [35, 44] + teleporter: [34, 0] +- name: Foresta Houses - Old Man's House - Old Man Back Exit + id: 48 + area: 17 + coordinates: [35, 27] + teleporter: [35, 0] +- name: Foresta - Old Man House - Barrel Tile Script # New, use the focus tower column's script + id: 483 + area: 17 + coordinates: [0x23, 0x1E] + teleporter: [0x0D, 8] +- name: Foresta Houses - Rest House - Bed Script + id: 49 + area: 17 + coordinates: [30, 6] + teleporter: [1, 8] +- name: Foresta Houses - Rest House - Rest House Exit + id: 50 + area: 17 + coordinates: [35, 20] + teleporter: [87, 3] +- name: Foresta Houses - Libra House - Libra House Script + id: 51 + area: 17 + coordinates: [8, 49] + teleporter: [67, 8] +- name: Foresta Houses - Gemini House - Gemini House Script + id: 52 + area: 17 + coordinates: [26, 55] + teleporter: [68, 8] +- name: Foresta Houses - Mobius House - Mobius House Script + id: 53 + area: 17 + coordinates: [14, 33] + teleporter: [69, 8] +- name: Sand Temple - Sand Temple Entrance + id: 54 + area: 18 + coordinates: [56, 27] + teleporter: [36, 0] +- name: Bone Dungeon 1F - Bone Dungeon Entrance + id: 55 + area: 19 + coordinates: [13, 60] + teleporter: [37, 0] +- name: Bone Dungeon 1F - To Bone Dungeon B1 + id: 56 + area: 19 + coordinates: [13, 39] + teleporter: [2, 2] +- name: Bone Dungeon B1 - Waterway - Exit Waterway + id: 57 + area: 20 + coordinates: [27, 39] + teleporter: [3, 2] +- name: Bone Dungeon B1 - Waterway - Tristam's Script + id: 58 + area: 20 + coordinates: [27, 45] + teleporter: [3, 8] +- name: Bone Dungeon B1 - Waterway - To Bone Dungeon 1F + id: 59 + area: 20 + coordinates: [54, 61] + teleporter: [88, 3] +- name: Bone Dungeon B1 - Checker Room - Exit Checker Room + id: 60 + area: 20 + coordinates: [23, 40] + teleporter: [4, 2] +- name: Bone Dungeon B1 - Checker Room - To Waterway + id: 61 + area: 20 + coordinates: [39, 49] + teleporter: [89, 3] +- name: Bone Dungeon B1 - Hidden Room - To B2 - Exploding Skull Room + id: 62 + area: 20 + coordinates: [5, 33] + teleporter: [91, 3] +- name: Bonne Dungeon B2 - Exploding Skull Room - To Hidden Passage + id: 63 + area: 21 + coordinates: [19, 13] + teleporter: [5, 2] +- name: Bonne Dungeon B2 - Exploding Skull Room - To Two Skulls Room + id: 64 + area: 21 + coordinates: [29, 15] + teleporter: [6, 2] +- name: Bonne Dungeon B2 - Exploding Skull Room - To Checker Room + id: 65 + area: 21 + coordinates: [8, 25] + teleporter: [90, 3] +- name: Bonne Dungeon B2 - Box Room - To B2 - Two Skulls Room + id: 66 + area: 21 + coordinates: [59, 12] + teleporter: [93, 3] +- name: Bonne Dungeon B2 - Quake Room - To B2 - Two Skulls Room + id: 67 + area: 21 + coordinates: [59, 28] + teleporter: [94, 3] +- name: Bonne Dungeon B2 - Two Skulls Room - To Box Room + id: 68 + area: 21 + coordinates: [53, 7] + teleporter: [7, 2] +- name: Bonne Dungeon B2 - Two Skulls Room - To Quake Room + id: 69 + area: 21 + coordinates: [41, 3] + teleporter: [8, 2] +- name: Bonne Dungeon B2 - Two Skulls Room - To Boss Room + id: 70 + area: 21 + coordinates: [47, 57] + teleporter: [9, 2] +- name: Bonne Dungeon B2 - Two Skulls Room - To B2 - Exploding Skull Room + id: 71 + area: 21 + coordinates: [54, 23] + teleporter: [92, 3] +- name: Bone Dungeon B2 - Boss Room - Flamerus Rex Script + id: 72 + area: 22 + coordinates: [29, 19] + teleporter: [4, 8] +- name: Bone Dungeon B2 - Boss Room - Tristam Leave Script + id: 73 + area: 22 + coordinates: [29, 23] + teleporter: [75, 8] +- name: Bone Dungeon B2 - Boss Room - To B2 - Two Skulls Room + id: 74 + area: 22 + coordinates: [30, 27] + teleporter: [95, 3] +- name: Libra Temple - Entrance + id: 75 + area: 23 + coordinates: [10, 15] + teleporter: [13, 6] +- name: Libra Temple - Libra Tile Script + id: 76 + area: 23 + coordinates: [9, 8] + teleporter: [59, 8] +- name: Aquaria Winter - Winter Entrance 1 + id: 77 + area: 24 + coordinates: [25, 25] + teleporter: [8, 6] +- name: Aquaria Winter - Winter Entrance 2 + id: 78 + area: 24 + coordinates: [25, 26] + teleporter: [8, 6] +- name: Aquaria Winter - Winter Entrance 3 + id: 79 + area: 24 + coordinates: [26, 25] + teleporter: [8, 6] +- name: Aquaria Winter - Winter Entrance 4 + id: 80 + area: 24 + coordinates: [26, 26] + teleporter: [8, 6] +- name: Aquaria Winter - Winter Phoebe's House Entrance Script #Modified to not be a script + id: 81 + area: 24 + coordinates: [8, 19] + teleporter: [10, 5] # original value [5, 8] +- name: Aquaria Winter - Winter Vendor House Entrance + id: 82 + area: 24 + coordinates: [8, 5] + teleporter: [44, 4] +- name: Aquaria Winter - Winter INN Entrance + id: 83 + area: 24 + coordinates: [26, 17] + teleporter: [11, 5] +- name: Aquaria Summer - Summer Entrance 1 + id: 84 + area: 25 + coordinates: [57, 25] + teleporter: [8, 6] +- name: Aquaria Summer - Summer Entrance 2 + id: 85 + area: 25 + coordinates: [57, 26] + teleporter: [8, 6] +- name: Aquaria Summer - Summer Entrance 3 + id: 86 + area: 25 + coordinates: [58, 25] + teleporter: [8, 6] +- name: Aquaria Summer - Summer Entrance 4 + id: 87 + area: 25 + coordinates: [58, 26] + teleporter: [8, 6] +- name: Aquaria Summer - Summer Phoebe's House Entrance + id: 88 + area: 25 + coordinates: [40, 19] + teleporter: [10, 5] +- name: Aquaria Summer - Spencer's Place Entrance Top + id: 89 + area: 25 + coordinates: [40, 16] + teleporter: [42, 0] +- name: Aquaria Summer - Spencer's Place Entrance Side + id: 90 + area: 25 + coordinates: [41, 18] + teleporter: [43, 0] +- name: Aquaria Summer - Summer Vendor House Entrance + id: 91 + area: 25 + coordinates: [40, 5] + teleporter: [44, 4] +- name: Aquaria Summer - Summer INN Entrance + id: 92 + area: 25 + coordinates: [58, 17] + teleporter: [11, 5] +- name: Phoebe's House - Entrance # Change to a script, same as vendor house + id: 93 + area: 26 + coordinates: [29, 14] + teleporter: [5, 8] # Original Value [11,3] +- name: Aquaria Vendor House - Vendor House Entrance's Script + id: 94 + area: 27 + coordinates: [7, 10] + teleporter: [40, 8] +- name: Aquaria Vendor House - Vendor House Stairs + id: 95 + area: 27 + coordinates: [1, 4] + teleporter: [47, 0] +- name: Aquaria Gemini Room - Gemini Script + id: 96 + area: 27 + coordinates: [2, 40] + teleporter: [72, 8] +- name: Aquaria Gemini Room - Gemini Room Stairs + id: 97 + area: 27 + coordinates: [4, 39] + teleporter: [48, 0] +- name: Aquaria INN - Aquaria INN entrance # Change to a script, same as vendor house + id: 98 + area: 27 + coordinates: [51, 46] + teleporter: [75, 8] # Original value [48,3] +- name: Wintry Cave 1F - Main Entrance + id: 99 + area: 28 + coordinates: [50, 58] + teleporter: [49, 0] +- name: Wintry Cave 1F - To 3F Top + id: 100 + area: 28 + coordinates: [40, 25] + teleporter: [14, 2] +- name: Wintry Cave 1F - To 2F + id: 101 + area: 28 + coordinates: [10, 43] + teleporter: [15, 2] +- name: Wintry Cave 1F - Phoebe's Script + id: 102 + area: 28 + coordinates: [44, 37] + teleporter: [6, 8] +- name: Wintry Cave 2F - To 3F Bottom + id: 103 + area: 29 + coordinates: [58, 5] + teleporter: [50, 0] +- name: Wintry Cave 2F - To 1F + id: 104 + area: 29 + coordinates: [38, 18] + teleporter: [97, 3] +- name: Wintry Cave 3F Top - Exit from 3F Top + id: 105 + area: 30 + coordinates: [24, 6] + teleporter: [96, 3] +- name: Wintry Cave 3F Bottom - Exit to 2F + id: 106 + area: 31 + coordinates: [4, 29] + teleporter: [51, 0] +- name: Life Temple - Entrance + id: 107 + area: 32 + coordinates: [9, 60] + teleporter: [14, 6] +- name: Life Temple - Libra Tile Script + id: 108 + area: 32 + coordinates: [3, 55] + teleporter: [60, 8] +- name: Life Temple - Mysterious Man Script + id: 109 + area: 32 + coordinates: [9, 44] + teleporter: [78, 8] +- name: Fall Basin - Back Exit Script + id: 110 + area: 33 + coordinates: [17, 5] + teleporter: [9, 0] # Remove script [42, 8] for overworld teleport (but not main exit) +- name: Fall Basin - Main Exit + id: 111 + area: 33 + coordinates: [15, 26] + teleporter: [53, 0] +- name: Fall Basin - Phoebe's Script + id: 112 + area: 33 + coordinates: [17, 6] + teleporter: [9, 8] +- name: Ice Pyramid B1 Taunt Room - To Climbing Wall Room + id: 113 + area: 34 + coordinates: [43, 6] + teleporter: [55, 0] +- name: Ice Pyramid 1F Maze - Main Entrance 1 + id: 114 + area: 35 + coordinates: [18, 36] + teleporter: [56, 0] +- name: Ice Pyramid 1F Maze - Main Entrance 2 + id: 115 + area: 35 + coordinates: [19, 36] + teleporter: [56, 0] +- name: Ice Pyramid 1F Maze - West Stairs To 2F South Tiled Room + id: 116 + area: 35 + coordinates: [3, 27] + teleporter: [57, 0] +- name: Ice Pyramid 1F Maze - West Center Stairs to 2F West Room + id: 117 + area: 35 + coordinates: [11, 15] + teleporter: [58, 0] +- name: Ice Pyramid 1F Maze - East Center Stairs to 2F Center Room + id: 118 + area: 35 + coordinates: [25, 16] + teleporter: [59, 0] +- name: Ice Pyramid 1F Maze - Upper Stairs to 2F Small North Room + id: 119 + area: 35 + coordinates: [31, 1] + teleporter: [60, 0] +- name: Ice Pyramid 1F Maze - East Stairs to 2F North Corridor + id: 120 + area: 35 + coordinates: [34, 9] + teleporter: [61, 0] +- name: Ice Pyramid 1F Maze - Statue's Script + id: 121 + area: 35 + coordinates: [21, 32] + teleporter: [77, 8] +- name: Ice Pyramid 2F South Tiled Room - To 1F + id: 122 + area: 36 + coordinates: [4, 26] + teleporter: [62, 0] +- name: Ice Pyramid 2F South Tiled Room - To 3F Two Boxes Room + id: 123 + area: 36 + coordinates: [22, 17] + teleporter: [67, 0] +- name: Ice Pyramid 2F West Room - To 1F + id: 124 + area: 36 + coordinates: [9, 10] + teleporter: [63, 0] +- name: Ice Pyramid 2F Center Room - To 1F + id: 125 + area: 36 + coordinates: [22, 14] + teleporter: [64, 0] +- name: Ice Pyramid 2F Small North Room - To 1F + id: 126 + area: 36 + coordinates: [26, 4] + teleporter: [65, 0] +- name: Ice Pyramid 2F North Corridor - To 1F + id: 127 + area: 36 + coordinates: [32, 8] + teleporter: [66, 0] +- name: Ice Pyramid 2F North Corridor - To 3F Main Loop + id: 128 + area: 36 + coordinates: [12, 7] + teleporter: [68, 0] +- name: Ice Pyramid 3F Two Boxes Room - To 2F South Tiled Room + id: 129 + area: 37 + coordinates: [24, 54] + teleporter: [69, 0] +- name: Ice Pyramid 3F Main Loop - To 2F Corridor + id: 130 + area: 37 + coordinates: [16, 45] + teleporter: [70, 0] +- name: Ice Pyramid 3F Main Loop - To 4F + id: 131 + area: 37 + coordinates: [19, 43] + teleporter: [71, 0] +- name: Ice Pyramid 4F Treasure Room - To 3F Main Loop + id: 132 + area: 38 + coordinates: [52, 5] + teleporter: [72, 0] +- name: Ice Pyramid 4F Treasure Room - To 5F Leap of Faith Room + id: 133 + area: 38 + coordinates: [62, 19] + teleporter: [73, 0] +- name: Ice Pyramid 5F Leap of Faith Room - To 4F Treasure Room + id: 134 + area: 39 + coordinates: [54, 63] + teleporter: [74, 0] +- name: Ice Pyramid 5F Leap of Faith Room - Bombed Ice Plate + id: 135 + area: 39 + coordinates: [47, 54] + teleporter: [77, 8] +- name: Ice Pyramid 5F Stairs to Ice Golem - To Ice Golem Room + id: 136 + area: 39 + coordinates: [39, 43] + teleporter: [75, 0] +- name: Ice Pyramid 5F Stairs to Ice Golem - To Climbing Wall Room + id: 137 + area: 39 + coordinates: [39, 60] + teleporter: [76, 0] +- name: Ice Pyramid - Duplicate Ice Golem Room # not used? + id: 138 + area: 40 + coordinates: [44, 43] + teleporter: [77, 0] +- name: Ice Pyramid Climbing Wall Room - To Taunt Room + id: 139 + area: 41 + coordinates: [4, 59] + teleporter: [78, 0] +- name: Ice Pyramid Climbing Wall Room - To 5F Stairs + id: 140 + area: 41 + coordinates: [4, 45] + teleporter: [79, 0] +- name: Ice Pyramid Ice Golem Room - To 5F Stairs + id: 141 + area: 42 + coordinates: [44, 43] + teleporter: [80, 0] +- name: Ice Pyramid Ice Golem Room - Ice Golem Script + id: 142 + area: 42 + coordinates: [53, 32] + teleporter: [10, 8] +- name: Spencer Waterfall - To Spencer Cave + id: 143 + area: 43 + coordinates: [48, 57] + teleporter: [81, 0] +- name: Spencer Waterfall - Upper Exit to Aquaria 1 + id: 144 + area: 43 + coordinates: [40, 5] + teleporter: [82, 0] +- name: Spencer Waterfall - Upper Exit to Aquaria 2 + id: 145 + area: 43 + coordinates: [40, 6] + teleporter: [82, 0] +- name: Spencer Waterfall - Upper Exit to Aquaria 3 + id: 146 + area: 43 + coordinates: [41, 5] + teleporter: [82, 0] +- name: Spencer Waterfall - Upper Exit to Aquaria 4 + id: 147 + area: 43 + coordinates: [41, 6] + teleporter: [82, 0] +- name: Spencer Waterfall - Right Exit to Aquaria 1 + id: 148 + area: 43 + coordinates: [46, 8] + teleporter: [83, 0] +- name: Spencer Waterfall - Right Exit to Aquaria 2 + id: 149 + area: 43 + coordinates: [47, 8] + teleporter: [83, 0] +- name: Spencer Cave Normal Main - To Waterfall + id: 150 + area: 44 + coordinates: [14, 39] + teleporter: [85, 0] +- name: Spencer Cave Normal From Overworld - Exit to Overworld + id: 151 + area: 44 + coordinates: [15, 57] + teleporter: [7, 6] +- name: Spencer Cave Unplug - Exit to Overworld + id: 152 + area: 45 + coordinates: [40, 29] + teleporter: [7, 6] +- name: Spencer Cave Unplug - Libra Teleporter Start Script + id: 153 + area: 45 + coordinates: [28, 21] + teleporter: [33, 8] +- name: Spencer Cave Unplug - Libra Teleporter End Script + id: 154 + area: 45 + coordinates: [46, 4] + teleporter: [34, 8] +- name: Spencer Cave Unplug - Mobius Teleporter Chest Script + id: 155 + area: 45 + coordinates: [21, 9] + teleporter: [35, 8] +- name: Spencer Cave Unplug - Mobius Teleporter Start Script + id: 156 + area: 45 + coordinates: [29, 28] + teleporter: [36, 8] +- name: Wintry Temple Outer Room - Main Entrance + id: 157 + area: 46 + coordinates: [8, 31] + teleporter: [15, 6] +- name: Wintry Temple Inner Room - Gemini Tile to Sealed temple + id: 158 + area: 46 + coordinates: [9, 24] + teleporter: [62, 8] +- name: Fireburg - To Overworld + id: 159 + area: 47 + coordinates: [4, 13] + teleporter: [9, 6] +- name: Fireburg - To Overworld + id: 160 + area: 47 + coordinates: [5, 13] + teleporter: [9, 6] +- name: Fireburg - To Overworld + id: 161 + area: 47 + coordinates: [28, 15] + teleporter: [9, 6] +- name: Fireburg - To Overworld + id: 162 + area: 47 + coordinates: [27, 15] + teleporter: [9, 6] +- name: Fireburg - Vendor House + id: 163 + area: 47 + coordinates: [10, 24] + teleporter: [91, 0] +- name: Fireburg - Reuben House + id: 164 + area: 47 + coordinates: [14, 6] + teleporter: [16, 2] +- name: Fireburg - Hotel + id: 165 + area: 47 + coordinates: [20, 8] + teleporter: [17, 2] +- name: Fireburg - GrenadeMan House Script + id: 166 + area: 47 + coordinates: [12, 18] + teleporter: [11, 8] +- name: Reuben House - Main Entrance + id: 167 + area: 48 + coordinates: [33, 46] + teleporter: [98, 3] +- name: GrenadeMan House - Entrance Script + id: 168 + area: 49 + coordinates: [55, 60] + teleporter: [9, 8] +- name: GrenadeMan House - To Mobius Crest Room + id: 169 + area: 49 + coordinates: [57, 52] + teleporter: [93, 0] +- name: GrenadeMan Mobius Room - Stairs to House + id: 170 + area: 49 + coordinates: [39, 26] + teleporter: [94, 0] +- name: GrenadeMan Mobius Room - Mobius Teleporter Script + id: 171 + area: 49 + coordinates: [39, 23] + teleporter: [54, 8] +- name: Fireburg Vendor House - Entrance Script # No use to be a script + id: 172 + area: 49 + coordinates: [7, 10] + teleporter: [95, 0] # Original value [39, 8] +- name: Fireburg Vendor House - Stairs to Gemini Room + id: 173 + area: 49 + coordinates: [1, 4] + teleporter: [96, 0] +- name: Fireburg Gemini Room - Stairs to Vendor House + id: 174 + area: 49 + coordinates: [4, 39] + teleporter: [97, 0] +- name: Fireburg Gemini Room - Gemini Teleporter Script + id: 175 + area: 49 + coordinates: [2, 40] + teleporter: [45, 8] +- name: Fireburg Hotel Lobby - Stairs to beds + id: 176 + area: 49 + coordinates: [4, 50] + teleporter: [213, 0] +- name: Fireburg Hotel Lobby - Entrance + id: 177 + area: 49 + coordinates: [17, 56] + teleporter: [99, 3] +- name: Fireburg Hotel Beds - Stairs to Hotel Lobby + id: 178 + area: 49 + coordinates: [45, 59] + teleporter: [214, 0] +- name: Mine Exterior - Main Entrance + id: 179 + area: 50 + coordinates: [5, 28] + teleporter: [98, 0] +- name: Mine Exterior - To Cliff + id: 180 + area: 50 + coordinates: [58, 29] + teleporter: [99, 0] +- name: Mine Exterior - To Parallel Room + id: 181 + area: 50 + coordinates: [8, 7] + teleporter: [20, 2] +- name: Mine Exterior - To Crescent Room + id: 182 + area: 50 + coordinates: [26, 15] + teleporter: [21, 2] +- name: Mine Exterior - To Climbing Room + id: 183 + area: 50 + coordinates: [21, 35] + teleporter: [22, 2] +- name: Mine Exterior - Jinn Fight Script + id: 184 + area: 50 + coordinates: [58, 31] + teleporter: [74, 8] +- name: Mine Parallel Room - To Mine Exterior + id: 185 + area: 51 + coordinates: [7, 60] + teleporter: [100, 3] +- name: Mine Crescent Room - To Mine Exterior + id: 186 + area: 51 + coordinates: [22, 61] + teleporter: [101, 3] +- name: Mine Climbing Room - To Mine Exterior + id: 187 + area: 51 + coordinates: [56, 21] + teleporter: [102, 3] +- name: Mine Cliff - Entrance + id: 188 + area: 52 + coordinates: [9, 5] + teleporter: [100, 0] +- name: Mine Cliff - Reuben Grenade Script + id: 189 + area: 52 + coordinates: [15, 7] + teleporter: [12, 8] +- name: Sealed Temple - To Overworld + id: 190 + area: 53 + coordinates: [58, 43] + teleporter: [16, 6] +- name: Sealed Temple - Gemini Tile Script + id: 191 + area: 53 + coordinates: [56, 38] + teleporter: [63, 8] +- name: Volcano Base - Main Entrance 1 + id: 192 + area: 54 + coordinates: [23, 25] + teleporter: [103, 0] +- name: Volcano Base - Main Entrance 2 + id: 193 + area: 54 + coordinates: [23, 26] + teleporter: [103, 0] +- name: Volcano Base - Main Entrance 3 + id: 194 + area: 54 + coordinates: [24, 25] + teleporter: [103, 0] +- name: Volcano Base - Main Entrance 4 + id: 195 + area: 54 + coordinates: [24, 26] + teleporter: [103, 0] +- name: Volcano Base - Left Stairs Script + id: 196 + area: 54 + coordinates: [20, 5] + teleporter: [31, 8] +- name: Volcano Base - Right Stairs Script + id: 197 + area: 54 + coordinates: [32, 5] + teleporter: [30, 8] +- name: Volcano Top Right - Top Exit + id: 198 + area: 55 + coordinates: [44, 8] + teleporter: [9, 0] # Original value [103, 0] changed to volcano escape so floor shuffling doesn't pick it up +- name: Volcano Top Left - To Right-Left Path Script + id: 199 + area: 55 + coordinates: [40, 24] + teleporter: [26, 8] +- name: Volcano Top Right - To Left-Right Path Script + id: 200 + area: 55 + coordinates: [52, 24] + teleporter: [79, 8] # Original Value [26, 8] +- name: Volcano Right Path - To Volcano Base Script + id: 201 + area: 56 + coordinates: [48, 42] + teleporter: [15, 8] # Original Value [27, 8] +- name: Volcano Left Path - To Volcano Cross Left-Right + id: 202 + area: 56 + coordinates: [40, 31] + teleporter: [25, 2] +- name: Volcano Left Path - To Volcano Cross Right-Left + id: 203 + area: 56 + coordinates: [52, 29] + teleporter: [26, 2] +- name: Volcano Left Path - To Volcano Base Script + id: 204 + area: 56 + coordinates: [36, 42] + teleporter: [27, 8] +- name: Volcano Cross Left-Right - To Volcano Left Path + id: 205 + area: 56 + coordinates: [10, 42] + teleporter: [103, 3] +- name: Volcano Cross Left-Right - To Volcano Top Right Script + id: 206 + area: 56 + coordinates: [16, 24] + teleporter: [29, 8] +- name: Volcano Cross Right-Left - To Volcano Top Left Script + id: 207 + area: 56 + coordinates: [8, 22] + teleporter: [28, 8] +- name: Volcano Cross Right-Left - To Volcano Left Path + id: 208 + area: 56 + coordinates: [16, 42] + teleporter: [104, 3] +- name: Lava Dome Inner Ring Main Loop - Main Entrance 1 + id: 209 + area: 57 + coordinates: [32, 5] + teleporter: [104, 0] +- name: Lava Dome Inner Ring Main Loop - Main Entrance 2 + id: 210 + area: 57 + coordinates: [33, 5] + teleporter: [104, 0] +- name: Lava Dome Inner Ring Main Loop - To Three Steps Room + id: 211 + area: 57 + coordinates: [14, 5] + teleporter: [105, 0] +- name: Lava Dome Inner Ring Main Loop - To Life Chest Room Lower + id: 212 + area: 57 + coordinates: [40, 17] + teleporter: [106, 0] +- name: Lava Dome Inner Ring Main Loop - To Big Jump Room Left + id: 213 + area: 57 + coordinates: [8, 11] + teleporter: [108, 0] +- name: Lava Dome Inner Ring Main Loop - To Split Corridor Room + id: 214 + area: 57 + coordinates: [11, 19] + teleporter: [111, 0] +- name: Lava Dome Inner Ring Center Ledge - To Life Chest Room Higher + id: 215 + area: 57 + coordinates: [32, 11] + teleporter: [107, 0] +- name: Lava Dome Inner Ring Plate Ledge - To Plate Corridor + id: 216 + area: 57 + coordinates: [12, 23] + teleporter: [109, 0] +- name: Lava Dome Inner Ring Plate Ledge - Plate Script + id: 217 + area: 57 + coordinates: [5, 23] + teleporter: [47, 8] +- name: Lava Dome Inner Ring Upper Ledges - To Pointless Room + id: 218 + area: 57 + coordinates: [0, 9] + teleporter: [110, 0] +- name: Lava Dome Inner Ring Upper Ledges - To Lower Moon Helm Room + id: 219 + area: 57 + coordinates: [0, 15] + teleporter: [112, 0] +- name: Lava Dome Inner Ring Upper Ledges - To Up-Down Corridor + id: 220 + area: 57 + coordinates: [54, 5] + teleporter: [113, 0] +- name: Lava Dome Inner Ring Big Door Ledge - To Jumping Maze II + id: 221 + area: 57 + coordinates: [54, 21] + teleporter: [114, 0] +- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 1 + id: 222 + area: 57 + coordinates: [62, 20] + teleporter: [29, 2] +- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 2 + id: 223 + area: 57 + coordinates: [63, 20] + teleporter: [29, 2] +- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 3 + id: 224 + area: 57 + coordinates: [62, 21] + teleporter: [29, 2] +- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 4 + id: 225 + area: 57 + coordinates: [63, 21] + teleporter: [29, 2] +- name: Lava Dome Inner Ring Tiny Bottom Ledge - To Four Boxes Corridor + id: 226 + area: 57 + coordinates: [50, 25] + teleporter: [115, 0] +- name: Lava Dome Jump Maze II - Lower Right Entrance + id: 227 + area: 58 + coordinates: [55, 28] + teleporter: [116, 0] +- name: Lava Dome Jump Maze II - Upper Entrance + id: 228 + area: 58 + coordinates: [35, 3] + teleporter: [119, 0] +- name: Lava Dome Jump Maze II - Lower Left Entrance + id: 229 + area: 58 + coordinates: [34, 27] + teleporter: [120, 0] +- name: Lava Dome Up-Down Corridor - Upper Entrance + id: 230 + area: 58 + coordinates: [29, 8] + teleporter: [117, 0] +- name: Lava Dome Up-Down Corridor - Lower Entrance + id: 231 + area: 58 + coordinates: [28, 25] + teleporter: [118, 0] +- name: Lava Dome Jump Maze I - South Entrance + id: 232 + area: 59 + coordinates: [20, 27] + teleporter: [121, 0] +- name: Lava Dome Jump Maze I - North Entrance + id: 233 + area: 59 + coordinates: [7, 3] + teleporter: [122, 0] +- name: Lava Dome Pointless Room - Entrance + id: 234 + area: 60 + coordinates: [2, 7] + teleporter: [123, 0] +- name: Lava Dome Lower Moon Helm Room - Left Entrance + id: 235 + area: 60 + coordinates: [2, 19] + teleporter: [124, 0] +- name: Lava Dome Lower Moon Helm Room - Right Entrance + id: 236 + area: 60 + coordinates: [11, 21] + teleporter: [125, 0] +- name: Lava Dome Moon Helm Room - Entrance + id: 237 + area: 60 + coordinates: [15, 23] + teleporter: [126, 0] +- name: Lava Dome Three Jumps Room - To Main Loop + id: 238 + area: 61 + coordinates: [58, 15] + teleporter: [127, 0] +- name: Lava Dome Life Chest Room - Lower South Entrance + id: 239 + area: 61 + coordinates: [38, 27] + teleporter: [128, 0] +- name: Lava Dome Life Chest Room - Upper South Entrance + id: 240 + area: 61 + coordinates: [28, 23] + teleporter: [129, 0] +- name: Lava Dome Big Jump Room - Left Entrance + id: 241 + area: 62 + coordinates: [42, 51] + teleporter: [133, 0] +- name: Lava Dome Big Jump Room - North Entrance + id: 242 + area: 62 + coordinates: [30, 29] + teleporter: [131, 0] +- name: Lava Dome Big Jump Room - Lower Right Stairs + id: 243 + area: 62 + coordinates: [61, 59] + teleporter: [132, 0] +- name: Lava Dome Split Corridor - Upper Stairs + id: 244 + area: 62 + coordinates: [30, 43] + teleporter: [130, 0] +- name: Lava Dome Split Corridor - Lower Stairs + id: 245 + area: 62 + coordinates: [36, 61] + teleporter: [134, 0] +- name: Lava Dome Plate Corridor - Right Entrance + id: 246 + area: 63 + coordinates: [19, 29] + teleporter: [135, 0] +- name: Lava Dome Plate Corridor - Left Entrance + id: 247 + area: 63 + coordinates: [60, 21] + teleporter: [137, 0] +- name: Lava Dome Four Boxes Stairs - Upper Entrance + id: 248 + area: 63 + coordinates: [22, 3] + teleporter: [136, 0] +- name: Lava Dome Four Boxes Stairs - Lower Entrance + id: 249 + area: 63 + coordinates: [22, 17] + teleporter: [16, 0] +- name: Lava Dome Hydra Room - South Entrance + id: 250 + area: 64 + coordinates: [14, 59] + teleporter: [105, 3] +- name: Lava Dome Hydra Room - North Exit + id: 251 + area: 64 + coordinates: [25, 31] + teleporter: [138, 0] +- name: Lava Dome Hydra Room - Hydra Script + id: 252 + area: 64 + coordinates: [14, 36] + teleporter: [14, 8] +- name: Lava Dome Escape Corridor - South Entrance + id: 253 + area: 65 + coordinates: [22, 17] + teleporter: [139, 0] +- name: Lava Dome Escape Corridor - North Entrance + id: 254 + area: 65 + coordinates: [22, 3] + teleporter: [9, 0] +- name: Rope Bridge - West Entrance 1 + id: 255 + area: 66 + coordinates: [3, 10] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 2 + id: 256 + area: 66 + coordinates: [3, 11] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 3 + id: 257 + area: 66 + coordinates: [3, 12] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 4 + id: 258 + area: 66 + coordinates: [3, 13] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 5 + id: 259 + area: 66 + coordinates: [4, 10] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 6 + id: 260 + area: 66 + coordinates: [4, 11] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 7 + id: 261 + area: 66 + coordinates: [4, 12] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 8 + id: 262 + area: 66 + coordinates: [4, 13] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 1 + id: 263 + area: 66 + coordinates: [59, 10] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 2 + id: 264 + area: 66 + coordinates: [59, 11] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 3 + id: 265 + area: 66 + coordinates: [59, 12] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 4 + id: 266 + area: 66 + coordinates: [59, 13] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 5 + id: 267 + area: 66 + coordinates: [60, 10] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 6 + id: 268 + area: 66 + coordinates: [60, 11] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 7 + id: 269 + area: 66 + coordinates: [60, 12] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 8 + id: 270 + area: 66 + coordinates: [60, 13] + teleporter: [140, 0] +- name: Rope Bridge - Reuben Fall Script + id: 271 + area: 66 + coordinates: [13, 12] + teleporter: [15, 8] +- name: Alive Forest - West Entrance 1 + id: 272 + area: 67 + coordinates: [8, 13] + teleporter: [142, 0] +- name: Alive Forest - West Entrance 2 + id: 273 + area: 67 + coordinates: [9, 13] + teleporter: [142, 0] +- name: Alive Forest - Giant Tree Entrance + id: 274 + area: 67 + coordinates: [42, 42] + teleporter: [143, 0] +- name: Alive Forest - Libra Teleporter Script + id: 275 + area: 67 + coordinates: [8, 52] + teleporter: [64, 8] +- name: Alive Forest - Gemini Teleporter Script + id: 276 + area: 67 + coordinates: [57, 49] + teleporter: [65, 8] +- name: Alive Forest - Mobius Teleporter Script + id: 277 + area: 67 + coordinates: [24, 10] + teleporter: [66, 8] +- name: Giant Tree 1F - Entrance Script 1 + id: 278 + area: 68 + coordinates: [18, 31] + teleporter: [56, 1] # The script is restored if no map shuffling [49, 8] +- name: Giant Tree 1F - Entrance Script 2 + id: 279 + area: 68 + coordinates: [19, 31] + teleporter: [56, 1] # Same [49, 8] +- name: Giant Tree 1F - North Entrance To 2F + id: 280 + area: 68 + coordinates: [16, 1] + teleporter: [144, 0] +- name: Giant Tree 2F Main Lobby - North Entrance to 1F + id: 281 + area: 69 + coordinates: [44, 33] + teleporter: [145, 0] +- name: Giant Tree 2F Main Lobby - Central Entrance to 3F + id: 282 + area: 69 + coordinates: [42, 47] + teleporter: [146, 0] +- name: Giant Tree 2F Main Lobby - West Entrance to Mushroom Room + id: 283 + area: 69 + coordinates: [58, 49] + teleporter: [149, 0] +- name: Giant Tree 2F West Ledge - To 3F Northwest Ledge + id: 284 + area: 69 + coordinates: [34, 37] + teleporter: [147, 0] +- name: Giant Tree 2F Fall From Vine Script + id: 482 + area: 69 + coordinates: [0x2E, 0x33] + teleporter: [76, 8] +- name: Giant Tree Meteor Chest Room - To 2F Mushroom Room + id: 285 + area: 69 + coordinates: [58, 44] + teleporter: [148, 0] +- name: Giant Tree 2F Mushroom Room - Entrance + id: 286 + area: 70 + coordinates: [55, 18] + teleporter: [150, 0] +- name: Giant Tree 2F Mushroom Room - North Face to Meteor + id: 287 + area: 70 + coordinates: [56, 7] + teleporter: [151, 0] +- name: Giant Tree 3F Central Room - Central Entrance to 2F + id: 288 + area: 71 + coordinates: [46, 53] + teleporter: [152, 0] +- name: Giant Tree 3F Central Room - East Entrance to Worm Room + id: 289 + area: 71 + coordinates: [58, 39] + teleporter: [153, 0] +- name: Giant Tree 3F Lower Corridor - Entrance from Worm Room + id: 290 + area: 71 + coordinates: [45, 39] + teleporter: [154, 0] +- name: Giant Tree 3F West Platform - Lower Entrance + id: 291 + area: 71 + coordinates: [33, 43] + teleporter: [155, 0] +- name: Giant Tree 3F West Platform - Top Entrance + id: 292 + area: 71 + coordinates: [52, 25] + teleporter: [156, 0] +- name: Giant Tree Worm Room - East Entrance + id: 293 + area: 72 + coordinates: [20, 58] + teleporter: [157, 0] +- name: Giant Tree Worm Room - West Entrance + id: 294 + area: 72 + coordinates: [6, 56] + teleporter: [158, 0] +- name: Giant Tree 4F Lower Floor - Entrance + id: 295 + area: 73 + coordinates: [20, 7] + teleporter: [159, 0] +- name: Giant Tree 4F Lower Floor - Lower West Mouth + id: 296 + area: 73 + coordinates: [8, 23] + teleporter: [160, 0] +- name: Giant Tree 4F Lower Floor - Lower Central Mouth + id: 297 + area: 73 + coordinates: [14, 25] + teleporter: [161, 0] +- name: Giant Tree 4F Lower Floor - Lower East Mouth + id: 298 + area: 73 + coordinates: [20, 25] + teleporter: [162, 0] +- name: Giant Tree 4F Upper Floor - Upper West Mouth + id: 299 + area: 73 + coordinates: [8, 19] + teleporter: [163, 0] +- name: Giant Tree 4F Upper Floor - Upper Central Mouth + id: 300 + area: 73 + coordinates: [12, 17] + teleporter: [164, 0] +- name: Giant Tree 4F Slime Room - Exit + id: 301 + area: 74 + coordinates: [47, 10] + teleporter: [165, 0] +- name: Giant Tree 4F Slime Room - West Entrance + id: 302 + area: 74 + coordinates: [45, 24] + teleporter: [166, 0] +- name: Giant Tree 4F Slime Room - Central Entrance + id: 303 + area: 74 + coordinates: [50, 24] + teleporter: [167, 0] +- name: Giant Tree 4F Slime Room - East Entrance + id: 304 + area: 74 + coordinates: [57, 28] + teleporter: [168, 0] +- name: Giant Tree 5F - Entrance + id: 305 + area: 75 + coordinates: [14, 51] + teleporter: [169, 0] +- name: Giant Tree 5F - Giant Tree Face # Unused + id: 306 + area: 75 + coordinates: [14, 37] + teleporter: [170, 0] +- name: Kaidge Temple - Entrance + id: 307 + area: 77 + coordinates: [44, 63] + teleporter: [18, 6] +- name: Kaidge Temple - Mobius Teleporter Script + id: 308 + area: 77 + coordinates: [35, 57] + teleporter: [71, 8] +- name: Windhole Temple - Entrance + id: 309 + area: 78 + coordinates: [10, 29] + teleporter: [173, 0] +- name: Mount Gale - Entrance 1 + id: 310 + area: 79 + coordinates: [1, 45] + teleporter: [174, 0] +- name: Mount Gale - Entrance 2 + id: 311 + area: 79 + coordinates: [2, 45] + teleporter: [174, 0] +- name: Windia - Main Entrance 1 + id: 312 + area: 80 + coordinates: [12, 40] + teleporter: [10, 6] +- name: Windia - Main Entrance 2 + id: 313 + area: 80 + coordinates: [13, 40] + teleporter: [10, 6] +- name: Windia - Main Entrance 3 + id: 314 + area: 80 + coordinates: [14, 40] + teleporter: [10, 6] +- name: Windia - Main Entrance 4 + id: 315 + area: 80 + coordinates: [15, 40] + teleporter: [10, 6] +- name: Windia - Main Entrance 5 + id: 316 + area: 80 + coordinates: [12, 41] + teleporter: [10, 6] +- name: Windia - Main Entrance 6 + id: 317 + area: 80 + coordinates: [13, 41] + teleporter: [10, 6] +- name: Windia - Main Entrance 7 + id: 318 + area: 80 + coordinates: [14, 41] + teleporter: [10, 6] +- name: Windia - Main Entrance 8 + id: 319 + area: 80 + coordinates: [15, 41] + teleporter: [10, 6] +- name: Windia - Otto's House + id: 320 + area: 80 + coordinates: [21, 39] + teleporter: [30, 5] +- name: Windia - INN's Script # Change to teleporter + id: 321 + area: 80 + coordinates: [18, 34] + teleporter: [31, 2] # Original value [79, 8] +- name: Windia - Vendor House + id: 322 + area: 80 + coordinates: [8, 36] + teleporter: [32, 5] +- name: Windia - Kid House + id: 323 + area: 80 + coordinates: [7, 23] + teleporter: [176, 4] +- name: Windia - Old People House + id: 324 + area: 80 + coordinates: [19, 21] + teleporter: [177, 4] +- name: Windia - Rainbow Bridge Script + id: 325 + area: 80 + coordinates: [21, 9] + teleporter: [10, 6] # Change to entrance, usually a script [41, 8] +- name: Otto's House - Attic Stairs + id: 326 + area: 81 + coordinates: [2, 19] + teleporter: [33, 2] +- name: Otto's House - Entrance + id: 327 + area: 81 + coordinates: [9, 30] + teleporter: [106, 3] +- name: Otto's Attic - Stairs + id: 328 + area: 81 + coordinates: [26, 23] + teleporter: [107, 3] +- name: Windia Kid House - Entrance Script # Change to teleporter + id: 329 + area: 82 + coordinates: [7, 10] + teleporter: [178, 0] # Original value [38, 8] +- name: Windia Kid House - Basement Stairs + id: 330 + area: 82 + coordinates: [1, 4] + teleporter: [180, 0] +- name: Windia Old People House - Entrance + id: 331 + area: 82 + coordinates: [55, 12] + teleporter: [179, 0] +- name: Windia Old People House - Basement Stairs + id: 332 + area: 82 + coordinates: [60, 5] + teleporter: [181, 0] +- name: Windia Kid House Basement - Stairs + id: 333 + area: 82 + coordinates: [43, 8] + teleporter: [182, 0] +- name: Windia Kid House Basement - Mobius Teleporter + id: 334 + area: 82 + coordinates: [41, 9] + teleporter: [44, 8] +- name: Windia Old People House Basement - Stairs + id: 335 + area: 82 + coordinates: [39, 26] + teleporter: [183, 0] +- name: Windia Old People House Basement - Mobius Teleporter Script + id: 336 + area: 82 + coordinates: [39, 23] + teleporter: [43, 8] +- name: Windia Inn Lobby - Stairs to Beds + id: 337 + area: 82 + coordinates: [45, 24] + teleporter: [215, 0] +- name: Windia Inn Lobby - Exit + id: 338 + area: 82 + coordinates: [53, 30] + teleporter: [135, 3] +- name: Windia Inn Beds - Stairs to Lobby + id: 339 + area: 82 + coordinates: [33, 59] + teleporter: [216, 0] +- name: Windia Vendor House - Entrance + id: 340 + area: 82 + coordinates: [29, 14] + teleporter: [108, 3] +- name: Pazuzu Tower 1F Main Lobby - Main Entrance 1 + id: 341 + area: 83 + coordinates: [47, 29] + teleporter: [184, 0] +- name: Pazuzu Tower 1F Main Lobby - Main Entrance 2 + id: 342 + area: 83 + coordinates: [47, 30] + teleporter: [184, 0] +- name: Pazuzu Tower 1F Main Lobby - Main Entrance 3 + id: 343 + area: 83 + coordinates: [48, 29] + teleporter: [184, 0] +- name: Pazuzu Tower 1F Main Lobby - Main Entrance 4 + id: 344 + area: 83 + coordinates: [48, 30] + teleporter: [184, 0] +- name: Pazuzu Tower 1F Main Lobby - East Entrance + id: 345 + area: 83 + coordinates: [55, 12] + teleporter: [185, 0] +- name: Pazuzu Tower 1F Main Lobby - South Stairs + id: 346 + area: 83 + coordinates: [51, 25] + teleporter: [186, 0] +- name: Pazuzu Tower 1F Main Lobby - Pazuzu Script 1 + id: 347 + area: 83 + coordinates: [47, 8] + teleporter: [16, 8] +- name: Pazuzu Tower 1F Main Lobby - Pazuzu Script 2 + id: 348 + area: 83 + coordinates: [48, 8] + teleporter: [16, 8] +- name: Pazuzu Tower 1F Boxes Room - West Stairs + id: 349 + area: 83 + coordinates: [38, 17] + teleporter: [187, 0] +- name: Pazuzu 2F - West Upper Stairs + id: 350 + area: 84 + coordinates: [7, 11] + teleporter: [188, 0] +- name: Pazuzu 2F - South Stairs + id: 351 + area: 84 + coordinates: [20, 24] + teleporter: [189, 0] +- name: Pazuzu 2F - West Lower Stairs + id: 352 + area: 84 + coordinates: [6, 17] + teleporter: [190, 0] +- name: Pazuzu 2F - Central Stairs + id: 353 + area: 84 + coordinates: [15, 15] + teleporter: [191, 0] +- name: Pazuzu 2F - Pazuzu 1 + id: 354 + area: 84 + coordinates: [15, 8] + teleporter: [17, 8] +- name: Pazuzu 2F - Pazuzu 2 + id: 355 + area: 84 + coordinates: [16, 8] + teleporter: [17, 8] +- name: Pazuzu 3F Main Room - North Stairs + id: 356 + area: 85 + coordinates: [23, 11] + teleporter: [192, 0] +- name: Pazuzu 3F Main Room - West Stairs + id: 357 + area: 85 + coordinates: [7, 15] + teleporter: [193, 0] +- name: Pazuzu 3F Main Room - Pazuzu Script 1 + id: 358 + area: 85 + coordinates: [15, 8] + teleporter: [18, 8] +- name: Pazuzu 3F Main Room - Pazuzu Script 2 + id: 359 + area: 85 + coordinates: [16, 8] + teleporter: [18, 8] +- name: Pazuzu 3F Central Island - Central Stairs + id: 360 + area: 85 + coordinates: [15, 14] + teleporter: [194, 0] +- name: Pazuzu 3F Central Island - South Stairs + id: 361 + area: 85 + coordinates: [17, 25] + teleporter: [195, 0] +- name: Pazuzu 4F - Northwest Stairs + id: 362 + area: 86 + coordinates: [39, 12] + teleporter: [196, 0] +- name: Pazuzu 4F - Southwest Stairs + id: 363 + area: 86 + coordinates: [39, 19] + teleporter: [197, 0] +- name: Pazuzu 4F - South Stairs + id: 364 + area: 86 + coordinates: [47, 24] + teleporter: [198, 0] +- name: Pazuzu 4F - Northeast Stairs + id: 365 + area: 86 + coordinates: [54, 9] + teleporter: [199, 0] +- name: Pazuzu 4F - Pazuzu Script 1 + id: 366 + area: 86 + coordinates: [47, 8] + teleporter: [19, 8] +- name: Pazuzu 4F - Pazuzu Script 2 + id: 367 + area: 86 + coordinates: [48, 8] + teleporter: [19, 8] +- name: Pazuzu 5F Pazuzu Loop - West Stairs + id: 368 + area: 87 + coordinates: [9, 49] + teleporter: [200, 0] +- name: Pazuzu 5F Pazuzu Loop - South Stairs + id: 369 + area: 87 + coordinates: [16, 55] + teleporter: [201, 0] +- name: Pazuzu 5F Upper Loop - Northeast Stairs + id: 370 + area: 87 + coordinates: [22, 40] + teleporter: [202, 0] +- name: Pazuzu 5F Upper Loop - Northwest Stairs + id: 371 + area: 87 + coordinates: [9, 40] + teleporter: [203, 0] +- name: Pazuzu 5F Upper Loop - Pazuzu Script 1 + id: 372 + area: 87 + coordinates: [15, 40] + teleporter: [20, 8] +- name: Pazuzu 5F Upper Loop - Pazuzu Script 2 + id: 373 + area: 87 + coordinates: [16, 40] + teleporter: [20, 8] +- name: Pazuzu 6F - West Stairs + id: 374 + area: 88 + coordinates: [41, 47] + teleporter: [204, 0] +- name: Pazuzu 6F - Northwest Stairs + id: 375 + area: 88 + coordinates: [41, 40] + teleporter: [205, 0] +- name: Pazuzu 6F - Northeast Stairs + id: 376 + area: 88 + coordinates: [54, 40] + teleporter: [206, 0] +- name: Pazuzu 6F - South Stairs + id: 377 + area: 88 + coordinates: [52, 56] + teleporter: [207, 0] +- name: Pazuzu 6F - Pazuzu Script 1 + id: 378 + area: 88 + coordinates: [47, 40] + teleporter: [21, 8] +- name: Pazuzu 6F - Pazuzu Script 2 + id: 379 + area: 88 + coordinates: [48, 40] + teleporter: [21, 8] +- name: Pazuzu 7F Main Room - Southwest Stairs + id: 380 + area: 89 + coordinates: [15, 54] + teleporter: [26, 0] +- name: Pazuzu 7F Main Room - Northeast Stairs + id: 381 + area: 89 + coordinates: [21, 40] + teleporter: [27, 0] +- name: Pazuzu 7F Main Room - Southeast Stairs + id: 382 + area: 89 + coordinates: [21, 56] + teleporter: [28, 0] +- name: Pazuzu 7F Main Room - Pazuzu Script 1 + id: 383 + area: 89 + coordinates: [15, 44] + teleporter: [22, 8] +- name: Pazuzu 7F Main Room - Pazuzu Script 2 + id: 384 + area: 89 + coordinates: [16, 44] + teleporter: [22, 8] +- name: Pazuzu 7F Main Room - Crystal Script # Added for floor shuffle + id: 480 + area: 89 + coordinates: [15, 40] + teleporter: [38, 8] +- name: Pazuzu 1F to 3F - South Stairs + id: 385 + area: 90 + coordinates: [43, 60] + teleporter: [29, 0] +- name: Pazuzu 1F to 3F - North Stairs + id: 386 + area: 90 + coordinates: [43, 36] + teleporter: [30, 0] +- name: Pazuzu 3F to 5F - South Stairs + id: 387 + area: 91 + coordinates: [43, 60] + teleporter: [40, 0] +- name: Pazuzu 3F to 5F - North Stairs + id: 388 + area: 91 + coordinates: [43, 36] + teleporter: [41, 0] +- name: Pazuzu 5F to 7F - South Stairs + id: 389 + area: 92 + coordinates: [43, 60] + teleporter: [38, 0] +- name: Pazuzu 5F to 7F - North Stairs + id: 390 + area: 92 + coordinates: [43, 36] + teleporter: [39, 0] +- name: Pazuzu 2F to 4F - South Stairs + id: 391 + area: 93 + coordinates: [43, 60] + teleporter: [21, 0] +- name: Pazuzu 2F to 4F - North Stairs + id: 392 + area: 93 + coordinates: [43, 36] + teleporter: [22, 0] +- name: Pazuzu 4F to 6F - South Stairs + id: 393 + area: 94 + coordinates: [43, 60] + teleporter: [2, 0] +- name: Pazuzu 4F to 6F - North Stairs + id: 394 + area: 94 + coordinates: [43, 36] + teleporter: [3, 0] +- name: Light Temple - Entrance + id: 395 + area: 95 + coordinates: [28, 57] + teleporter: [19, 6] +- name: Light Temple - Mobius Teleporter Script + id: 396 + area: 95 + coordinates: [29, 37] + teleporter: [70, 8] +- name: Ship Dock - Mobius Teleporter Script + id: 397 + area: 96 + coordinates: [15, 18] + teleporter: [61, 8] +- name: Ship Dock - From Overworld + id: 398 + area: 96 + coordinates: [15, 11] + teleporter: [73, 0] +- name: Ship Dock - Entrance + id: 399 + area: 96 + coordinates: [15, 23] + teleporter: [17, 6] +- name: Mac Ship Deck - East Entrance Script + id: 400 + area: 97 + coordinates: [26, 40] + teleporter: [37, 8] +- name: Mac Ship Deck - Central Stairs Script + id: 401 + area: 97 + coordinates: [16, 47] + teleporter: [50, 8] +- name: Mac Ship Deck - West Stairs Script + id: 402 + area: 97 + coordinates: [8, 34] + teleporter: [51, 8] +- name: Mac Ship Deck - East Stairs Script + id: 403 + area: 97 + coordinates: [24, 36] + teleporter: [52, 8] +- name: Mac Ship Deck - North Stairs Script + id: 404 + area: 97 + coordinates: [12, 9] + teleporter: [53, 8] +- name: Mac Ship B1 Outer Ring - South Stairs + id: 405 + area: 98 + coordinates: [16, 45] + teleporter: [208, 0] +- name: Mac Ship B1 Outer Ring - West Stairs + id: 406 + area: 98 + coordinates: [8, 35] + teleporter: [175, 0] +- name: Mac Ship B1 Outer Ring - East Stairs + id: 407 + area: 98 + coordinates: [25, 37] + teleporter: [172, 0] +- name: Mac Ship B1 Outer Ring - Northwest Stairs + id: 408 + area: 98 + coordinates: [10, 23] + teleporter: [88, 0] +- name: Mac Ship B1 Square Room - North Stairs + id: 409 + area: 98 + coordinates: [14, 9] + teleporter: [141, 0] +- name: Mac Ship B1 Square Room - South Stairs + id: 410 + area: 98 + coordinates: [16, 12] + teleporter: [87, 0] +- name: Mac Ship B1 Mac Room - Stairs # Unused? + id: 411 + area: 98 + coordinates: [16, 51] + teleporter: [101, 0] +- name: Mac Ship B1 Central Corridor - South Stairs + id: 412 + area: 98 + coordinates: [16, 38] + teleporter: [102, 0] +- name: Mac Ship B1 Central Corridor - North Stairs + id: 413 + area: 98 + coordinates: [16, 26] + teleporter: [86, 0] +- name: Mac Ship B2 South Corridor - South Stairs + id: 414 + area: 99 + coordinates: [48, 51] + teleporter: [57, 1] +- name: Mac Ship B2 South Corridor - North Stairs Script + id: 415 + area: 99 + coordinates: [48, 38] + teleporter: [55, 8] +- name: Mac Ship B2 North Corridor - South Stairs Script + id: 416 + area: 99 + coordinates: [48, 27] + teleporter: [56, 8] +- name: Mac Ship B2 North Corridor - North Stairs Script + id: 417 + area: 99 + coordinates: [48, 12] + teleporter: [57, 8] +- name: Mac Ship B2 Outer Ring - Northwest Stairs Script + id: 418 + area: 99 + coordinates: [55, 11] + teleporter: [58, 8] +- name: Mac Ship B1 Outer Ring Cleared - South Stairs + id: 419 + area: 100 + coordinates: [16, 45] + teleporter: [208, 0] +- name: Mac Ship B1 Outer Ring Cleared - West Stairs + id: 420 + area: 100 + coordinates: [8, 35] + teleporter: [175, 0] +- name: Mac Ship B1 Outer Ring Cleared - East Stairs + id: 421 + area: 100 + coordinates: [25, 37] + teleporter: [172, 0] +- name: Mac Ship B1 Square Room Cleared - North Stairs + id: 422 + area: 100 + coordinates: [14, 9] + teleporter: [141, 0] +- name: Mac Ship B1 Square Room Cleared - South Stairs + id: 423 + area: 100 + coordinates: [16, 12] + teleporter: [87, 0] +- name: Mac Ship B1 Mac Room Cleared - Main Stairs + id: 424 + area: 100 + coordinates: [16, 51] + teleporter: [101, 0] +- name: Mac Ship B1 Central Corridor Cleared - South Stairs + id: 425 + area: 100 + coordinates: [16, 38] + teleporter: [102, 0] +- name: Mac Ship B1 Central Corridor Cleared - North Stairs + id: 426 + area: 100 + coordinates: [16, 26] + teleporter: [86, 0] +- name: Mac Ship B1 Central Corridor Cleared - Northwest Stairs + id: 427 + area: 100 + coordinates: [23, 10] + teleporter: [88, 0] +- name: Doom Castle Corridor of Destiny - South Entrance + id: 428 + area: 101 + coordinates: [59, 29] + teleporter: [84, 0] +- name: Doom Castle Corridor of Destiny - Ice Floor Entrance + id: 429 + area: 101 + coordinates: [59, 21] + teleporter: [35, 2] +- name: Doom Castle Corridor of Destiny - Lava Floor Entrance + id: 430 + area: 101 + coordinates: [59, 13] + teleporter: [209, 0] +- name: Doom Castle Corridor of Destiny - Sky Floor Entrance + id: 431 + area: 101 + coordinates: [59, 5] + teleporter: [211, 0] +- name: Doom Castle Corridor of Destiny - Hero Room Entrance + id: 432 + area: 101 + coordinates: [59, 61] + teleporter: [13, 2] +- name: Doom Castle Ice Floor - Entrance + id: 433 + area: 102 + coordinates: [23, 42] + teleporter: [109, 3] +- name: Doom Castle Lava Floor - Entrance + id: 434 + area: 103 + coordinates: [23, 40] + teleporter: [210, 0] +- name: Doom Castle Sky Floor - Entrance + id: 435 + area: 104 + coordinates: [24, 41] + teleporter: [212, 0] +- name: Doom Castle Hero Room - Dark King Entrance 1 + id: 436 + area: 106 + coordinates: [15, 5] + teleporter: [54, 0] +- name: Doom Castle Hero Room - Dark King Entrance 2 + id: 437 + area: 106 + coordinates: [16, 5] + teleporter: [54, 0] +- name: Doom Castle Hero Room - Dark King Entrance 3 + id: 438 + area: 106 + coordinates: [15, 4] + teleporter: [54, 0] +- name: Doom Castle Hero Room - Dark King Entrance 4 + id: 439 + area: 106 + coordinates: [16, 4] + teleporter: [54, 0] +- name: Doom Castle Hero Room - Hero Statue Script + id: 440 + area: 106 + coordinates: [15, 17] + teleporter: [24, 8] +- name: Doom Castle Hero Room - Entrance + id: 441 + area: 106 + coordinates: [15, 24] + teleporter: [110, 3] +- name: Doom Castle Dark King Room - Entrance + id: 442 + area: 107 + coordinates: [14, 26] + teleporter: [52, 0] +- name: Doom Castle Dark King Room - Dark King Script + id: 443 + area: 107 + coordinates: [14, 15] + teleporter: [25, 8] +- name: Doom Castle Dark King Room - Unknown + id: 444 + area: 107 + coordinates: [47, 54] + teleporter: [77, 0] +- name: Overworld - Level Forest + id: 445 + area: 0 + type: "Overworld" + teleporter: [0x2E, 8] +- name: Overworld - Foresta + id: 446 + area: 0 + type: "Overworld" + teleporter: [0x02, 1] +- name: Overworld - Sand Temple + id: 447 + area: 0 + type: "Overworld" + teleporter: [0x03, 1] +- name: Overworld - Bone Dungeon + id: 448 + area: 0 + type: "Overworld" + teleporter: [0x04, 1] +- name: Overworld - Focus Tower Foresta + id: 449 + area: 0 + type: "Overworld" + teleporter: [0x05, 1] +- name: Overworld - Focus Tower Aquaria + id: 450 + area: 0 + type: "Overworld" + teleporter: [0x13, 1] +- name: Overworld - Libra Temple + id: 451 + area: 0 + type: "Overworld" + teleporter: [0x07, 1] +- name: Overworld - Aquaria + id: 452 + area: 0 + type: "Overworld" + teleporter: [0x08, 8] +- name: Overworld - Wintry Cave + id: 453 + area: 0 + type: "Overworld" + teleporter: [0x0A, 1] +- name: Overworld - Life Temple + id: 454 + area: 0 + type: "Overworld" + teleporter: [0x0B, 1] +- name: Overworld - Falls Basin + id: 455 + area: 0 + type: "Overworld" + teleporter: [0x0C, 1] +- name: Overworld - Ice Pyramid + id: 456 + area: 0 + type: "Overworld" + teleporter: [0x0D, 1] # Will be switched to a script +- name: Overworld - Spencer's Place + id: 457 + area: 0 + type: "Overworld" + teleporter: [0x30, 8] +- name: Overworld - Wintry Temple + id: 458 + area: 0 + type: "Overworld" + teleporter: [0x10, 1] +- name: Overworld - Focus Tower Frozen Strip + id: 459 + area: 0 + type: "Overworld" + teleporter: [0x11, 1] +- name: Overworld - Focus Tower Fireburg + id: 460 + area: 0 + type: "Overworld" + teleporter: [0x12, 1] +- name: Overworld - Fireburg + id: 461 + area: 0 + type: "Overworld" + teleporter: [0x14, 1] +- name: Overworld - Mine + id: 462 + area: 0 + type: "Overworld" + teleporter: [0x15, 1] +- name: Overworld - Sealed Temple + id: 463 + area: 0 + type: "Overworld" + teleporter: [0x16, 1] +- name: Overworld - Volcano + id: 464 + area: 0 + type: "Overworld" + teleporter: [0x17, 1] +- name: Overworld - Lava Dome + id: 465 + area: 0 + type: "Overworld" + teleporter: [0x18, 1] +- name: Overworld - Focus Tower Windia + id: 466 + area: 0 + type: "Overworld" + teleporter: [0x06, 1] +- name: Overworld - Rope Bridge + id: 467 + area: 0 + type: "Overworld" + teleporter: [0x19, 1] +- name: Overworld - Alive Forest + id: 468 + area: 0 + type: "Overworld" + teleporter: [0x1A, 1] +- name: Overworld - Giant Tree + id: 469 + area: 0 + type: "Overworld" + teleporter: [0x1B, 1] +- name: Overworld - Kaidge Temple + id: 470 + area: 0 + type: "Overworld" + teleporter: [0x1C, 1] +- name: Overworld - Windia + id: 471 + area: 0 + type: "Overworld" + teleporter: [0x1D, 1] +- name: Overworld - Windhole Temple + id: 472 + area: 0 + type: "Overworld" + teleporter: [0x1E, 1] +- name: Overworld - Mount Gale + id: 473 + area: 0 + type: "Overworld" + teleporter: [0x1F, 1] +- name: Overworld - Pazuzu Tower + id: 474 + area: 0 + type: "Overworld" + teleporter: [0x20, 1] +- name: Overworld - Ship Dock + id: 475 + area: 0 + type: "Overworld" + teleporter: [0x3E, 1] +- name: Overworld - Doom Castle + id: 476 + area: 0 + type: "Overworld" + teleporter: [0x21, 1] +- name: Overworld - Light Temple + id: 477 + area: 0 + type: "Overworld" + teleporter: [0x22, 1] +- name: Overworld - Mac Ship + id: 478 + area: 0 + type: "Overworld" + teleporter: [0x24, 1] +- name: Overworld - Mac Ship Doom + id: 479 + area: 0 + type: "Overworld" + teleporter: [0x24, 1] +- name: Dummy House - Bed Script + id: 480 + area: 17 + coordinates: [0x28, 0x38] + teleporter: [1, 8] +- name: Dummy House - Entrance + id: 481 + area: 17 + coordinates: [0x29, 0x3B] + teleporter: [0, 10] #None diff --git a/worlds/ffmq/data/rooms.yaml b/worlds/ffmq/data/rooms.yaml new file mode 100644 index 000000000000..4343d785eb7d --- /dev/null +++ b/worlds/ffmq/data/rooms.yaml @@ -0,0 +1,4026 @@ +- name: Overworld + id: 0 + type: "Overworld" + game_objects: [] + links: + - target_room: 220 # To Forest Subregion + access: [] +- name: Subregion Foresta + id: 220 + type: "Subregion" + region: "Foresta" + game_objects: + - name: "Foresta South Battlefield" + object_id: 0x01 + location: "ForestaSouthBattlefield" + location_slot: "ForestaSouthBattlefield" + type: "BattlefieldXp" + access: [] + - name: "Foresta West Battlefield" + object_id: 0x02 + location: "ForestaWestBattlefield" + location_slot: "ForestaWestBattlefield" + type: "BattlefieldItem" + access: [] + - name: "Foresta East Battlefield" + object_id: 0x03 + location: "ForestaEastBattlefield" + location_slot: "ForestaEastBattlefield" + type: "BattlefieldGp" + access: [] + links: + - target_room: 15 # Level Forest + location: "LevelForest" + location_slot: "LevelForest" + entrance: 445 + teleporter: [0x2E, 8] + access: [] + - target_room: 16 # Foresta + location: "Foresta" + location_slot: "Foresta" + entrance: 446 + teleporter: [0x02, 1] + access: [] + - target_room: 24 # Sand Temple + location: "SandTemple" + location_slot: "SandTemple" + entrance: 447 + teleporter: [0x03, 1] + access: [] + - target_room: 25 # Bone Dungeon + location: "BoneDungeon" + location_slot: "BoneDungeon" + entrance: 448 + teleporter: [0x04, 1] + access: [] + - target_room: 3 # Focus Tower Foresta + location: "FocusTowerForesta" + location_slot: "FocusTowerForesta" + entrance: 449 + teleporter: [0x05, 1] + access: [] + - target_room: 221 + access: ["SandCoin"] + - target_room: 224 + access: ["RiverCoin"] + - target_room: 226 + access: ["SunCoin"] +- name: Subregion Aquaria + id: 221 + type: "Subregion" + region: "Aquaria" + game_objects: + - name: "South of Libra Temple Battlefield" + object_id: 0x04 + location: "AquariaBattlefield01" + location_slot: "AquariaBattlefield01" + type: "BattlefieldXp" + access: [] + - name: "East of Libra Temple Battlefield" + object_id: 0x05 + location: "AquariaBattlefield02" + location_slot: "AquariaBattlefield02" + type: "BattlefieldGp" + access: [] + - name: "South of Aquaria Battlefield" + object_id: 0x06 + location: "AquariaBattlefield03" + location_slot: "AquariaBattlefield03" + type: "BattlefieldItem" + access: [] + - name: "South of Wintry Cave Battlefield" + object_id: 0x07 + location: "WintryBattlefield01" + location_slot: "WintryBattlefield01" + type: "BattlefieldXp" + access: [] + - name: "West of Wintry Cave Battlefield" + object_id: 0x08 + location: "WintryBattlefield02" + location_slot: "WintryBattlefield02" + type: "BattlefieldGp" + access: [] + - name: "Ice Pyramid Battlefield" + object_id: 0x09 + location: "PyramidBattlefield01" + location_slot: "PyramidBattlefield01" + type: "BattlefieldXp" + access: [] + links: + - target_room: 10 # Focus Tower Aquaria + location: "FocusTowerAquaria" + location_slot: "FocusTowerAquaria" + entrance: 450 + teleporter: [0x13, 1] + access: [] + - target_room: 39 # Libra Temple + location: "LibraTemple" + location_slot: "LibraTemple" + entrance: 451 + teleporter: [0x07, 1] + access: [] + - target_room: 40 # Aquaria + location: "Aquaria" + location_slot: "Aquaria" + entrance: 452 + teleporter: [0x08, 8] + access: [] + - target_room: 45 # Wintry Cave + location: "WintryCave" + location_slot: "WintryCave" + entrance: 453 + teleporter: [0x0A, 1] + access: [] + - target_room: 52 # Falls Basin + location: "FallsBasin" + location_slot: "FallsBasin" + entrance: 455 + teleporter: [0x0C, 1] + access: [] + - target_room: 54 # Ice Pyramid + location: "IcePyramid" + location_slot: "IcePyramid" + entrance: 456 + teleporter: [0x0D, 1] # Will be switched to a script + access: [] + - target_room: 220 + access: ["SandCoin"] + - target_room: 224 + access: ["SandCoin", "RiverCoin"] + - target_room: 226 + access: ["SandCoin", "SunCoin"] + - target_room: 223 + access: ["SummerAquaria"] +- name: Subregion Life Temple + id: 222 + type: "Subregion" + region: "LifeTemple" + game_objects: [] + links: + - target_room: 51 # Life Temple + location: "LifeTemple" + location_slot: "LifeTemple" + entrance: 454 + teleporter: [0x0B, 1] + access: [] +- name: Subregion Frozen Fields + id: 223 + type: "Subregion" + region: "AquariaFrozenField" + game_objects: + - name: "North of Libra Temple Battlefield" + object_id: 0x0A + location: "LibraBattlefield01" + location_slot: "LibraBattlefield01" + type: "BattlefieldItem" + access: [] + - name: "Aquaria Frozen Field Battlefield" + object_id: 0x0B + location: "LibraBattlefield02" + location_slot: "LibraBattlefield02" + type: "BattlefieldXp" + access: [] + links: + - target_room: 74 # Wintry Temple + location: "WintryTemple" + location_slot: "WintryTemple" + entrance: 458 + teleporter: [0x10, 1] + access: [] + - target_room: 14 # Focus Tower Frozen Strip + location: "FocusTowerFrozen" + location_slot: "FocusTowerFrozen" + entrance: 459 + teleporter: [0x11, 1] + access: [] + - target_room: 221 + access: [] + - target_room: 225 + access: ["SummerAquaria", "DualheadHydra"] +- name: Subregion Fireburg + id: 224 + type: "Subregion" + region: "Fireburg" + game_objects: + - name: "Path to Fireburg Southern Battlefield" + object_id: 0x0C + location: "FireburgBattlefield01" + location_slot: "FireburgBattlefield01" + type: "BattlefieldGp" + access: [] + - name: "Path to Fireburg Central Battlefield" + object_id: 0x0D + location: "FireburgBattlefield02" + location_slot: "FireburgBattlefield02" + type: "BattlefieldItem" + access: [] + - name: "Path to Fireburg Northern Battlefield" + object_id: 0x0E + location: "FireburgBattlefield03" + location_slot: "FireburgBattlefield03" + type: "BattlefieldXp" + access: [] + - name: "Sealed Temple Battlefield" + object_id: 0x0F + location: "MineBattlefield01" + location_slot: "MineBattlefield01" + type: "BattlefieldGp" + access: [] + - name: "Mine Battlefield" + object_id: 0x10 + location: "MineBattlefield02" + location_slot: "MineBattlefield02" + type: "BattlefieldItem" + access: [] + - name: "Boulder Battlefield" + object_id: 0x11 + location: "MineBattlefield03" + location_slot: "MineBattlefield03" + type: "BattlefieldXp" + access: [] + links: + - target_room: 13 # Focus Tower Fireburg + location: "FocusTowerFireburg" + location_slot: "FocusTowerFireburg" + entrance: 460 + teleporter: [0x12, 1] + access: [] + - target_room: 76 # Fireburg + location: "Fireburg" + location_slot: "Fireburg" + entrance: 461 + teleporter: [0x14, 1] + access: [] + - target_room: 84 # Mine + location: "Mine" + location_slot: "Mine" + entrance: 462 + teleporter: [0x15, 1] + access: [] + - target_room: 92 # Sealed Temple + location: "SealedTemple" + location_slot: "SealedTemple" + entrance: 463 + teleporter: [0x16, 1] + access: [] + - target_room: 93 # Volcano + location: "Volcano" + location_slot: "Volcano" + entrance: 464 + teleporter: [0x17, 1] # Also this one / 0x0F, 8 + access: [] + - target_room: 100 # Lava Dome + location: "LavaDome" + location_slot: "LavaDome" + entrance: 465 + teleporter: [0x18, 1] + access: [] + - target_room: 220 + access: ["RiverCoin"] + - target_room: 221 + access: ["SandCoin", "RiverCoin"] + - target_room: 226 + access: ["RiverCoin", "SunCoin"] + - target_room: 225 + access: ["DualheadHydra"] +- name: Subregion Volcano Battlefield + id: 225 + type: "Subregion" + region: "VolcanoBattlefield" + game_objects: + - name: "Volcano Battlefield" + object_id: 0x12 + location: "VolcanoBattlefield01" + location_slot: "VolcanoBattlefield01" + type: "BattlefieldXp" + access: [] + links: + - target_room: 224 + access: ["DualheadHydra"] + - target_room: 223 + access: ["SummerAquaria"] +- name: Subregion Windia + id: 226 + type: "Subregion" + region: "Windia" + game_objects: + - name: "Kaidge Temple Battlefield" + object_id: 0x13 + location: "WindiaBattlefield01" + location_slot: "WindiaBattlefield01" + type: "BattlefieldXp" + access: [] + - name: "South of Windia Battlefield" + object_id: 0x14 + location: "WindiaBattlefield02" + location_slot: "WindiaBattlefield02" + type: "BattlefieldXp" + access: [] + links: + - target_room: 9 # Focus Tower Windia + location: "FocusTowerWindia" + location_slot: "FocusTowerWindia" + entrance: 466 + teleporter: [0x06, 1] + access: [] + - target_room: 123 # Rope Bridge + location: "RopeBridge" + location_slot: "RopeBridge" + entrance: 467 + teleporter: [0x19, 1] + access: [] + - target_room: 124 # Alive Forest + location: "AliveForest" + location_slot: "AliveForest" + entrance: 468 + teleporter: [0x1A, 1] + access: [] + - target_room: 125 # Giant Tree + location: "GiantTree" + location_slot: "GiantTree" + entrance: 469 + teleporter: [0x1B, 1] + access: ["Barred"] + - target_room: 152 # Kaidge Temple + location: "KaidgeTemple" + location_slot: "KaidgeTemple" + entrance: 470 + teleporter: [0x1C, 1] + access: [] + - target_room: 156 # Windia + location: "Windia" + location_slot: "Windia" + entrance: 471 + teleporter: [0x1D, 1] + access: [] + - target_room: 154 # Windhole Temple + location: "WindholeTemple" + location_slot: "WindholeTemple" + entrance: 472 + teleporter: [0x1E, 1] + access: [] + - target_room: 155 # Mount Gale + location: "MountGale" + location_slot: "MountGale" + entrance: 473 + teleporter: [0x1F, 1] + access: [] + - target_room: 166 # Pazuzu Tower + location: "PazuzusTower" + location_slot: "PazuzusTower" + entrance: 474 + teleporter: [0x20, 1] + access: [] + - target_room: 220 + access: ["SunCoin"] + - target_room: 221 + access: ["SandCoin", "SunCoin"] + - target_room: 224 + access: ["RiverCoin", "SunCoin"] + - target_room: 227 + access: ["RainbowBridge"] +- name: Subregion Spencer's Cave + id: 227 + type: "Subregion" + region: "SpencerCave" + game_objects: [] + links: + - target_room: 73 # Spencer's Place + location: "SpencersPlace" + location_slot: "SpencersPlace" + entrance: 457 + teleporter: [0x30, 8] + access: [] + - target_room: 226 + access: ["RainbowBridge"] +- name: Subregion Ship Dock + id: 228 + type: "Subregion" + region: "ShipDock" + game_objects: [] + links: + - target_room: 186 # Ship Dock + location: "ShipDock" + location_slot: "ShipDock" + entrance: 475 + teleporter: [0x3E, 1] + access: [] + - target_room: 229 + access: ["ShipLiberated", "ShipDockAccess"] +- name: Subregion Mac's Ship + id: 229 + type: "Subregion" + region: "MacShip" + game_objects: [] + links: + - target_room: 187 # Mac Ship + location: "MacsShip" + location_slot: "MacsShip" + entrance: 478 + teleporter: [0x24, 1] + access: [] + - target_room: 228 + access: ["ShipLiberated", "ShipDockAccess"] + - target_room: 231 + access: ["ShipLoaned", "ShipDockAccess", "ShipSteeringWheel"] +- name: Subregion Light Temple + id: 230 + type: "Subregion" + region: "LightTemple" + game_objects: [] + links: + - target_room: 185 # Light Temple + location: "LightTemple" + location_slot: "LightTemple" + entrance: 477 + teleporter: [0x23, 1] + access: [] +- name: Subregion Doom Castle + id: 231 + type: "Subregion" + region: "DoomCastle" + game_objects: [] + links: + - target_room: 1 # Doom Castle + location: "DoomCastle" + location_slot: "DoomCastle" + entrance: 476 + teleporter: [0x21, 1] + access: [] + - target_room: 187 # Mac Ship Doom + location: "MacsShipDoom" + location_slot: "MacsShipDoom" + entrance: 479 + teleporter: [0x24, 1] + access: ["Barred"] + - target_room: 229 + access: ["ShipLoaned", "ShipDockAccess", "ShipSteeringWheel"] +- name: Doom Castle - Sand Floor + id: 1 + game_objects: + - name: "Doom Castle B2 - Southeast Chest" + object_id: 0x01 + type: "Chest" + access: ["Bomb"] + - name: "Doom Castle B2 - Bone Ledge Box" + object_id: 0x1E + type: "Box" + access: [] + - name: "Doom Castle B2 - Hook Platform Box" + object_id: 0x1F + type: "Box" + access: ["DragonClaw"] + links: + - target_room: 231 + entrance: 1 + teleporter: [1, 6] + access: [] + - target_room: 5 + entrance: 0 + teleporter: [0, 0] + access: ["DragonClaw", "MegaGrenade"] +- name: Doom Castle - Aero Room + id: 2 + game_objects: + - name: "Doom Castle B2 - Sun Door Chest" + object_id: 0x00 + type: "Chest" + access: [] + links: + - target_room: 4 + entrance: 2 + teleporter: [1, 0] + access: [] +- name: Focus Tower B1 - Main Loop + id: 3 + game_objects: [] + links: + - target_room: 220 + entrance: 3 + teleporter: [2, 6] + access: [] + - target_room: 6 + entrance: 4 + teleporter: [4, 0] + access: [] +- name: Focus Tower B1 - Aero Corridor + id: 4 + game_objects: [] + links: + - target_room: 9 + entrance: 5 + teleporter: [5, 0] + access: [] + - target_room: 2 + entrance: 6 + teleporter: [8, 0] + access: [] +- name: Focus Tower B1 - Inner Loop + id: 5 + game_objects: [] + links: + - target_room: 1 + entrance: 8 + teleporter: [7, 0] + access: [] + - target_room: 201 + entrance: 7 + teleporter: [6, 0] + access: [] +- name: Focus Tower 1F Main Lobby + id: 6 + game_objects: + - name: "Focus Tower 1F - Main Lobby Box" + object_id: 0x21 + type: "Box" + access: [] + links: + - target_room: 3 + entrance: 11 + teleporter: [11, 0] + access: [] + - target_room: 7 + access: ["SandCoin"] + - target_room: 8 + access: ["RiverCoin"] + - target_room: 9 + access: ["SunCoin"] +- name: Focus Tower 1F SandCoin Room + id: 7 + game_objects: [] + links: + - target_room: 6 + access: ["SandCoin"] + - target_room: 10 + entrance: 10 + teleporter: [10, 0] + access: [] +- name: Focus Tower 1F RiverCoin Room + id: 8 + game_objects: [] + links: + - target_room: 6 + access: ["RiverCoin"] + - target_room: 11 + entrance: 14 + teleporter: [14, 0] + access: [] +- name: Focus Tower 1F SunCoin Room + id: 9 + game_objects: [] + links: + - target_room: 6 + access: ["SunCoin"] + - target_room: 4 + entrance: 12 + teleporter: [12, 0] + access: [] + - target_room: 226 + entrance: 9 + teleporter: [3, 6] + access: [] +- name: Focus Tower 1F SkyCoin Room + id: 201 + game_objects: [] + links: + - target_room: 195 + entrance: 13 + teleporter: [13, 0] + access: ["SkyCoin", "FlamerusRex", "IceGolem", "DualheadHydra", "Pazuzu"] + - target_room: 5 + entrance: 15 + teleporter: [15, 0] + access: [] +- name: Focus Tower 2F - Sand Coin Passage + id: 10 + game_objects: + - name: "Focus Tower 2F - Sand Door Chest" + object_id: 0x03 + type: "Chest" + access: [] + links: + - target_room: 221 + entrance: 16 + teleporter: [4, 6] + access: [] + - target_room: 7 + entrance: 17 + teleporter: [17, 0] + access: [] +- name: Focus Tower 2F - River Coin Passage + id: 11 + game_objects: [] + links: + - target_room: 8 + entrance: 18 + teleporter: [18, 0] + access: [] + - target_room: 13 + entrance: 19 + teleporter: [20, 0] + access: [] +- name: Focus Tower 2F - Venus Chest Room + id: 12 + game_objects: + - name: "Focus Tower 2F - Back Door Chest" + object_id: 0x02 + type: "Chest" + access: [] + - name: "Focus Tower 2F - Venus Chest" + object_id: 9 + type: "NPC" + access: ["Bomb", "VenusKey"] + links: + - target_room: 14 + entrance: 20 + teleporter: [19, 0] + access: [] +- name: Focus Tower 3F - Lower Floor + id: 13 + game_objects: + - name: "Focus Tower 3F - River Door Box" + object_id: 0x22 + type: "Box" + access: [] + links: + - target_room: 224 + entrance: 22 + teleporter: [6, 6] + access: [] + - target_room: 11 + entrance: 23 + teleporter: [24, 0] + access: [] +- name: Focus Tower 3F - Upper Floor + id: 14 + game_objects: [] + links: + - target_room: 223 + entrance: 24 + teleporter: [5, 6] + access: [] + - target_room: 12 + entrance: 25 + teleporter: [23, 0] + access: [] +- name: Level Forest + id: 15 + game_objects: + - name: "Level Forest - Northwest Box" + object_id: 0x28 + type: "Box" + access: ["Axe"] + - name: "Level Forest - Northeast Box" + object_id: 0x29 + type: "Box" + access: ["Axe"] + - name: "Level Forest - Middle Box" + object_id: 0x2A + type: "Box" + access: [] + - name: "Level Forest - Southwest Box" + object_id: 0x2B + type: "Box" + access: ["Axe"] + - name: "Level Forest - Southeast Box" + object_id: 0x2C + type: "Box" + access: ["Axe"] + - name: "Minotaur" + object_id: 0 + type: "Trigger" + on_trigger: ["Minotaur"] + access: ["Kaeli1"] + - name: "Level Forest - Old Man" + object_id: 0 + type: "NPC" + access: [] + - name: "Level Forest - Kaeli" + object_id: 1 + type: "NPC" + access: ["Kaeli1", "Minotaur"] + links: + - target_room: 220 + entrance: 28 + teleporter: [25, 0] + access: [] +- name: Foresta + id: 16 + game_objects: + - name: "Foresta - Outside Box" + object_id: 0x2D + type: "Box" + access: ["Axe"] + links: + - target_room: 220 + entrance: 38 + teleporter: [31, 0] + access: [] + - target_room: 17 + entrance: 44 + teleporter: [0, 5] + access: [] + - target_room: 18 + entrance: 42 + teleporter: [32, 4] + access: [] + - target_room: 19 + entrance: 43 + teleporter: [33, 0] + access: [] + - target_room: 20 + entrance: 45 + teleporter: [1, 5] + access: [] +- name: Kaeli's House + id: 17 + game_objects: + - name: "Foresta - Kaeli's House Box" + object_id: 0x2E + type: "Box" + access: [] + - name: "Kaeli 1" + object_id: 0 + type: "Trigger" + on_trigger: ["Kaeli1"] + access: ["TreeWither"] + - name: "Kaeli 2" + object_id: 0 + type: "Trigger" + on_trigger: ["Kaeli2"] + access: ["Kaeli1", "Minotaur", "Elixir"] + links: + - target_room: 16 + entrance: 46 + teleporter: [86, 3] + access: [] +- name: Foresta Houses - Old Man's House Main + id: 18 + game_objects: [] + links: + - target_room: 19 + access: ["BarrelPushed"] + - target_room: 16 + entrance: 47 + teleporter: [34, 0] + access: [] +- name: Foresta Houses - Old Man's House Back + id: 19 + game_objects: + - name: "Foresta - Old Man House Chest" + object_id: 0x05 + type: "Chest" + access: [] + - name: "Old Man Barrel" + object_id: 0 + type: "Trigger" + on_trigger: ["BarrelPushed"] + access: [] + links: + - target_room: 18 + access: ["BarrelPushed"] + - target_room: 16 + entrance: 48 + teleporter: [35, 0] + access: [] +- name: Foresta Houses - Rest House + id: 20 + game_objects: + - name: "Foresta - Rest House Box" + object_id: 0x2F + type: "Box" + access: [] + links: + - target_room: 16 + entrance: 50 + teleporter: [87, 3] + access: [] +- name: Libra Treehouse + id: 21 + game_objects: + - name: "Alive Forest - Libra Treehouse Box" + object_id: 0x32 + type: "Box" + access: [] + links: + - target_room: 124 + entrance: 51 + teleporter: [67, 8] + access: ["LibraCrest"] +- name: Gemini Treehouse + id: 22 + game_objects: + - name: "Alive Forest - Gemini Treehouse Box" + object_id: 0x33 + type: "Box" + access: [] + links: + - target_room: 124 + entrance: 52 + teleporter: [68, 8] + access: ["GeminiCrest"] +- name: Mobius Treehouse + id: 23 + game_objects: + - name: "Alive Forest - Mobius Treehouse West Box" + object_id: 0x30 + type: "Box" + access: [] + - name: "Alive Forest - Mobius Treehouse East Box" + object_id: 0x31 + type: "Box" + access: [] + links: + - target_room: 124 + entrance: 53 + teleporter: [69, 8] + access: ["MobiusCrest"] +- name: Sand Temple + id: 24 + game_objects: + - name: "Tristam Sand Temple" + object_id: 0 + type: "Trigger" + on_trigger: ["Tristam"] + access: [] + links: + - target_room: 220 + entrance: 54 + teleporter: [36, 0] + access: [] +- name: Bone Dungeon 1F + id: 25 + game_objects: + - name: "Bone Dungeon 1F - Entrance Room West Box" + object_id: 0x35 + type: "Box" + access: [] + - name: "Bone Dungeon 1F - Entrance Room Middle Box" + object_id: 0x36 + type: "Box" + access: [] + - name: "Bone Dungeon 1F - Entrance Room East Box" + object_id: 0x37 + type: "Box" + access: [] + links: + - target_room: 220 + entrance: 55 + teleporter: [37, 0] + access: [] + - target_room: 26 + entrance: 56 + teleporter: [2, 2] + access: [] +- name: Bone Dungeon B1 - Waterway + id: 26 + game_objects: + - name: "Bone Dungeon B1 - Skull Chest" + object_id: 0x06 + type: "Chest" + access: ["Bomb"] + - name: "Bone Dungeon B1 - Tristam" + object_id: 2 + type: "NPC" + access: ["Tristam"] + links: + - target_room: 25 + entrance: 59 + teleporter: [88, 3] + access: [] + - target_room: 28 + entrance: 57 + teleporter: [3, 2] + access: ["Bomb"] +- name: Bone Dungeon B1 - Checker Room + id: 28 + game_objects: + - name: "Bone Dungeon B1 - Checker Room Box" + object_id: 0x38 + type: "Box" + access: ["Bomb"] + links: + - target_room: 26 + entrance: 61 + teleporter: [89, 3] + access: [] + - target_room: 30 + entrance: 60 + teleporter: [4, 2] + access: [] +- name: Bone Dungeon B1 - Hidden Room + id: 29 + game_objects: + - name: "Bone Dungeon B1 - Ribcage Waterway Box" + object_id: 0x39 + type: "Box" + access: [] + links: + - target_room: 31 + entrance: 62 + teleporter: [91, 3] + access: [] +- name: Bone Dungeon B2 - Exploding Skull Room - First Room + id: 30 + game_objects: + - name: "Bone Dungeon B2 - Spines Room Alcove Box" + object_id: 0x3B + type: "Box" + access: [] + - name: "Long Spine" + object_id: 0 + type: "Trigger" + on_trigger: ["LongSpineBombed"] + access: ["Bomb"] + links: + - target_room: 28 + entrance: 65 + teleporter: [90, 3] + access: [] + - target_room: 31 + access: ["LongSpineBombed"] +- name: Bone Dungeon B2 - Exploding Skull Room - Second Room + id: 31 + game_objects: + - name: "Bone Dungeon B2 - Spines Room Looped Hallway Box" + object_id: 0x3A + type: "Box" + access: [] + - name: "Short Spine" + object_id: 0 + type: "Trigger" + on_trigger: ["ShortSpineBombed"] + access: ["Bomb"] + links: + - target_room: 29 + entrance: 63 + teleporter: [5, 2] + access: ["LongSpineBombed"] + - target_room: 32 + access: ["ShortSpineBombed"] + - target_room: 30 + access: ["LongSpineBombed"] +- name: Bone Dungeon B2 - Exploding Skull Room - Third Room + id: 32 + game_objects: [] + links: + - target_room: 35 + entrance: 64 + teleporter: [6, 2] + access: [] + - target_room: 31 + access: ["ShortSpineBombed"] +- name: Bone Dungeon B2 - Box Room + id: 33 + game_objects: + - name: "Bone Dungeon B2 - Lone Room Box" + object_id: 0x3D + type: "Box" + access: [] + links: + - target_room: 36 + entrance: 66 + teleporter: [93, 3] + access: [] +- name: Bone Dungeon B2 - Quake Room + id: 34 + game_objects: + - name: "Bone Dungeon B2 - Penultimate Room Chest" + object_id: 0x07 + type: "Chest" + access: [] + links: + - target_room: 37 + entrance: 67 + teleporter: [94, 3] + access: [] +- name: Bone Dungeon B2 - Two Skulls Room - First Room + id: 35 + game_objects: + - name: "Bone Dungeon B2 - Two Skulls Room Box" + object_id: 0x3C + type: "Box" + access: [] + - name: "Skull 1" + object_id: 0 + type: "Trigger" + on_trigger: ["Skull1Bombed"] + access: ["Bomb"] + links: + - target_room: 32 + entrance: 71 + teleporter: [92, 3] + access: [] + - target_room: 36 + access: ["Skull1Bombed"] +- name: Bone Dungeon B2 - Two Skulls Room - Second Room + id: 36 + game_objects: + - name: "Skull 2" + object_id: 0 + type: "Trigger" + on_trigger: ["Skull2Bombed"] + access: ["Bomb"] + links: + - target_room: 33 + entrance: 68 + teleporter: [7, 2] + access: [] + - target_room: 37 + access: ["Skull2Bombed"] + - target_room: 35 + access: ["Skull1Bombed"] +- name: Bone Dungeon B2 - Two Skulls Room - Third Room + id: 37 + game_objects: [] + links: + - target_room: 34 + entrance: 69 + teleporter: [8, 2] + access: [] + - target_room: 38 + entrance: 70 + teleporter: [9, 2] + access: ["Bomb"] + - target_room: 36 + access: ["Skull2Bombed"] +- name: Bone Dungeon B2 - Boss Room + id: 38 + game_objects: + - name: "Bone Dungeon B2 - North Box" + object_id: 0x3E + type: "Box" + access: [] + - name: "Bone Dungeon B2 - South Box" + object_id: 0x3F + type: "Box" + access: [] + - name: "Bone Dungeon B2 - Flamerus Rex Chest" + object_id: 0x08 + type: "Chest" + access: [] + - name: "Bone Dungeon B2 - Tristam's Treasure Chest" + object_id: 0x04 + type: "Chest" + access: [] + - name: "Flamerus Rex" + object_id: 0 + type: "Trigger" + on_trigger: ["FlamerusRex"] + access: [] + links: + - target_room: 37 + entrance: 74 + teleporter: [95, 3] + access: [] +- name: Libra Temple + id: 39 + game_objects: + - name: "Libra Temple - Box" + object_id: 0x40 + type: "Box" + access: [] + - name: "Phoebe" + object_id: 0 + type: "Trigger" + on_trigger: ["Phoebe1"] + access: [] + links: + - target_room: 221 + entrance: 75 + teleporter: [13, 6] + access: [] + - target_room: 51 + entrance: 76 + teleporter: [59, 8] + access: ["LibraCrest"] +- name: Aquaria + id: 40 + game_objects: + - name: "Summer Aquaria" + object_id: 0 + type: "Trigger" + on_trigger: ["SummerAquaria"] + access: ["WakeWater"] + links: + - target_room: 221 + entrance: 77 + teleporter: [8, 6] + access: [] + - target_room: 41 + entrance: 81 + teleporter: [10, 5] + access: [] + - target_room: 42 + entrance: 82 + teleporter: [44, 4] + access: [] + - target_room: 44 + entrance: 83 + teleporter: [11, 5] + access: [] + - target_room: 71 + entrance: 89 + teleporter: [42, 0] + access: ["SummerAquaria"] + - target_room: 71 + entrance: 90 + teleporter: [43, 0] + access: ["SummerAquaria"] +- name: Phoebe's House + id: 41 + game_objects: + - name: "Aquaria - Phoebe's House Chest" + object_id: 0x41 + type: "Box" + access: [] + links: + - target_room: 40 + entrance: 93 + teleporter: [5, 8] + access: [] +- name: Aquaria Vendor House + id: 42 + game_objects: + - name: "Aquaria - Vendor" + object_id: 4 + type: "NPC" + access: [] + - name: "Aquaria - Vendor House Box" + object_id: 0x42 + type: "Box" + access: [] + links: + - target_room: 40 + entrance: 94 + teleporter: [40, 8] + access: [] + - target_room: 43 + entrance: 95 + teleporter: [47, 0] + access: [] +- name: Aquaria Gemini Room + id: 43 + game_objects: [] + links: + - target_room: 42 + entrance: 97 + teleporter: [48, 0] + access: [] + - target_room: 81 + entrance: 96 + teleporter: [72, 8] + access: ["GeminiCrest"] +- name: Aquaria INN + id: 44 + game_objects: [] + links: + - target_room: 40 + entrance: 98 + teleporter: [75, 8] + access: [] +- name: Wintry Cave 1F - East Ledge + id: 45 + game_objects: + - name: "Wintry Cave 1F - North Box" + object_id: 0x43 + type: "Box" + access: [] + - name: "Wintry Cave 1F - Entrance Box" + object_id: 0x46 + type: "Box" + access: [] + - name: "Wintry Cave 1F - Slippery Cliff Box" + object_id: 0x44 + type: "Box" + access: ["Claw"] + - name: "Wintry Cave 1F - Phoebe" + object_id: 5 + type: "NPC" + access: ["Phoebe1"] + links: + - target_room: 221 + entrance: 99 + teleporter: [49, 0] + access: [] + - target_room: 49 + entrance: 100 + teleporter: [14, 2] + access: ["Bomb"] + - target_room: 46 + access: ["Claw"] +- name: Wintry Cave 1F - Central Space + id: 46 + game_objects: + - name: "Wintry Cave 1F - Scenic Overlook Box" + object_id: 0x45 + type: "Box" + access: ["Claw"] + links: + - target_room: 45 + access: ["Claw"] + - target_room: 47 + access: ["Claw"] +- name: Wintry Cave 1F - West Ledge + id: 47 + game_objects: [] + links: + - target_room: 48 + entrance: 101 + teleporter: [15, 2] + access: ["Bomb"] + - target_room: 46 + access: ["Claw"] +- name: Wintry Cave 2F + id: 48 + game_objects: + - name: "Wintry Cave 2F - West Left Box" + object_id: 0x47 + type: "Box" + access: [] + - name: "Wintry Cave 2F - West Right Box" + object_id: 0x48 + type: "Box" + access: [] + - name: "Wintry Cave 2F - East Left Box" + object_id: 0x49 + type: "Box" + access: [] + - name: "Wintry Cave 2F - East Right Box" + object_id: 0x4A + type: "Box" + access: [] + links: + - target_room: 47 + entrance: 104 + teleporter: [97, 3] + access: [] + - target_room: 50 + entrance: 103 + teleporter: [50, 0] + access: [] +- name: Wintry Cave 3F Top + id: 49 + game_objects: + - name: "Wintry Cave 3F - West Box" + object_id: 0x4B + type: "Box" + access: [] + - name: "Wintry Cave 3F - East Box" + object_id: 0x4C + type: "Box" + access: [] + links: + - target_room: 45 + entrance: 105 + teleporter: [96, 3] + access: [] +- name: Wintry Cave 3F Bottom + id: 50 + game_objects: + - name: "Wintry Cave 3F - Squidite Chest" + object_id: 0x09 + type: "Chest" + access: ["Phanquid"] + - name: "Phanquid" + object_id: 0 + type: "Trigger" + on_trigger: ["Phanquid"] + access: [] + - name: "Wintry Cave 3F - Before Boss Box" + object_id: 0x4D + type: "Box" + access: [] + links: + - target_room: 48 + entrance: 106 + teleporter: [51, 0] + access: [] +- name: Life Temple + id: 51 + game_objects: + - name: "Life Temple - Box" + object_id: 0x4E + type: "Box" + access: [] + - name: "Life Temple - Mysterious Man" + object_id: 6 + type: "NPC" + access: [] + links: + - target_room: 222 + entrance: 107 + teleporter: [14, 6] + access: [] + - target_room: 39 + entrance: 108 + teleporter: [60, 8] + access: ["LibraCrest"] +- name: Fall Basin + id: 52 + game_objects: + - name: "Falls Basin - Snow Crab Chest" + object_id: 0x0A + type: "Chest" + access: ["FreezerCrab"] + - name: "Freezer Crab" + object_id: 0 + type: "Trigger" + on_trigger: ["FreezerCrab"] + access: [] + - name: "Falls Basin - Box" + object_id: 0x4F + type: "Box" + access: [] + links: + - target_room: 221 + entrance: 111 + teleporter: [53, 0] + access: [] +- name: Ice Pyramid B1 Taunt Room + id: 53 + game_objects: + - name: "Ice Pyramid B1 - Chest" + object_id: 0x0B + type: "Chest" + access: [] + - name: "Ice Pyramid B1 - West Box" + object_id: 0x50 + type: "Box" + access: [] + - name: "Ice Pyramid B1 - North Box" + object_id: 0x51 + type: "Box" + access: [] + - name: "Ice Pyramid B1 - East Box" + object_id: 0x52 + type: "Box" + access: [] + links: + - target_room: 68 + entrance: 113 + teleporter: [55, 0] + access: [] +- name: Ice Pyramid 1F Maze Lobby + id: 54 + game_objects: + - name: "Ice Pyramid 1F Statue" + object_id: 0 + type: "Trigger" + on_trigger: ["IcePyramid1FStatue"] + access: ["Sword"] + links: + - target_room: 221 + entrance: 114 + teleporter: [56, 0] + access: [] + - target_room: 55 + access: ["IcePyramid1FStatue"] +- name: Ice Pyramid 1F Maze + id: 55 + game_objects: + - name: "Ice Pyramid 1F - East Alcove Chest" + object_id: 0x0D + type: "Chest" + access: [] + - name: "Ice Pyramid 1F - Sandwiched Alcove Box" + object_id: 0x53 + type: "Box" + access: [] + - name: "Ice Pyramid 1F - Southwest Left Box" + object_id: 0x54 + type: "Box" + access: [] + - name: "Ice Pyramid 1F - Southwest Right Box" + object_id: 0x55 + type: "Box" + access: [] + links: + - target_room: 56 + entrance: 116 + teleporter: [57, 0] + access: [] + - target_room: 57 + entrance: 117 + teleporter: [58, 0] + access: [] + - target_room: 58 + entrance: 118 + teleporter: [59, 0] + access: [] + - target_room: 59 + entrance: 119 + teleporter: [60, 0] + access: [] + - target_room: 60 + entrance: 120 + teleporter: [61, 0] + access: [] + - target_room: 54 + access: ["IcePyramid1FStatue"] +- name: Ice Pyramid 2F South Tiled Room + id: 56 + game_objects: + - name: "Ice Pyramid 2F - South Side Glass Door Box" + object_id: 0x57 + type: "Box" + access: ["Sword"] + - name: "Ice Pyramid 2F - South Side East Box" + object_id: 0x5B + type: "Box" + access: [] + links: + - target_room: 55 + entrance: 122 + teleporter: [62, 0] + access: [] + - target_room: 61 + entrance: 123 + teleporter: [67, 0] + access: [] +- name: Ice Pyramid 2F West Room + id: 57 + game_objects: + - name: "Ice Pyramid 2F - Northwest Room Box" + object_id: 0x5A + type: "Box" + access: [] + links: + - target_room: 55 + entrance: 124 + teleporter: [63, 0] + access: [] +- name: Ice Pyramid 2F Center Room + id: 58 + game_objects: + - name: "Ice Pyramid 2F - Center Room Box" + object_id: 0x56 + type: "Box" + access: [] + links: + - target_room: 55 + entrance: 125 + teleporter: [64, 0] + access: [] +- name: Ice Pyramid 2F Small North Room + id: 59 + game_objects: + - name: "Ice Pyramid 2F - North Room Glass Door Box" + object_id: 0x58 + type: "Box" + access: ["Sword"] + links: + - target_room: 55 + entrance: 126 + teleporter: [65, 0] + access: [] +- name: Ice Pyramid 2F North Corridor + id: 60 + game_objects: + - name: "Ice Pyramid 2F - North Corridor Glass Door Box" + object_id: 0x59 + type: "Box" + access: ["Sword"] + links: + - target_room: 55 + entrance: 127 + teleporter: [66, 0] + access: [] + - target_room: 62 + entrance: 128 + teleporter: [68, 0] + access: [] +- name: Ice Pyramid 3F Two Boxes Room + id: 61 + game_objects: + - name: "Ice Pyramid 3F - Staircase Dead End Left Box" + object_id: 0x5E + type: "Box" + access: [] + - name: "Ice Pyramid 3F - Staircase Dead End Right Box" + object_id: 0x5F + type: "Box" + access: [] + links: + - target_room: 56 + entrance: 129 + teleporter: [69, 0] + access: [] +- name: Ice Pyramid 3F Main Loop + id: 62 + game_objects: + - name: "Ice Pyramid 3F - Inner Room North Box" + object_id: 0x5C + type: "Box" + access: [] + - name: "Ice Pyramid 3F - Inner Room South Box" + object_id: 0x5D + type: "Box" + access: [] + - name: "Ice Pyramid 3F - East Alcove Box" + object_id: 0x60 + type: "Box" + access: [] + - name: "Ice Pyramid 3F - Leapfrog Box" + object_id: 0x61 + type: "Box" + access: [] + - name: "Ice Pyramid 3F Statue" + object_id: 0 + type: "Trigger" + on_trigger: ["IcePyramid3FStatue"] + access: ["Sword"] + links: + - target_room: 60 + entrance: 130 + teleporter: [70, 0] + access: [] + - target_room: 63 + access: ["IcePyramid3FStatue"] +- name: Ice Pyramid 3F Blocked Room + id: 63 + game_objects: [] + links: + - target_room: 64 + entrance: 131 + teleporter: [71, 0] + access: [] + - target_room: 62 + access: ["IcePyramid3FStatue"] +- name: Ice Pyramid 4F Main Loop + id: 64 + game_objects: [] + links: + - target_room: 66 + entrance: 133 + teleporter: [73, 0] + access: [] + - target_room: 63 + entrance: 132 + teleporter: [72, 0] + access: [] + - target_room: 65 + access: ["IcePyramid4FStatue"] +- name: Ice Pyramid 4F Treasure Room + id: 65 + game_objects: + - name: "Ice Pyramid 4F - Chest" + object_id: 0x0C + type: "Chest" + access: [] + - name: "Ice Pyramid 4F - Northwest Box" + object_id: 0x62 + type: "Box" + access: [] + - name: "Ice Pyramid 4F - West Left Box" + object_id: 0x63 + type: "Box" + access: [] + - name: "Ice Pyramid 4F - West Right Box" + object_id: 0x64 + type: "Box" + access: [] + - name: "Ice Pyramid 4F - South Left Box" + object_id: 0x65 + type: "Box" + access: [] + - name: "Ice Pyramid 4F - South Right Box" + object_id: 0x66 + type: "Box" + access: [] + - name: "Ice Pyramid 4F - East Left Box" + object_id: 0x67 + type: "Box" + access: [] + - name: "Ice Pyramid 4F - East Right Box" + object_id: 0x68 + type: "Box" + access: [] + - name: "Ice Pyramid 4F Statue" + object_id: 0 + type: "Trigger" + on_trigger: ["IcePyramid4FStatue"] + access: ["Sword"] + links: + - target_room: 64 + access: ["IcePyramid4FStatue"] +- name: Ice Pyramid 5F Leap of Faith Room + id: 66 + game_objects: + - name: "Ice Pyramid 5F - Glass Door Left Box" + object_id: 0x69 + type: "Box" + access: ["IcePyramid5FStatue"] + - name: "Ice Pyramid 5F - West Ledge Box" + object_id: 0x6A + type: "Box" + access: [] + - name: "Ice Pyramid 5F - South Shelf Box" + object_id: 0x6B + type: "Box" + access: [] + - name: "Ice Pyramid 5F - South Leapfrog Box" + object_id: 0x6C + type: "Box" + access: [] + - name: "Ice Pyramid 5F - Glass Door Right Box" + object_id: 0x6D + type: "Box" + access: ["IcePyramid5FStatue"] + - name: "Ice Pyramid 5F - North Box" + object_id: 0x6E + type: "Box" + access: [] + links: + - target_room: 64 + entrance: 134 + teleporter: [74, 0] + access: [] + - target_room: 65 + access: [] + - target_room: 53 + access: ["Bomb", "Claw", "Sword"] +- name: Ice Pyramid 5F Stairs to Ice Golem + id: 67 + game_objects: + - name: "Ice Pyramid 5F Statue" + object_id: 0 + type: "Trigger" + on_trigger: ["IcePyramid5FStatue"] + access: ["Sword"] + links: + - target_room: 69 + entrance: 137 + teleporter: [76, 0] + access: [] + - target_room: 65 + access: [] + - target_room: 70 + entrance: 136 + teleporter: [75, 0] + access: [] +- name: Ice Pyramid Climbing Wall Room Lower Space + id: 68 + game_objects: [] + links: + - target_room: 53 + entrance: 139 + teleporter: [78, 0] + access: [] + - target_room: 69 + access: ["Claw"] +- name: Ice Pyramid Climbing Wall Room Upper Space + id: 69 + game_objects: [] + links: + - target_room: 67 + entrance: 140 + teleporter: [79, 0] + access: [] + - target_room: 68 + access: ["Claw"] +- name: Ice Pyramid Ice Golem Room + id: 70 + game_objects: + - name: "Ice Pyramid 6F - Ice Golem Chest" + object_id: 0x0E + type: "Chest" + access: ["IceGolem"] + - name: "Ice Golem" + object_id: 0 + type: "Trigger" + on_trigger: ["IceGolem"] + access: [] + links: + - target_room: 67 + entrance: 141 + teleporter: [80, 0] + access: [] + - target_room: 66 + access: [] +- name: Spencer Waterfall + id: 71 + game_objects: [] + links: + - target_room: 72 + entrance: 143 + teleporter: [81, 0] + access: [] + - target_room: 40 + entrance: 145 + teleporter: [82, 0] + access: [] + - target_room: 40 + entrance: 148 + teleporter: [83, 0] + access: [] +- name: Spencer Cave Normal Main + id: 72 + game_objects: + - name: "Spencer's Cave - Box" + object_id: 0x6F + type: "Box" + access: ["Claw"] + - name: "Spencer's Cave - Spencer" + object_id: 8 + type: "NPC" + access: [] + - name: "Spencer's Cave - Locked Chest" + object_id: 13 + type: "NPC" + access: ["VenusKey"] + links: + - target_room: 71 + entrance: 150 + teleporter: [85, 0] + access: [] +- name: Spencer Cave Normal South Ledge + id: 73 + game_objects: + - name: "Collapse Spencer's Cave" + object_id: 0 + type: "Trigger" + on_trigger: ["ShipLiberated"] + access: ["MegaGrenade"] + links: + - target_room: 227 + entrance: 151 + teleporter: [7, 6] + access: [] + - target_room: 203 + access: ["MegaGrenade"] +# - target_room: 72 # access to spencer? +# access: ["MegaGrenade"] +- name: Spencer Cave Caved In Main Loop + id: 203 + game_objects: [] + links: + - target_room: 73 + access: [] + - target_room: 207 + entrance: 156 + teleporter: [36, 8] + access: ["MobiusCrest"] + - target_room: 204 + access: ["Claw"] + - target_room: 205 + access: ["Bomb"] +- name: Spencer Cave Caved In Waters + id: 204 + game_objects: + - name: "Bomb Libra Block" + object_id: 0 + type: "Trigger" + on_trigger: ["SpencerCaveLibraBlockBombed"] + access: ["MegaGrenade", "Claw"] + links: + - target_room: 203 + access: ["Claw"] +- name: Spencer Cave Caved In Libra Nook + id: 205 + game_objects: [] + links: + - target_room: 206 + entrance: 153 + teleporter: [33, 8] + access: ["LibraCrest"] +- name: Spencer Cave Caved In Libra Corridor + id: 206 + game_objects: [] + links: + - target_room: 205 + entrance: 154 + teleporter: [34, 8] + access: ["LibraCrest"] + - target_room: 207 + access: ["SpencerCaveLibraBlockBombed"] +- name: Spencer Cave Caved In Mobius Chest + id: 207 + game_objects: + - name: "Spencer's Cave - Mobius Chest" + object_id: 0x0F + type: "Chest" + access: [] + links: + - target_room: 203 + entrance: 155 + teleporter: [35, 8] + access: ["MobiusCrest"] + - target_room: 206 + access: ["Bomb"] +- name: Wintry Temple Outer Room + id: 74 + game_objects: [] + links: + - target_room: 223 + entrance: 157 + teleporter: [15, 6] + access: [] +- name: Wintry Temple Inner Room + id: 75 + game_objects: + - name: "Wintry Temple - West Box" + object_id: 0x70 + type: "Box" + access: [] + - name: "Wintry Temple - North Box" + object_id: 0x71 + type: "Box" + access: [] + links: + - target_room: 92 + entrance: 158 + teleporter: [62, 8] + access: ["GeminiCrest"] +- name: Fireburg Upper Plaza + id: 76 + game_objects: [] + links: + - target_room: 224 + entrance: 159 + teleporter: [9, 6] + access: [] + - target_room: 80 + entrance: 163 + teleporter: [91, 0] + access: [] + - target_room: 77 + entrance: 164 + teleporter: [16, 2] + access: [] + - target_room: 82 + entrance: 165 + teleporter: [17, 2] + access: [] + - target_room: 208 + access: ["Claw"] +- name: Fireburg Lower Plaza + id: 208 + game_objects: + - name: "Fireburg - Hidden Tunnel Box" + object_id: 0x74 + type: "Box" + access: [] + links: + - target_room: 76 + access: ["Claw"] + - target_room: 78 + entrance: 166 + teleporter: [11, 8] + access: ["MultiKey"] +- name: Reuben's House + id: 77 + game_objects: + - name: "Fireburg - Reuben's House Arion" + object_id: 14 + type: "NPC" + access: ["ReubenDadSaved"] + - name: "Reuben" + object_id: 0 + type: "Trigger" + on_trigger: ["Reuben1"] + access: [] + - name: "Fireburg - Reuben's House Box" + object_id: 0x75 + type: "Box" + access: [] + links: + - target_room: 76 + entrance: 167 + teleporter: [98, 3] + access: [] +- name: GrenadeMan's House + id: 78 + game_objects: + - name: "Fireburg - Locked House Man" + object_id: 12 + type: "NPC" + access: [] + links: + - target_room: 208 + entrance: 168 + teleporter: [9, 8] + access: ["MultiKey"] + - target_room: 79 + entrance: 169 + teleporter: [93, 0] + access: [] +- name: GrenadeMan's Mobius Room + id: 79 + game_objects: [] + links: + - target_room: 78 + entrance: 170 + teleporter: [94, 0] + access: [] + - target_room: 161 + entrance: 171 + teleporter: [54, 8] + access: ["MobiusCrest"] +- name: Fireburg Vendor House + id: 80 + game_objects: + - name: "Fireburg - Vendor" + object_id: 11 + type: "NPC" + access: [] + links: + - target_room: 76 + entrance: 172 + teleporter: [95, 0] + access: [] + - target_room: 81 + entrance: 173 + teleporter: [96, 0] + access: [] +- name: Fireburg Gemini Room + id: 81 + game_objects: [] + links: + - target_room: 80 + entrance: 174 + teleporter: [97, 0] + access: [] + - target_room: 43 + entrance: 175 + teleporter: [45, 8] + access: ["GeminiCrest"] +- name: Fireburg Hotel Lobby + id: 82 + game_objects: + - name: "Fireburg - Tristam" + object_id: 10 + type: "NPC" + access: [] + - name: "Tristam Fireburg" + object_id: 0 + type: "Trigger" + on_trigger: ["Tristam"] + access: [] + links: + - target_room: 76 + entrance: 177 + teleporter: [99, 3] + access: [] + - target_room: 83 + entrance: 176 + teleporter: [213, 0] + access: [] +- name: Fireburg Hotel Beds + id: 83 + game_objects: [] + links: + - target_room: 82 + entrance: 178 + teleporter: [214, 0] + access: [] +- name: Mine Exterior North West Platforms + id: 84 + game_objects: [] + links: + - target_room: 224 + entrance: 179 + teleporter: [98, 0] + access: [] + - target_room: 88 + entrance: 181 + teleporter: [20, 2] + access: ["Bomb"] + - target_room: 85 + access: ["Claw"] + - target_room: 86 + access: ["Claw"] + - target_room: 87 + access: ["Claw"] +- name: Mine Exterior Central Ledge + id: 85 + game_objects: [] + links: + - target_room: 90 + entrance: 183 + teleporter: [22, 2] + access: ["Bomb"] + - target_room: 84 + access: ["Claw"] +- name: Mine Exterior North Ledge + id: 86 + game_objects: [] + links: + - target_room: 89 + entrance: 182 + teleporter: [21, 2] + access: ["Bomb"] + - target_room: 85 + access: ["Claw"] +- name: Mine Exterior South East Platforms + id: 87 + game_objects: + - name: "Jinn" + object_id: 0 + type: "Trigger" + on_trigger: ["Jinn"] + access: [] + links: + - target_room: 91 + entrance: 180 + teleporter: [99, 0] + access: ["Jinn"] + - target_room: 86 + access: [] + - target_room: 85 + access: ["Claw"] +- name: Mine Parallel Room + id: 88 + game_objects: + - name: "Mine - Parallel Room West Box" + object_id: 0x77 + type: "Box" + access: ["Claw"] + - name: "Mine - Parallel Room East Box" + object_id: 0x78 + type: "Box" + access: ["Claw"] + links: + - target_room: 84 + entrance: 185 + teleporter: [100, 3] + access: [] +- name: Mine Crescent Room + id: 89 + game_objects: + - name: "Mine - Crescent Room Chest" + object_id: 0x10 + type: "Chest" + access: [] + links: + - target_room: 86 + entrance: 186 + teleporter: [101, 3] + access: [] +- name: Mine Climbing Room + id: 90 + game_objects: + - name: "Mine - Glitchy Collision Cave Box" + object_id: 0x76 + type: "Box" + access: ["Claw"] + links: + - target_room: 85 + entrance: 187 + teleporter: [102, 3] + access: [] +- name: Mine Cliff + id: 91 + game_objects: + - name: "Mine - Cliff Southwest Box" + object_id: 0x79 + type: "Box" + access: [] + - name: "Mine - Cliff Northwest Box" + object_id: 0x7A + type: "Box" + access: [] + - name: "Mine - Cliff Northeast Box" + object_id: 0x7B + type: "Box" + access: [] + - name: "Mine - Cliff Southeast Box" + object_id: 0x7C + type: "Box" + access: [] + - name: "Mine - Reuben" + object_id: 7 + type: "NPC" + access: ["Reuben1"] + - name: "Reuben's dad Saved" + object_id: 0 + type: "Trigger" + on_trigger: ["ReubenDadSaved"] + access: ["MegaGrenade"] + links: + - target_room: 87 + entrance: 188 + teleporter: [100, 0] + access: [] +- name: Sealed Temple + id: 92 + game_objects: + - name: "Sealed Temple - West Box" + object_id: 0x7D + type: "Box" + access: [] + - name: "Sealed Temple - East Box" + object_id: 0x7E + type: "Box" + access: [] + links: + - target_room: 224 + entrance: 190 + teleporter: [16, 6] + access: [] + - target_room: 75 + entrance: 191 + teleporter: [63, 8] + access: ["GeminiCrest"] +- name: Volcano Base + id: 93 + game_objects: + - name: "Volcano - Base Chest" + object_id: 0x11 + type: "Chest" + access: [] + - name: "Volcano - Base West Box" + object_id: 0x7F + type: "Box" + access: [] + - name: "Volcano - Base East Left Box" + object_id: 0x80 + type: "Box" + access: [] + - name: "Volcano - Base East Right Box" + object_id: 0x81 + type: "Box" + access: [] + links: + - target_room: 224 + entrance: 192 + teleporter: [103, 0] + access: [] + - target_room: 98 + entrance: 196 + teleporter: [31, 8] + access: [] + - target_room: 96 + entrance: 197 + teleporter: [30, 8] + access: [] +- name: Volcano Top Left + id: 94 + game_objects: + - name: "Volcano - Medusa Chest" + object_id: 0x12 + type: "Chest" + access: ["Medusa"] + - name: "Medusa" + object_id: 0 + type: "Trigger" + on_trigger: ["Medusa"] + access: [] + - name: "Volcano - Behind Medusa Box" + object_id: 0x82 + type: "Box" + access: [] + links: + - target_room: 209 + entrance: 199 + teleporter: [26, 8] + access: [] +- name: Volcano Top Right + id: 95 + game_objects: + - name: "Volcano - Top of the Volcano Left Box" + object_id: 0x83 + type: "Box" + access: [] + - name: "Volcano - Top of the Volcano Right Box" + object_id: 0x84 + type: "Box" + access: [] + links: + - target_room: 99 + entrance: 200 + teleporter: [79, 8] + access: [] +- name: Volcano Right Path + id: 96 + game_objects: + - name: "Volcano - Right Path Box" + object_id: 0x87 + type: "Box" + access: [] + links: + - target_room: 93 + entrance: 201 + teleporter: [15, 8] + access: [] +- name: Volcano Left Path + id: 98 + game_objects: + - name: "Volcano - Left Path Box" + object_id: 0x86 + type: "Box" + access: [] + links: + - target_room: 93 + entrance: 204 + teleporter: [27, 8] + access: [] + - target_room: 99 + entrance: 202 + teleporter: [25, 2] + access: [] + - target_room: 209 + entrance: 203 + teleporter: [26, 2] + access: [] +- name: Volcano Cross Left-Right + id: 99 + game_objects: [] + links: + - target_room: 95 + entrance: 206 + teleporter: [29, 8] + access: [] + - target_room: 98 + entrance: 205 + teleporter: [103, 3] + access: [] +- name: Volcano Cross Right-Left + id: 209 + game_objects: + - name: "Volcano - Crossover Section Box" + object_id: 0x85 + type: "Box" + access: [] + links: + - target_room: 98 + entrance: 208 + teleporter: [104, 3] + access: [] + - target_room: 94 + entrance: 207 + teleporter: [28, 8] + access: [] +- name: Lava Dome Inner Ring Main Loop + id: 100 + game_objects: + - name: "Lava Dome - Exterior Caldera Near Switch Cliff Box" + object_id: 0x88 + type: "Box" + access: [] + - name: "Lava Dome - Exterior South Cliff Box" + object_id: 0x89 + type: "Box" + access: [] + links: + - target_room: 224 + entrance: 209 + teleporter: [104, 0] + access: [] + - target_room: 113 + entrance: 211 + teleporter: [105, 0] + access: [] + - target_room: 114 + entrance: 212 + teleporter: [106, 0] + access: [] + - target_room: 116 + entrance: 213 + teleporter: [108, 0] + access: [] + - target_room: 118 + entrance: 214 + teleporter: [111, 0] + access: [] +- name: Lava Dome Inner Ring Center Ledge + id: 101 + game_objects: + - name: "Lava Dome - Exterior Center Dropoff Ledge Box" + object_id: 0x8A + type: "Box" + access: [] + links: + - target_room: 115 + entrance: 215 + teleporter: [107, 0] + access: [] + - target_room: 100 + access: ["Claw"] +- name: Lava Dome Inner Ring Plate Ledge + id: 102 + game_objects: + - name: "Lava Dome Plate" + object_id: 0 + type: "Trigger" + on_trigger: ["LavaDomePlate"] + access: [] + links: + - target_room: 119 + entrance: 216 + teleporter: [109, 0] + access: [] +- name: Lava Dome Inner Ring Upper Ledge West + id: 103 + game_objects: [] + links: + - target_room: 111 + entrance: 219 + teleporter: [112, 0] + access: [] + - target_room: 108 + entrance: 220 + teleporter: [113, 0] + access: [] + - target_room: 104 + access: ["Claw"] + - target_room: 100 + access: ["Claw"] +- name: Lava Dome Inner Ring Upper Ledge East + id: 104 + game_objects: [] + links: + - target_room: 110 + entrance: 218 + teleporter: [110, 0] + access: [] + - target_room: 103 + access: ["Claw"] +- name: Lava Dome Inner Ring Big Door Ledge + id: 105 + game_objects: [] + links: + - target_room: 107 + entrance: 221 + teleporter: [114, 0] + access: [] + - target_room: 121 + entrance: 222 + teleporter: [29, 2] + access: ["LavaDomePlate"] +- name: Lava Dome Inner Ring Tiny Bottom Ledge + id: 106 + game_objects: + - name: "Lava Dome - Exterior Dead End Caldera Box" + object_id: 0x8B + type: "Box" + access: [] + links: + - target_room: 120 + entrance: 226 + teleporter: [115, 0] + access: [] +- name: Lava Dome Jump Maze II + id: 107 + game_objects: + - name: "Lava Dome - Gold Maze Northwest Box" + object_id: 0x8C + type: "Box" + access: [] + - name: "Lava Dome - Gold Maze Southwest Box" + object_id: 0xF6 + type: "Box" + access: [] + - name: "Lava Dome - Gold Maze Northeast Box" + object_id: 0xF7 + type: "Box" + access: [] + - name: "Lava Dome - Gold Maze North Box" + object_id: 0xF8 + type: "Box" + access: [] + - name: "Lava Dome - Gold Maze Center Box" + object_id: 0xF9 + type: "Box" + access: [] + - name: "Lava Dome - Gold Maze Southeast Box" + object_id: 0xFA + type: "Box" + access: [] + links: + - target_room: 105 + entrance: 227 + teleporter: [116, 0] + access: [] + - target_room: 108 + entrance: 228 + teleporter: [119, 0] + access: [] + - target_room: 120 + entrance: 229 + teleporter: [120, 0] + access: [] +- name: Lava Dome Up-Down Corridor + id: 108 + game_objects: [] + links: + - target_room: 107 + entrance: 231 + teleporter: [118, 0] + access: [] + - target_room: 103 + entrance: 230 + teleporter: [117, 0] + access: [] +- name: Lava Dome Jump Maze I + id: 109 + game_objects: + - name: "Lava Dome - Bare Maze Leapfrog Alcove North Box" + object_id: 0x8D + type: "Box" + access: [] + - name: "Lava Dome - Bare Maze Leapfrog Alcove South Box" + object_id: 0x8E + type: "Box" + access: [] + - name: "Lava Dome - Bare Maze Center Box" + object_id: 0x8F + type: "Box" + access: [] + - name: "Lava Dome - Bare Maze Southwest Box" + object_id: 0x90 + type: "Box" + access: [] + links: + - target_room: 118 + entrance: 232 + teleporter: [121, 0] + access: [] + - target_room: 111 + entrance: 233 + teleporter: [122, 0] + access: [] +- name: Lava Dome Pointless Room + id: 110 + game_objects: [] + links: + - target_room: 104 + entrance: 234 + teleporter: [123, 0] + access: [] +- name: Lava Dome Lower Moon Helm Room + id: 111 + game_objects: + - name: "Lava Dome - U-Bend Room North Box" + object_id: 0x92 + type: "Box" + access: [] + - name: "Lava Dome - U-Bend Room South Box" + object_id: 0x93 + type: "Box" + access: [] + links: + - target_room: 103 + entrance: 235 + teleporter: [124, 0] + access: [] + - target_room: 109 + entrance: 236 + teleporter: [125, 0] + access: [] +- name: Lava Dome Moon Helm Room + id: 112 + game_objects: + - name: "Lava Dome - Beyond River Room Chest" + object_id: 0x13 + type: "Chest" + access: [] + - name: "Lava Dome - Beyond River Room Box" + object_id: 0x91 + type: "Box" + access: [] + links: + - target_room: 117 + entrance: 237 + teleporter: [126, 0] + access: [] +- name: Lava Dome Three Jumps Room + id: 113 + game_objects: + - name: "Lava Dome - Three Jumps Room Box" + object_id: 0x96 + type: "Box" + access: [] + links: + - target_room: 100 + entrance: 238 + teleporter: [127, 0] + access: [] +- name: Lava Dome Life Chest Room Lower Ledge + id: 114 + game_objects: + - name: "Lava Dome - Gold Bar Room Boulder Chest" + object_id: 0x1C + type: "Chest" + access: ["MegaGrenade"] + links: + - target_room: 100 + entrance: 239 + teleporter: [128, 0] + access: [] + - target_room: 115 + access: ["Claw"] +- name: Lava Dome Life Chest Room Upper Ledge + id: 115 + game_objects: + - name: "Lava Dome - Gold Bar Room Leapfrog Alcove Box West" + object_id: 0x94 + type: "Box" + access: [] + - name: "Lava Dome - Gold Bar Room Leapfrog Alcove Box East" + object_id: 0x95 + type: "Box" + access: [] + links: + - target_room: 101 + entrance: 240 + teleporter: [129, 0] + access: [] + - target_room: 114 + access: ["Claw"] +- name: Lava Dome Big Jump Room Main Area + id: 116 + game_objects: + - name: "Lava Dome - Lava River Room North Box" + object_id: 0x98 + type: "Box" + access: [] + - name: "Lava Dome - Lava River Room East Box" + object_id: 0x99 + type: "Box" + access: [] + - name: "Lava Dome - Lava River Room South Box" + object_id: 0x9A + type: "Box" + access: [] + links: + - target_room: 100 + entrance: 241 + teleporter: [133, 0] + access: [] + - target_room: 119 + entrance: 243 + teleporter: [132, 0] + access: [] + - target_room: 117 + access: ["MegaGrenade"] +- name: Lava Dome Big Jump Room MegaGrenade Area + id: 117 + game_objects: [] + links: + - target_room: 112 + entrance: 242 + teleporter: [131, 0] + access: [] + - target_room: 116 + access: ["Bomb"] +- name: Lava Dome Split Corridor + id: 118 + game_objects: + - name: "Lava Dome - Split Corridor Box" + object_id: 0x97 + type: "Box" + access: [] + links: + - target_room: 109 + entrance: 244 + teleporter: [130, 0] + access: [] + - target_room: 100 + entrance: 245 + teleporter: [134, 0] + access: [] +- name: Lava Dome Plate Corridor + id: 119 + game_objects: [] + links: + - target_room: 102 + entrance: 246 + teleporter: [135, 0] + access: [] + - target_room: 116 + entrance: 247 + teleporter: [137, 0] + access: [] +- name: Lava Dome Four Boxes Stairs + id: 120 + game_objects: + - name: "Lava Dome - Caldera Stairway West Left Box" + object_id: 0x9B + type: "Box" + access: [] + - name: "Lava Dome - Caldera Stairway West Right Box" + object_id: 0x9C + type: "Box" + access: [] + - name: "Lava Dome - Caldera Stairway East Left Box" + object_id: 0x9D + type: "Box" + access: [] + - name: "Lava Dome - Caldera Stairway East Right Box" + object_id: 0x9E + type: "Box" + access: [] + links: + - target_room: 107 + entrance: 248 + teleporter: [136, 0] + access: [] + - target_room: 106 + entrance: 249 + teleporter: [16, 0] + access: [] +- name: Lava Dome Hydra Room + id: 121 + game_objects: + - name: "Lava Dome - Dualhead Hydra Chest" + object_id: 0x14 + type: "Chest" + access: ["DualheadHydra"] + - name: "Dualhead Hydra" + object_id: 0 + type: "Trigger" + on_trigger: ["DualheadHydra"] + access: [] + - name: "Lava Dome - Hydra Room Northwest Box" + object_id: 0x9F + type: "Box" + access: [] + - name: "Lava Dome - Hydra Room Southweast Box" + object_id: 0xA0 + type: "Box" + access: [] + links: + - target_room: 105 + entrance: 250 + teleporter: [105, 3] + access: [] + - target_room: 122 + entrance: 251 + teleporter: [138, 0] + access: ["DualheadHydra"] +- name: Lava Dome Escape Corridor + id: 122 + game_objects: [] + links: + - target_room: 121 + entrance: 253 + teleporter: [139, 0] + access: [] +- name: Rope Bridge + id: 123 + game_objects: + - name: "Rope Bridge - West Box" + object_id: 0xA3 + type: "Box" + access: [] + - name: "Rope Bridge - East Box" + object_id: 0xA4 + type: "Box" + access: [] + links: + - target_room: 226 + entrance: 255 + teleporter: [140, 0] + access: [] +- name: Alive Forest + id: 124 + game_objects: + - name: "Alive Forest - Tree Stump Chest" + object_id: 0x15 + type: "Chest" + access: ["Axe"] + - name: "Alive Forest - Near Entrance Box" + object_id: 0xA5 + type: "Box" + access: ["Axe"] + - name: "Alive Forest - After Bridge Box" + object_id: 0xA6 + type: "Box" + access: ["Axe"] + - name: "Alive Forest - Gemini Stump Box" + object_id: 0xA7 + type: "Box" + access: ["Axe"] + links: + - target_room: 226 + entrance: 272 + teleporter: [142, 0] + access: ["Axe"] + - target_room: 21 + entrance: 275 + teleporter: [64, 8] + access: ["LibraCrest", "Axe"] + - target_room: 22 + entrance: 276 + teleporter: [65, 8] + access: ["GeminiCrest", "Axe"] + - target_room: 23 + entrance: 277 + teleporter: [66, 8] + access: ["MobiusCrest", "Axe"] + - target_room: 125 + entrance: 274 + teleporter: [143, 0] + access: ["Axe"] +- name: Giant Tree 1F Main Area + id: 125 + game_objects: + - name: "Giant Tree 1F - Northwest Box" + object_id: 0xA8 + type: "Box" + access: [] + - name: "Giant Tree 1F - Southwest Box" + object_id: 0xA9 + type: "Box" + access: [] + - name: "Giant Tree 1F - Center Box" + object_id: 0xAA + type: "Box" + access: [] + - name: "Giant Tree 1F - East Box" + object_id: 0xAB + type: "Box" + access: [] + links: + - target_room: 124 + entrance: 278 + teleporter: [56, 1] # [49, 8] script restored if no map shuffling + access: [] + - target_room: 202 + access: ["DragonClaw"] +- name: Giant Tree 1F North Island + id: 202 + game_objects: [] + links: + - target_room: 127 + entrance: 280 + teleporter: [144, 0] + access: [] + - target_room: 125 + access: ["DragonClaw"] +- name: Giant Tree 1F Central Island + id: 126 + game_objects: [] + links: + - target_room: 202 + access: ["DragonClaw"] +- name: Giant Tree 2F Main Lobby + id: 127 + game_objects: + - name: "Giant Tree 2F - North Box" + object_id: 0xAC + type: "Box" + access: [] + links: + - target_room: 126 + access: ["DragonClaw"] + - target_room: 125 + entrance: 281 + teleporter: [145, 0] + access: [] + - target_room: 133 + entrance: 283 + teleporter: [149, 0] + access: [] + - target_room: 129 + access: ["DragonClaw"] +- name: Giant Tree 2F West Ledge + id: 128 + game_objects: + - name: "Giant Tree 2F - Dropdown Ledge Box" + object_id: 0xAE + type: "Box" + access: [] + links: + - target_room: 140 + entrance: 284 + teleporter: [147, 0] + access: ["Sword"] + - target_room: 130 + access: ["DragonClaw"] +- name: Giant Tree 2F Lower Area + id: 129 + game_objects: + - name: "Giant Tree 2F - South Box" + object_id: 0xAD + type: "Box" + access: [] + links: + - target_room: 130 + access: ["Claw"] + - target_room: 131 + access: ["Claw"] +- name: Giant Tree 2F Central Island + id: 130 + game_objects: [] + links: + - target_room: 129 + access: ["Claw"] + - target_room: 135 + entrance: 282 + teleporter: [146, 0] + access: ["Sword"] +- name: Giant Tree 2F East Ledge + id: 131 + game_objects: [] + links: + - target_room: 129 + access: ["Claw"] + - target_room: 130 + access: ["DragonClaw"] +- name: Giant Tree 2F Meteor Chest Room + id: 132 + game_objects: + - name: "Giant Tree 2F - Gidrah Chest" + object_id: 0x16 + type: "Chest" + access: [] + links: + - target_room: 133 + entrance: 285 + teleporter: [148, 0] + access: [] +- name: Giant Tree 2F Mushroom Room + id: 133 + game_objects: + - name: "Giant Tree 2F - Mushroom Tunnel West Box" + object_id: 0xAF + type: "Box" + access: ["Axe"] + - name: "Giant Tree 2F - Mushroom Tunnel East Box" + object_id: 0xB0 + type: "Box" + access: ["Axe"] + links: + - target_room: 127 + entrance: 286 + teleporter: [150, 0] + access: ["Axe"] + - target_room: 132 + entrance: 287 + teleporter: [151, 0] + access: ["Axe", "Gidrah"] +- name: Giant Tree 3F Central Island + id: 135 + game_objects: + - name: "Giant Tree 3F - Central Island Box" + object_id: 0xB3 + type: "Box" + access: [] + links: + - target_room: 130 + entrance: 288 + teleporter: [152, 0] + access: [] + - target_room: 136 + access: ["Claw"] + - target_room: 137 + access: ["DragonClaw"] +- name: Giant Tree 3F Central Area + id: 136 + game_objects: + - name: "Giant Tree 3F - Center North Box" + object_id: 0xB1 + type: "Box" + access: [] + - name: "Giant Tree 3F - Center West Box" + object_id: 0xB2 + type: "Box" + access: [] + links: + - target_room: 135 + access: ["Claw"] + - target_room: 127 + access: [] + - target_room: 131 + access: [] +- name: Giant Tree 3F Lower Ledge + id: 137 + game_objects: [] + links: + - target_room: 135 + access: ["DragonClaw"] + - target_room: 142 + entrance: 289 + teleporter: [153, 0] + access: ["Sword"] +- name: Giant Tree 3F West Area + id: 138 + game_objects: + - name: "Giant Tree 3F - West Side Box" + object_id: 0xB4 + type: "Box" + access: [] + links: + - target_room: 128 + access: [] + - target_room: 210 + entrance: 290 + teleporter: [154, 0] + access: [] +- name: Giant Tree 3F Middle Up Island + id: 139 + game_objects: [] + links: + - target_room: 136 + access: ["Claw"] +- name: Giant Tree 3F West Platform + id: 140 + game_objects: [] + links: + - target_room: 139 + access: ["Claw"] + - target_room: 141 + access: ["Claw"] + - target_room: 128 + entrance: 291 + teleporter: [155, 0] + access: [] +- name: Giant Tree 3F North Ledge + id: 141 + game_objects: [] + links: + - target_room: 143 + entrance: 292 + teleporter: [156, 0] + access: ["Sword"] + - target_room: 139 + access: ["Claw"] + - target_room: 136 + access: ["Claw"] +- name: Giant Tree Worm Room Upper Ledge + id: 142 + game_objects: + - name: "Giant Tree 3F - Worm Room North Box" + object_id: 0xB5 + type: "Box" + access: ["Axe"] + - name: "Giant Tree 3F - Worm Room South Box" + object_id: 0xB6 + type: "Box" + access: ["Axe"] + links: + - target_room: 137 + entrance: 293 + teleporter: [157, 0] + access: ["Axe"] + - target_room: 210 + access: ["Axe", "Claw"] +- name: Giant Tree Worm Room Lower Ledge + id: 210 + game_objects: [] + links: + - target_room: 138 + entrance: 294 + teleporter: [158, 0] + access: [] +- name: Giant Tree 4F Lower Floor + id: 143 + game_objects: [] + links: + - target_room: 141 + entrance: 295 + teleporter: [159, 0] + access: [] + - target_room: 148 + entrance: 296 + teleporter: [160, 0] + access: [] + - target_room: 148 + entrance: 297 + teleporter: [161, 0] + access: [] + - target_room: 147 + entrance: 298 + teleporter: [162, 0] + access: ["Sword"] +- name: Giant Tree 4F Middle Floor + id: 144 + game_objects: + - name: "Giant Tree 4F - Highest Platform North Box" + object_id: 0xB7 + type: "Box" + access: [] + - name: "Giant Tree 4F - Highest Platform South Box" + object_id: 0xB8 + type: "Box" + access: [] + links: + - target_room: 149 + entrance: 299 + teleporter: [163, 0] + access: [] + - target_room: 145 + access: ["Claw"] + - target_room: 146 + access: ["DragonClaw"] +- name: Giant Tree 4F Upper Floor + id: 145 + game_objects: [] + links: + - target_room: 150 + entrance: 300 + teleporter: [164, 0] + access: ["Sword"] + - target_room: 144 + access: ["Claw"] +- name: Giant Tree 4F South Ledge + id: 146 + game_objects: + - name: "Giant Tree 4F - Hook Ledge Northeast Box" + object_id: 0xB9 + type: "Box" + access: [] + - name: "Giant Tree 4F - Hook Ledge Southwest Box" + object_id: 0xBA + type: "Box" + access: [] + links: + - target_room: 144 + access: ["DragonClaw"] +- name: Giant Tree 4F Slime Room East Area + id: 147 + game_objects: + - name: "Giant Tree 4F - East Slime Room Box" + object_id: 0xBC + type: "Box" + access: ["Axe"] + links: + - target_room: 143 + entrance: 304 + teleporter: [168, 0] + access: [] +- name: Giant Tree 4F Slime Room West Area + id: 148 + game_objects: [] + links: + - target_room: 143 + entrance: 303 + teleporter: [167, 0] + access: ["Axe"] + - target_room: 143 + entrance: 302 + teleporter: [166, 0] + access: ["Axe"] + - target_room: 149 + access: ["Axe", "Claw"] +- name: Giant Tree 4F Slime Room Platform + id: 149 + game_objects: + - name: "Giant Tree 4F - West Slime Room Box" + object_id: 0xBB + type: "Box" + access: [] + links: + - target_room: 144 + entrance: 301 + teleporter: [165, 0] + access: [] + - target_room: 148 + access: ["Claw"] +- name: Giant Tree 5F Lower Area + id: 150 + game_objects: + - name: "Giant Tree 5F - Northwest Left Box" + object_id: 0xBD + type: "Box" + access: [] + - name: "Giant Tree 5F - Northwest Right Box" + object_id: 0xBE + type: "Box" + access: [] + - name: "Giant Tree 5F - South Left Box" + object_id: 0xBF + type: "Box" + access: [] + - name: "Giant Tree 5F - South Right Box" + object_id: 0xC0 + type: "Box" + access: [] + links: + - target_room: 145 + entrance: 305 + teleporter: [169, 0] + access: [] + - target_room: 151 + access: ["Claw"] + - target_room: 143 + access: [] +- name: Giant Tree 5F Gidrah Platform + id: 151 + game_objects: + - name: "Gidrah" + object_id: 0 + type: "Trigger" + on_trigger: ["Gidrah"] + access: [] + links: + - target_room: 150 + access: ["Claw"] +- name: Kaidge Temple Lower Ledge + id: 152 + game_objects: [] + links: + - target_room: 226 + entrance: 307 + teleporter: [18, 6] + access: [] + - target_room: 153 + access: ["Claw"] +- name: Kaidge Temple Upper Ledge + id: 153 + game_objects: + - name: "Kaidge Temple - Box" + object_id: 0xC1 + type: "Box" + access: [] + links: + - target_room: 185 + entrance: 308 + teleporter: [71, 8] + access: ["MobiusCrest"] + - target_room: 152 + access: ["Claw"] +- name: Windhole Temple + id: 154 + game_objects: + - name: "Windhole Temple - Box" + object_id: 0xC2 + type: "Box" + access: [] + links: + - target_room: 226 + entrance: 309 + teleporter: [173, 0] + access: [] +- name: Mount Gale + id: 155 + game_objects: + - name: "Mount Gale - Dullahan Chest" + object_id: 0x17 + type: "Chest" + access: ["DragonClaw", "Dullahan"] + - name: "Dullahan" + object_id: 0 + type: "Trigger" + on_trigger: ["Dullahan"] + access: ["DragonClaw"] + - name: "Mount Gale - East Box" + object_id: 0xC3 + type: "Box" + access: ["DragonClaw"] + - name: "Mount Gale - West Box" + object_id: 0xC4 + type: "Box" + access: [] + links: + - target_room: 226 + entrance: 310 + teleporter: [174, 0] + access: [] +- name: Windia + id: 156 + game_objects: [] + links: + - target_room: 226 + entrance: 312 + teleporter: [10, 6] + access: [] + - target_room: 157 + entrance: 320 + teleporter: [30, 5] + access: [] + - target_room: 163 + entrance: 321 + teleporter: [31, 2] + access: [] + - target_room: 165 + entrance: 322 + teleporter: [32, 5] + access: [] + - target_room: 159 + entrance: 323 + teleporter: [176, 4] + access: [] + - target_room: 160 + entrance: 324 + teleporter: [177, 4] + access: [] +- name: Otto's House + id: 157 + game_objects: + - name: "Otto" + object_id: 0 + type: "Trigger" + on_trigger: ["RainbowBridge"] + access: ["ThunderRock"] + links: + - target_room: 156 + entrance: 327 + teleporter: [106, 3] + access: [] + - target_room: 158 + entrance: 326 + teleporter: [33, 2] + access: [] +- name: Otto's Attic + id: 158 + game_objects: + - name: "Windia - Otto's Attic Box" + object_id: 0xC5 + type: "Box" + access: [] + links: + - target_room: 157 + entrance: 328 + teleporter: [107, 3] + access: [] +- name: Windia Kid House + id: 159 + game_objects: [] + links: + - target_room: 156 + entrance: 329 + teleporter: [178, 0] + access: [] + - target_room: 161 + entrance: 330 + teleporter: [180, 0] + access: [] +- name: Windia Old People House + id: 160 + game_objects: [] + links: + - target_room: 156 + entrance: 331 + teleporter: [179, 0] + access: [] + - target_room: 162 + entrance: 332 + teleporter: [181, 0] + access: [] +- name: Windia Kid House Basement + id: 161 + game_objects: [] + links: + - target_room: 159 + entrance: 333 + teleporter: [182, 0] + access: [] + - target_room: 79 + entrance: 334 + teleporter: [44, 8] + access: ["MobiusCrest"] +- name: Windia Old People House Basement + id: 162 + game_objects: + - name: "Windia - Mobius Basement West Box" + object_id: 0xC8 + type: "Box" + access: [] + - name: "Windia - Mobius Basement East Box" + object_id: 0xC9 + type: "Box" + access: [] + links: + - target_room: 160 + entrance: 335 + teleporter: [183, 0] + access: [] + - target_room: 186 + entrance: 336 + teleporter: [43, 8] + access: ["MobiusCrest"] +- name: Windia Inn Lobby + id: 163 + game_objects: [] + links: + - target_room: 156 + entrance: 338 + teleporter: [135, 3] + access: [] + - target_room: 164 + entrance: 337 + teleporter: [215, 0] + access: [] +- name: Windia Inn Beds + id: 164 + game_objects: + - name: "Windia - Inn Bedroom North Box" + object_id: 0xC6 + type: "Box" + access: [] + - name: "Windia - Inn Bedroom South Box" + object_id: 0xC7 + type: "Box" + access: [] + - name: "Windia - Kaeli" + object_id: 15 + type: "NPC" + access: ["Kaeli2"] + links: + - target_room: 163 + entrance: 339 + teleporter: [216, 0] + access: [] +- name: Windia Vendor House + id: 165 + game_objects: + - name: "Windia - Vendor" + object_id: 16 + type: "NPC" + access: [] + links: + - target_room: 156 + entrance: 340 + teleporter: [108, 3] + access: [] +- name: Pazuzu Tower 1F Main Lobby + id: 166 + game_objects: + - name: "Pazuzu 1F" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu1F"] + access: [] + links: + - target_room: 226 + entrance: 341 + teleporter: [184, 0] + access: [] + - target_room: 180 + entrance: 345 + teleporter: [185, 0] + access: [] +- name: Pazuzu Tower 1F Boxes Room + id: 167 + game_objects: + - name: "Pazuzu's Tower 1F - Descent Bomb Wall West Box" + object_id: 0xCA + type: "Box" + access: ["Bomb"] + - name: "Pazuzu's Tower 1F - Descent Bomb Wall Center Box" + object_id: 0xCB + type: "Box" + access: ["Bomb"] + - name: "Pazuzu's Tower 1F - Descent Bomb Wall East Box" + object_id: 0xCC + type: "Box" + access: ["Bomb"] + - name: "Pazuzu's Tower 1F - Descent Box" + object_id: 0xCD + type: "Box" + access: [] + links: + - target_room: 169 + entrance: 349 + teleporter: [187, 0] + access: [] +- name: Pazuzu Tower 1F Southern Platform + id: 168 + game_objects: [] + links: + - target_room: 169 + entrance: 346 + teleporter: [186, 0] + access: [] + - target_room: 166 + access: ["DragonClaw"] +- name: Pazuzu 2F + id: 169 + game_objects: + - name: "Pazuzu's Tower 2F - East Room West Box" + object_id: 0xCE + type: "Box" + access: [] + - name: "Pazuzu's Tower 2F - East Room East Box" + object_id: 0xCF + type: "Box" + access: [] + - name: "Pazuzu 2F Lock" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu2FLock"] + access: ["Axe"] + - name: "Pazuzu 2F" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu2F"] + access: ["Bomb"] + links: + - target_room: 183 + entrance: 350 + teleporter: [188, 0] + access: [] + - target_room: 168 + entrance: 351 + teleporter: [189, 0] + access: [] + - target_room: 167 + entrance: 352 + teleporter: [190, 0] + access: [] + - target_room: 171 + entrance: 353 + teleporter: [191, 0] + access: [] +- name: Pazuzu 3F Main Room + id: 170 + game_objects: + - name: "Pazuzu's Tower 3F - Guest Room West Box" + object_id: 0xD0 + type: "Box" + access: [] + - name: "Pazuzu's Tower 3F - Guest Room East Box" + object_id: 0xD1 + type: "Box" + access: [] + - name: "Pazuzu 3F" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu3F"] + access: [] + links: + - target_room: 180 + entrance: 356 + teleporter: [192, 0] + access: [] + - target_room: 181 + entrance: 357 + teleporter: [193, 0] + access: [] +- name: Pazuzu 3F Central Island + id: 171 + game_objects: [] + links: + - target_room: 169 + entrance: 360 + teleporter: [194, 0] + access: [] + - target_room: 170 + access: ["DragonClaw"] + - target_room: 172 + access: ["DragonClaw"] +- name: Pazuzu 3F Southern Island + id: 172 + game_objects: + - name: "Pazuzu's Tower 3F - South Ledge Box" + object_id: 0xD2 + type: "Box" + access: [] + links: + - target_room: 173 + entrance: 361 + teleporter: [195, 0] + access: [] + - target_room: 171 + access: ["DragonClaw"] +- name: Pazuzu 4F + id: 173 + game_objects: + - name: "Pazuzu's Tower 4F - Elevator West Box" + object_id: 0xD3 + type: "Box" + access: ["Bomb"] + - name: "Pazuzu's Tower 4F - Elevator East Box" + object_id: 0xD4 + type: "Box" + access: ["Bomb"] + - name: "Pazuzu's Tower 4F - East Storage Room Chest" + object_id: 0x18 + type: "Chest" + access: [] + - name: "Pazuzu 4F Lock" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu4FLock"] + access: ["Axe"] + - name: "Pazuzu 4F" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu4F"] + access: ["Bomb"] + links: + - target_room: 183 + entrance: 362 + teleporter: [196, 0] + access: [] + - target_room: 184 + entrance: 363 + teleporter: [197, 0] + access: [] + - target_room: 172 + entrance: 364 + teleporter: [198, 0] + access: [] + - target_room: 175 + entrance: 365 + teleporter: [199, 0] + access: [] +- name: Pazuzu 5F Pazuzu Loop + id: 174 + game_objects: + - name: "Pazuzu 5F" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu5F"] + access: [] + links: + - target_room: 181 + entrance: 368 + teleporter: [200, 0] + access: [] + - target_room: 182 + entrance: 369 + teleporter: [201, 0] + access: [] +- name: Pazuzu 5F Upper Loop + id: 175 + game_objects: + - name: "Pazuzu's Tower 5F - North Box" + object_id: 0xD5 + type: "Box" + access: [] + - name: "Pazuzu's Tower 5F - South Box" + object_id: 0xD6 + type: "Box" + access: [] + links: + - target_room: 173 + entrance: 370 + teleporter: [202, 0] + access: [] + - target_room: 176 + entrance: 371 + teleporter: [203, 0] + access: [] +- name: Pazuzu 6F + id: 176 + game_objects: + - name: "Pazuzu's Tower 6F - Box" + object_id: 0xD7 + type: "Box" + access: [] + - name: "Pazuzu's Tower 6F - Chest" + object_id: 0x19 + type: "Chest" + access: [] + - name: "Pazuzu 6F Lock" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu6FLock"] + access: ["Bomb", "Axe"] + - name: "Pazuzu 6F" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu6F"] + access: ["Bomb"] + links: + - target_room: 184 + entrance: 374 + teleporter: [204, 0] + access: [] + - target_room: 175 + entrance: 375 + teleporter: [205, 0] + access: [] + - target_room: 178 + entrance: 376 + teleporter: [206, 0] + access: [] + - target_room: 178 + entrance: 377 + teleporter: [207, 0] + access: [] +- name: Pazuzu 7F Southwest Area + id: 177 + game_objects: [] + links: + - target_room: 182 + entrance: 380 + teleporter: [26, 0] + access: [] + - target_room: 178 + access: ["DragonClaw"] +- name: Pazuzu 7F Rest of the Area + id: 178 + game_objects: [] + links: + - target_room: 177 + access: ["DragonClaw"] + - target_room: 176 + entrance: 381 + teleporter: [27, 0] + access: [] + - target_room: 176 + entrance: 382 + teleporter: [28, 0] + access: [] + - target_room: 179 + access: ["DragonClaw", "Pazuzu2FLock", "Pazuzu4FLock", "Pazuzu6FLock", "Pazuzu1F", "Pazuzu2F", "Pazuzu3F", "Pazuzu4F", "Pazuzu5F", "Pazuzu6F"] +- name: Pazuzu 7F Sky Room + id: 179 + game_objects: + - name: "Pazuzu's Tower 7F - Pazuzu Chest" + object_id: 0x1A + type: "Chest" + access: [] + - name: "Pazuzu" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu"] + access: ["Pazuzu2FLock", "Pazuzu4FLock", "Pazuzu6FLock", "Pazuzu1F", "Pazuzu2F", "Pazuzu3F", "Pazuzu4F", "Pazuzu5F", "Pazuzu6F"] + links: + - target_room: 178 + access: ["DragonClaw"] +- name: Pazuzu 1F to 3F + id: 180 + game_objects: [] + links: + - target_room: 166 + entrance: 385 + teleporter: [29, 0] + access: [] + - target_room: 170 + entrance: 386 + teleporter: [30, 0] + access: [] +- name: Pazuzu 3F to 5F + id: 181 + game_objects: [] + links: + - target_room: 170 + entrance: 387 + teleporter: [40, 0] + access: [] + - target_room: 174 + entrance: 388 + teleporter: [41, 0] + access: [] +- name: Pazuzu 5F to 7F + id: 182 + game_objects: [] + links: + - target_room: 174 + entrance: 389 + teleporter: [38, 0] + access: [] + - target_room: 177 + entrance: 390 + teleporter: [39, 0] + access: [] +- name: Pazuzu 2F to 4F + id: 183 + game_objects: [] + links: + - target_room: 169 + entrance: 391 + teleporter: [21, 0] + access: [] + - target_room: 173 + entrance: 392 + teleporter: [22, 0] + access: [] +- name: Pazuzu 4F to 6F + id: 184 + game_objects: [] + links: + - target_room: 173 + entrance: 393 + teleporter: [2, 0] + access: [] + - target_room: 176 + entrance: 394 + teleporter: [3, 0] + access: [] +- name: Light Temple + id: 185 + game_objects: + - name: "Light Temple - Box" + object_id: 0xD8 + type: "Box" + access: [] + links: + - target_room: 230 + entrance: 395 + teleporter: [19, 6] + access: [] + - target_room: 153 + entrance: 396 + teleporter: [70, 8] + access: ["MobiusCrest"] +- name: Ship Dock + id: 186 + game_objects: + - name: "Ship Dock Access" + object_id: 0 + type: "Trigger" + on_trigger: ["ShipDockAccess"] + access: [] + links: + - target_room: 228 + entrance: 399 + teleporter: [17, 6] + access: [] + - target_room: 162 + entrance: 397 + teleporter: [61, 8] + access: ["MobiusCrest"] +- name: Mac Ship Deck + id: 187 + game_objects: + - name: "Mac Ship Steering Wheel" + object_id: 00 + type: "Trigger" + on_trigger: ["ShipSteeringWheel"] + access: [] + - name: "Mac's Ship Deck - North Box" + object_id: 0xD9 + type: "Box" + access: [] + - name: "Mac's Ship Deck - Center Box" + object_id: 0xDA + type: "Box" + access: [] + - name: "Mac's Ship Deck - South Box" + object_id: 0xDB + type: "Box" + access: [] + links: + - target_room: 229 + entrance: 400 + teleporter: [37, 8] + access: [] + - target_room: 188 + entrance: 401 + teleporter: [50, 8] + access: [] + - target_room: 188 + entrance: 402 + teleporter: [51, 8] + access: [] + - target_room: 188 + entrance: 403 + teleporter: [52, 8] + access: [] + - target_room: 189 + entrance: 404 + teleporter: [53, 8] + access: [] +- name: Mac Ship B1 Outer Ring + id: 188 + game_objects: + - name: "Mac's Ship B1 - Northwest Hook Platform Box" + object_id: 0xE4 + type: "Box" + access: ["DragonClaw"] + - name: "Mac's Ship B1 - Center Hook Platform Box" + object_id: 0xE5 + type: "Box" + access: ["DragonClaw"] + links: + - target_room: 187 + entrance: 405 + teleporter: [208, 0] + access: [] + - target_room: 187 + entrance: 406 + teleporter: [175, 0] + access: [] + - target_room: 187 + entrance: 407 + teleporter: [172, 0] + access: [] + - target_room: 193 + entrance: 408 + teleporter: [88, 0] + access: [] + - target_room: 193 + access: [] +- name: Mac Ship B1 Square Room + id: 189 + game_objects: [] + links: + - target_room: 187 + entrance: 409 + teleporter: [141, 0] + access: [] + - target_room: 192 + entrance: 410 + teleporter: [87, 0] + access: [] +- name: Mac Ship B1 Central Corridor + id: 190 + game_objects: + - name: "Mac's Ship B1 - Central Corridor Box" + object_id: 0xE6 + type: "Box" + access: [] + links: + - target_room: 192 + entrance: 413 + teleporter: [86, 0] + access: [] + - target_room: 191 + entrance: 412 + teleporter: [102, 0] + access: [] + - target_room: 193 + access: [] +- name: Mac Ship B2 South Corridor + id: 191 + game_objects: [] + links: + - target_room: 190 + entrance: 415 + teleporter: [55, 8] + access: [] + - target_room: 194 + entrance: 414 + teleporter: [57, 1] + access: [] +- name: Mac Ship B2 North Corridor + id: 192 + game_objects: [] + links: + - target_room: 190 + entrance: 416 + teleporter: [56, 8] + access: [] + - target_room: 189 + entrance: 417 + teleporter: [57, 8] + access: [] +- name: Mac Ship B2 Outer Ring + id: 193 + game_objects: + - name: "Mac's Ship B2 - Barrel Room South Box" + object_id: 0xDF + type: "Box" + access: [] + - name: "Mac's Ship B2 - Barrel Room North Box" + object_id: 0xE0 + type: "Box" + access: [] + - name: "Mac's Ship B2 - Southwest Room Box" + object_id: 0xE1 + type: "Box" + access: [] + - name: "Mac's Ship B2 - Southeast Room Box" + object_id: 0xE2 + type: "Box" + access: [] + links: + - target_room: 188 + entrance: 418 + teleporter: [58, 8] + access: [] +- name: Mac Ship B1 Mac Room + id: 194 + game_objects: + - name: "Mac's Ship B1 - Mac Room Chest" + object_id: 0x1B + type: "Chest" + access: [] + - name: "Captain Mac" + object_id: 0 + type: "Trigger" + on_trigger: ["ShipLoaned"] + access: ["CaptainCap"] + links: + - target_room: 191 + entrance: 424 + teleporter: [101, 0] + access: [] +- name: Doom Castle Corridor of Destiny + id: 195 + game_objects: [] + links: + - target_room: 201 + entrance: 428 + teleporter: [84, 0] + access: [] + - target_room: 196 + entrance: 429 + teleporter: [35, 2] + access: [] + - target_room: 197 + entrance: 430 + teleporter: [209, 0] + access: ["StoneGolem"] + - target_room: 198 + entrance: 431 + teleporter: [211, 0] + access: ["StoneGolem", "TwinheadWyvern"] + - target_room: 199 + entrance: 432 + teleporter: [13, 2] + access: ["StoneGolem", "TwinheadWyvern", "Zuh"] +- name: Doom Castle Ice Floor + id: 196 + game_objects: + - name: "Doom Castle 4F - Northwest Room Box" + object_id: 0xE7 + type: "Box" + access: ["Sword", "DragonClaw"] + - name: "Doom Castle 4F - Southwest Room Box" + object_id: 0xE8 + type: "Box" + access: ["Sword", "DragonClaw"] + - name: "Doom Castle 4F - Northeast Room Box" + object_id: 0xE9 + type: "Box" + access: ["Sword"] + - name: "Doom Castle 4F - Southeast Room Box" + object_id: 0xEA + type: "Box" + access: ["Sword", "DragonClaw"] + - name: "Stone Golem" + object_id: 0 + type: "Trigger" + on_trigger: ["StoneGolem"] + access: ["Sword", "DragonClaw"] + links: + - target_room: 195 + entrance: 433 + teleporter: [109, 3] + access: [] +- name: Doom Castle Lava Floor + id: 197 + game_objects: + - name: "Doom Castle 5F - North Left Box" + object_id: 0xEB + type: "Box" + access: ["DragonClaw"] + - name: "Doom Castle 5F - North Right Box" + object_id: 0xEC + type: "Box" + access: ["DragonClaw"] + - name: "Doom Castle 5F - South Left Box" + object_id: 0xED + type: "Box" + access: ["DragonClaw"] + - name: "Doom Castle 5F - South Right Box" + object_id: 0xEE + type: "Box" + access: ["DragonClaw"] + - name: "Twinhead Wyvern" + object_id: 0 + type: "Trigger" + on_trigger: ["TwinheadWyvern"] + access: ["DragonClaw"] + links: + - target_room: 195 + entrance: 434 + teleporter: [210, 0] + access: [] +- name: Doom Castle Sky Floor + id: 198 + game_objects: + - name: "Doom Castle 6F - West Box" + object_id: 0xEF + type: "Box" + access: [] + - name: "Doom Castle 6F - East Box" + object_id: 0xF0 + type: "Box" + access: [] + - name: "Zuh" + object_id: 0 + type: "Trigger" + on_trigger: ["Zuh"] + access: ["DragonClaw"] + links: + - target_room: 195 + entrance: 435 + teleporter: [212, 0] + access: [] + - target_room: 197 + access: [] +- name: Doom Castle Hero Room + id: 199 + game_objects: + - name: "Doom Castle Hero Chest 01" + object_id: 0xF2 + type: "Chest" + access: [] + - name: "Doom Castle Hero Chest 02" + object_id: 0xF3 + type: "Chest" + access: [] + - name: "Doom Castle Hero Chest 03" + object_id: 0xF4 + type: "Chest" + access: [] + - name: "Doom Castle Hero Chest 04" + object_id: 0xF5 + type: "Chest" + access: [] + links: + - target_room: 200 + entrance: 436 + teleporter: [54, 0] + access: [] + - target_room: 195 + entrance: 441 + teleporter: [110, 3] + access: [] +- name: Doom Castle Dark King Room + id: 200 + game_objects: [] + links: + - target_room: 199 + entrance: 442 + teleporter: [52, 0] + access: [] diff --git a/worlds/ffmq/data/settings.yaml b/worlds/ffmq/data/settings.yaml new file mode 100644 index 000000000000..aa973ee22b0b --- /dev/null +++ b/worlds/ffmq/data/settings.yaml @@ -0,0 +1,140 @@ +# YAML Preset file for FFMQR +Final Fantasy Mystic Quest: + enemies_density: + All: 0 + ThreeQuarter: 0 + Half: 0 + Quarter: 0 + None: 0 + chests_shuffle: + Prioritize: 0 + Include: 0 + shuffle_boxes_content: + true: 0 + false: 0 + npcs_shuffle: + Prioritize: 0 + Include: 0 + Exclude: 0 + battlefields_shuffle: + Prioritize: 0 + Include: 0 + Exclude: 0 + logic_options: + Friendly: 0 + Standard: 0 + Expert: 0 + shuffle_enemies_position: + true: 0 + false: 0 + enemies_scaling_lower: + Quarter: 0 + Half: 0 + ThreeQuarter: 0 + Normal: 0 + OneAndQuarter: 0 + OneAndHalf: 0 + Double: 0 + DoubleAndHalf: 0 + Triple: 0 + enemies_scaling_upper: + Quarter: 0 + Half: 0 + ThreeQuarter: 0 + Normal: 0 + OneAndQuarter: 0 + OneAndHalf: 0 + Double: 0 + DoubleAndHalf: 0 + Triple: 0 + bosses_scaling_lower: + Quarter: 0 + Half: 0 + ThreeQuarter: 0 + Normal: 0 + OneAndQuarter: 0 + OneAndHalf: 0 + Double: 0 + DoubleAndHalf: 0 + Triple: 0 + bosses_scaling_upper: + Quarter: 0 + Half: 0 + ThreeQuarter: 0 + Normal: 0 + OneAndQuarter: 0 + OneAndHalf: 0 + Double: 0 + DoubleAndHalf: 0 + Triple: 0 + enemizer_attacks: + Normal: 0 + Safe: 0 + Chaos: 0 + SelfDestruct: 0 + SimpleShuffle: 0 + leveling_curve: + Half: 0 + Normal: 0 + OneAndHalf: 0 + Double: 0 + DoubleHalf: 0 + Triple: 0 + Quadruple: 0 + battles_quantity: + Ten: 0 + Seven: 0 + Five: 0 + Three: 0 + One: 0 + RandomHigh: 0 + RandomLow: 0 + shuffle_battlefield_rewards: + true: 0 + false: 0 + random_starting_weapon: + true: 0 + false: 0 + progressive_gear: + true: 0 + false: 0 + tweaked_dungeons: + true: 0 + false: 0 + doom_castle_mode: + Standard: 0 + BossRush: 0 + DarkKingOnly: 0 + doom_castle_shortcut: + true: 0 + false: 0 + sky_coin_mode: + Standard: 0 + StartWith: 0 + SaveTheCrystals: 0 + ShatteredSkyCoin: 0 + sky_coin_fragments_qty: + Low16: 0 + Mid24: 0 + High32: 0 + RandomNarrow: 0 + RandomWide: 0 + enable_spoilers: + true: 0 + false: 0 + progressive_formations: + Disabled: 0 + RegionsStrict: 0 + RegionsKeepType: 0 + map_shuffling: + None: 0 + Overworld: 0 + Dungeons: 0 + OverworldDungeons: 0 + Everything: 0 + crest_shuffle: + true: 0 + false: 0 +description: Generated by Archipelago +game: Final Fantasy Mystic Quest +name: Player diff --git a/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md new file mode 100644 index 000000000000..dd4ea354fab1 --- /dev/null +++ b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md @@ -0,0 +1,33 @@ +# Final Fantasy Mystic Quest + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Besides items being shuffled, you have multiple options for shuffling maps, crest warps, and battlefield locations. +There are a number of other options for tweaking the difficulty of the game. + +## What items and locations get shuffled? + +Items received normally through chests, from NPCs, or battlefields are shuffled. Optionally, you may also include +the items from brown boxes. + +## Which items can be in another player's world? + +Any of the items which can be shuffled may also be placed into another player's world. + +## What does another world's item look like in Final Fantasy Mystic Quest? + +For locations that are originally boxes or chests, they will appear as a box if the item in it is categorized as a +filler item, and a chest if it contains a useful or advancement item. Trap items may randomly appear as a box or chest. +When opening a chest with an item for another player, you will see the Archipelago icon and it will tell you you've +found an "Archipelago Item" + +## When the player receives an item, what happens? + +A dialogue box will open to show you the item you've received. You will not receive items while you are in battle, +menus, or the overworld (except sometimes when closing the menu). + diff --git a/worlds/ffmq/docs/setup_en.md b/worlds/ffmq/docs/setup_en.md new file mode 100644 index 000000000000..9d9088dbc232 --- /dev/null +++ b/worlds/ffmq/docs/setup_en.md @@ -0,0 +1,162 @@ +# Final Fantasy Mystic Quest Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client` + +- Hardware or software capable of loading and playing SNES ROM files + - An emulator capable of connecting to SNI such as: + - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), + - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html) + - RetroArch 1.10.1 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or, + - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other + compatible hardware + +- Your legally obtained Final Fantasy Mystic Quest 1.1 ROM file, probably named `Final Fantasy - Mystic Quest (U) (V1.1).sfc` +The Archipelago community cannot supply you with this. + +## Installation Procedures + +### Windows Setup + +1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this, + or you are on an older version, you may run the installer again to install the SNI Client. +2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM + files. + 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. + 2. Right-click on a ROM file and select **Open with...** + 3. Check the box next to **Always use this app to open .sfc files** + 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC** + 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you + extracted in step one. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The Player Settings page on the website allows you to configure your personal settings and export a config file from +them. Player settings page: [Final Fantasy Mystic Quest Player Settings Page](/games/Final%20Fantasy%20Mystic%20Quest/player-settings) + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/mysterycheck) + +## Generating a Single-Player Game + +1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. + - Player Settings page: [Final Fantasy Mystic Quest Player Settings Page](/games/Final%20Fantasy%20Mystic%20Quest/player-settings) +2. You will be presented with a "Seed Info" page. +3. Click the "Create New Room" link. +4. You will be presented with a server page, from which you can download your `.apmq` patch file. +5. Go to the [FFMQR website](https://ffmqrando.net/Archipelago) and select your Final Fantasy Mystic Quest 1.1 ROM +and the .apmq file you received, choose optional preferences, and click `Generate` to get your patched ROM. +7. Since this is a single-player game, you will no longer need the client, so feel free to close it. + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM + +When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing +everyone's patch files. Your patch file should have a `.apmq` extension. + +Go to the [FFMQR website](https://ffmqrando.net/Archipelago) and select your Final Fantasy Mystic Quest 1.1 ROM +and the .apmq file you received, choose optional preferences, and click `Generate` to get your patched ROM. + +Manually launch the SNI Client, and run the patched ROM in your chosen software or hardware. + +### Connect to the client + +#### With an emulator + +When the client launched automatically, SNI should have also automatically launched in the background. If this is its +first time launching, you may be prompted to allow it to communicate through the Windows Firewall. + +##### snes9x-rr + +1. Load your ROM file if it hasn't already been loaded. +2. Click on the File menu and hover on **Lua Scripting** +3. Click on **New Lua Script Window...** +4. In the new window, click **Browse...** +5. Select the connector lua file included with your client + - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the + emulator is 64-bit or 32-bit. +6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of +the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install. + +##### BizHawk + +1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these + menu options: + `Config --> Cores --> SNES --> BSNES` + Once you have changed the loaded core, you must restart BizHawk. +2. Load your ROM file if it hasn't already been loaded. +3. Click on the Tools menu and click on **Lua Console** +4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`. +5. Select the `Connector.lua` file included with your client + - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the + emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. + +##### RetroArch 1.10.1 or newer + +You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.1. + +1. Enter the RetroArch main menu screen. +2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. +3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default + Network Command Port at 55355. + +![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury + Performance)". + +When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to +read ROM data. + +#### With hardware + +This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do +this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES +releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases) + +Other hardware may find helpful information on the usb2snes platforms +page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms) + +1. Close your emulator, which may have auto-launched. +2. Power on your device and load the ROM. + +### Connect to the Archipelago Server + +The patch file which launched your client should have automatically connected you to the AP Server. There are a few +reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the +client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it +into the "Server" input field then press enter. + +The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". + +### Play the game + +When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on +successfully joining a multiworld game! + +## Hosting a MultiWorld game + +The recommended way to host a game is to use our hosting service. The process is relatively simple: + +1. Collect config files from your players. +2. Create a zip file containing your players' config files. +3. Upload that zip file to the Generate page above. + - Generate page: [WebHost Seed Generation Page](/generate) +4. Wait a moment while the seed is generated. +5. When the seed is generated, you will be redirected to a "Seed Info" page. +6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so + they may download their patch files from there. +7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all + players in the game. Any observers may also be given the link to this page. +8. Once all players have joined, you may begin playing. From ce2f9312ca18106168870c8cf836dfd545b7488b Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 28 Nov 2023 13:50:12 -0800 Subject: [PATCH 101/104] BizHawkClient: Change `open_connection` to use 127.0.0.1 instead of localhost (#2525) When using localhost on mac, both ipv4 and ipv6 are tried and raise separate errors which are combined by asyncio and difficult/inelegant to handle. Python 3.12 adds the argument all_errors, which would make this easier. --- worlds/_bizhawk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py index cddfde4ff37f..94a9ce1ddf04 100644 --- a/worlds/_bizhawk/__init__.py +++ b/worlds/_bizhawk/__init__.py @@ -97,7 +97,7 @@ async def connect(ctx: BizHawkContext) -> bool: for port in ports: try: - ctx.streams = await asyncio.open_connection("localhost", port) + ctx.streams = await asyncio.open_connection("127.0.0.1", port) ctx.connection_status = ConnectionStatus.TENTATIVE ctx._port = port return True From 737686a88d54f8ace38f8b577d54d55f5b6c4250 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 28 Nov 2023 13:56:27 -0800 Subject: [PATCH 102/104] BizHawkClient: Use `local_path` when autolaunching BizHawk with lua script (#2526) * BizHawkClient: Change autolaunch path to lua script to use local_path * BizHawkClient: Remove unnecessary call to os.path.join and linting --- worlds/_bizhawk/context.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 2699b0f5f106..4ee6e24f591d 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -208,19 +208,30 @@ async def _run_game(rom: str): if auto_start is True: emuhawk_path = Utils.get_settings().bizhawkclient_options.emuhawk_path - subprocess.Popen([emuhawk_path, "--lua=data/lua/connector_bizhawk_generic.lua", os.path.realpath(rom)], - cwd=Utils.local_path("."), - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + subprocess.Popen( + [ + emuhawk_path, + f"--lua={Utils.local_path('data', 'lua', 'connector_bizhawk_generic.lua')}", + os.path.realpath(rom), + ], + cwd=Utils.local_path("."), + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) elif isinstance(auto_start, str): import shlex - subprocess.Popen([*shlex.split(auto_start), os.path.realpath(rom)], - cwd=Utils.local_path("."), - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + subprocess.Popen( + [ + *shlex.split(auto_start), + os.path.realpath(rom) + ], + cwd=Utils.local_path("."), + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) async def _patch_and_run_game(patch_file: str): From 39969abd6ad6aa715d979ef6eece1f242e58e575 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Wed, 29 Nov 2023 00:11:17 +0100 Subject: [PATCH 103/104] WebHostLib: fix NamedRange in options presets (#2528) --- WebHostLib/static/assets/player-options.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/player-options.js b/WebHostLib/static/assets/player-options.js index 37ba7f98ff19..92cd6c43f3cc 100644 --- a/WebHostLib/static/assets/player-options.js +++ b/WebHostLib/static/assets/player-options.js @@ -369,7 +369,7 @@ const setPresets = (optionsData, presetName) => { break; } - case 'special_range': { + case 'named_range': { const selectElement = document.querySelector(`select[data-key='${option}']`); const rangeElement = document.querySelector(`input[data-key='${option}']`); const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`); From 6c5f8250fba413dd9188041c58a132b3aa7981bd Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Wed, 29 Nov 2023 01:19:42 -0500 Subject: [PATCH 104/104] TLOZ: Use the proper location name lookup (#2529) --- Zelda1Client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Zelda1Client.py b/Zelda1Client.py index db3d3519aa60..cd76a0a5ca78 100644 --- a/Zelda1Client.py +++ b/Zelda1Client.py @@ -13,7 +13,6 @@ import Utils from Utils import async_start -from worlds import lookup_any_location_id_to_name from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \ get_base_parser @@ -153,7 +152,7 @@ def get_payload(ctx: ZeldaContext): def reconcile_shops(ctx: ZeldaContext): - checked_location_names = [lookup_any_location_id_to_name[location] for location in ctx.checked_locations] + checked_location_names = [ctx.location_names[location] for location in ctx.checked_locations] shops = [location for location in checked_location_names if "Shop" in location] left_slots = [shop for shop in shops if "Left" in shop] middle_slots = [shop for shop in shops if "Middle" in shop] @@ -191,7 +190,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone= locations_checked = [] location = None for location in ctx.missing_locations: - location_name = lookup_any_location_id_to_name[location] + location_name = ctx.location_names[location] if location_name in Locations.overworld_locations and zone == "overworld": status = locations_array[Locations.major_location_offsets[location_name]]