From e1f1bf83c246ad9f0a189a9a382644594a9bd67b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 5 Nov 2023 06:15:39 +0100 Subject: [PATCH 01/45] Core: Running item Plando dot (#2405) --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 691b88b13706..7b42a89d12be 100644 --- a/Main.py +++ b/Main.py @@ -265,7 +265,7 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ if any(world.item_links.values()): world._all_state = None - logger.info("Running Item Plando") + logger.info("Running Item Plando.") distribute_planned(world) From 84fb2f58faebb975a28a8e561fe836056b908a23 Mon Sep 17 00:00:00 2001 From: axe-y <58866768+axe-y@users.noreply.github.com> Date: Mon, 6 Nov 2023 00:01:49 -0500 Subject: [PATCH 02/45] DLC Quest Stardew: bug (#2423) --- worlds/dlcquest/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index e4e0a29274da..c22b7cd9847b 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -3,7 +3,7 @@ from BaseClasses import Tutorial, CollectionState from worlds.AutoWorld import WebWorld, World from . import Options -from .Items import DLCQuestItem, ItemData, create_items, item_table +from .Items import DLCQuestItem, ItemData, create_items, item_table, items_by_group, Group from .Locations import DLCQuestLocation, location_table from .Options import DLCQuestOptions from .Regions import create_regions @@ -60,7 +60,9 @@ def create_items(self): created_items = create_items(self, self.options, locations_count + len(items_to_exclude), self.multiworld.random) self.multiworld.itempool += created_items - self.multiworld.early_items[self.player]["Movement Pack"] = 1 + + if self.options.campaign == Options.Campaign.option_basic or self.options.campaign == Options.Campaign.option_both: + self.multiworld.early_items[self.player]["Movement Pack"] = 1 for item in items_to_exclude: if item in self.multiworld.itempool: @@ -77,6 +79,10 @@ def create_item(self, item: Union[str, ItemData]) -> DLCQuestItem: return DLCQuestItem(item.name, item.classification, item.code, self.player) + def get_filler_item_name(self) -> str: + trap = self.multiworld.random.choice(items_by_group[Group.Trap]) + return trap.name + def fill_slot_data(self): options_dict = self.options.as_dict( "death_link", "ending_choice", "campaign", "coinsanity", "item_shuffle" 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 03/45] 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 04/45] 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 05/45] 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 06/45] 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 07/45] 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 08/45] 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 09/45] 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 10/45] 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 11/45] 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 12/45] 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 13/45] 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 14/45] 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 15/45] 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 16/45] 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 17/45] 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 18/45] 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 19/45] 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 20/45] 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 43/45] 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 44/45] 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 45/45] 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