diff --git a/BaseClasses.py b/BaseClasses.py index 7c12a94dea65..26cdfb528569 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -487,8 +487,10 @@ def get_placeable_locations(self, state=None, player=None) -> List[Location]: def get_unfilled_locations_for_players(self, location_names: List[str], players: Iterable[int]): for player in players: if not location_names: - location_names = [location.name for location in self.get_unfilled_locations(player)] - for location_name in location_names: + valid_locations = [location.name for location in self.get_unfilled_locations(player)] + else: + valid_locations = location_names + for location_name in valid_locations: location = self._location_cache.get((location_name, player), None) if location is not None and location.item is None: yield location @@ -851,14 +853,6 @@ def can_reach(self, state: CollectionState) -> bool: state.update_reachable_regions(self.player) return self in state.reachable_regions[self.player] - def can_reach_private(self, state: CollectionState) -> bool: - for entrance in self.entrances: - if entrance.can_reach(state): - if not self in state.path: - state.path[self] = (self.name, state.path.get(entrance, None)) - return True - return False - @property def hint_text(self) -> str: return self._hint_text if self._hint_text else self.name diff --git a/LttPAdjuster.py b/LttPAdjuster.py index d1c03bd49eeb..802ec47dd1f0 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -25,7 +25,7 @@ from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \ - get_adjuster_settings, tkinter_center_window, init_logging + get_adjuster_settings, get_adjuster_settings_no_defaults, tkinter_center_window, init_logging GAME_ALTTP = "A Link to the Past" @@ -43,6 +43,47 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): def _get_help_string(self, action): return textwrap.dedent(action.help) +# See argparse.BooleanOptionalAction +class BooleanOptionalActionWithDisable(argparse.Action): + def __init__(self, + option_strings, + dest, + default=None, + type=None, + choices=None, + required=False, + help=None, + metavar=None): + + _option_strings = [] + for option_string in option_strings: + _option_strings.append(option_string) + + if option_string.startswith('--'): + option_string = '--disable' + option_string[2:] + _option_strings.append(option_string) + + if help is not None and default is not None: + help += " (default: %(default)s)" + + super().__init__( + option_strings=_option_strings, + dest=dest, + nargs=0, + default=default, + type=type, + choices=choices, + required=required, + help=help, + metavar=metavar) + + def __call__(self, parser, namespace, values, option_string=None): + if option_string in self.option_strings: + setattr(namespace, self.dest, not option_string.startswith('--disable')) + + def format_usage(self): + return ' | '.join(self.option_strings) + def get_argparser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) @@ -52,6 +93,8 @@ def get_argparser() -> argparse.ArgumentParser: help='Path to an ALttP Japan(1.0) rom to use as a base.') parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.') + parser.add_argument('--auto_apply', default='ask', + choices=['ask', 'always', 'never'], help='Whether or not to apply settings automatically in the future.') parser.add_argument('--menuspeed', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'], help='''\ @@ -61,7 +104,7 @@ def get_argparser() -> argparse.ArgumentParser: parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true') parser.add_argument('--deathlink', help='Enable DeathLink system.', action='store_true') parser.add_argument('--allowcollect', help='Allow collection of other player items', action='store_true') - parser.add_argument('--disablemusic', help='Disables game music.', action='store_true') + parser.add_argument('--music', default=True, help='Enables/Disables game music.', action=BooleanOptionalActionWithDisable) parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?', choices=['normal', 'hide_goal', 'hide_required', 'hide_both'], help='''\ @@ -104,21 +147,23 @@ def get_argparser() -> argparse.ArgumentParser: Alternatively, can be a ALttP Rom patched with a Link sprite that will be extracted. ''') + parser.add_argument('--sprite_pool', nargs='+', default=[], help=''' + A list of sprites to pull from. + ''') parser.add_argument('--oof', help='''\ Path to a sound effect to replace Link's "oof" sound. Needs to be in a .brr format and have a length of no more than 2673 bytes, created from a 16-bit signed PCM .wav at 12khz. https://github.com/boldowa/snesbrr ''') - parser.add_argument('--names', default='', type=str) parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.') return parser def main(): parser = get_argparser() - args = parser.parse_args() - args.music = not args.disablemusic + args = parser.parse_args(namespace=get_adjuster_settings_no_defaults(GAME_ALTTP)) + # set up logger loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[ args.loglevel] @@ -530,9 +575,6 @@ def hide(self): def get_rom_frame(parent=None): adjuster_settings = get_adjuster_settings(GAME_ALTTP) - if not adjuster_settings: - adjuster_settings = Namespace() - adjuster_settings.baserom = "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" romFrame = Frame(parent) baseRomLabel = Label(romFrame, text='LttP Base Rom: ') @@ -560,33 +602,8 @@ def RomSelect(): return romFrame, romVar - def get_rom_options_frame(parent=None): adjuster_settings = get_adjuster_settings(GAME_ALTTP) - defaults = { - "auto_apply": 'ask', - "music": True, - "reduceflashing": True, - "deathlink": False, - "sprite": None, - "oof": None, - "quickswap": True, - "menuspeed": 'normal', - "heartcolor": 'red', - "heartbeep": 'normal', - "ow_palettes": 'default', - "uw_palettes": 'default', - "hud_palettes": 'default', - "sword_palettes": 'default', - "shield_palettes": 'default', - "sprite_pool": [], - "allowcollect": False, - } - if not adjuster_settings: - adjuster_settings = Namespace() - for key, defaultvalue in defaults.items(): - if not hasattr(adjuster_settings, key): - setattr(adjuster_settings, key, defaultvalue) romOptionsFrame = LabelFrame(parent, text="Rom options") romOptionsFrame.columnconfigure(0, weight=1) diff --git a/MMBN3Client.py b/MMBN3Client.py index d8ee581bd453..3f7474a6fd50 100644 --- a/MMBN3Client.py +++ b/MMBN3Client.py @@ -71,6 +71,7 @@ def __init__(self, server_address, password): self.auth_name = None self.slot_data = dict() self.patching_error = False + self.sent_hints = [] async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -175,13 +176,16 @@ async def parse_payload(payload: dict, ctx: MMBN3Context, force: bool): # If trade hinting is enabled, send scout checks if ctx.slot_data.get("trade_quest_hinting", 0) == 2: - scouted_locs = [loc.id for loc in scoutable_locations + trade_bits = [loc.id for loc in scoutable_locations if check_location_scouted(loc, payload["locations"])] - await ctx.send_msgs([{ - "cmd": "LocationScouts", - "locations": scouted_locs, - "create_as_hint": 2 - }]) + scouted_locs = [loc for loc in trade_bits if loc not in ctx.sent_hints] + if len(scouted_locs) > 0: + ctx.sent_hints.extend(scouted_locs) + await ctx.send_msgs([{ + "cmd": "LocationScouts", + "locations": scouted_locs, + "create_as_hint": 2 + }]) def check_location_packet(location, memory): diff --git a/Main.py b/Main.py index fe56dc7d9e09..860be6347c66 100644 --- a/Main.py +++ b/Main.py @@ -392,7 +392,7 @@ def precollect_hint(location): f.write(bytes([3])) # version of format f.write(multidata) - multidata_task = pool.submit(write_multidata) + output_file_futures.append(pool.submit(write_multidata)) if not check_accessibility_task.result(): if not world.can_beat_game(): raise Exception("Game appears as unbeatable. Aborting.") @@ -400,7 +400,6 @@ def precollect_hint(location): logger.warning("Location Accessibility requirements not fulfilled.") # retrieve exceptions via .result() if they occurred. - multidata_task.result() for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1): if i % 10 == 0 or i == len(output_file_futures): logger.info(f'Generating output files ({i}/{len(output_file_futures)}).') diff --git a/MultiServer.py b/MultiServer.py index 229a82b6f574..8be8d641324a 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -2118,13 +2118,15 @@ def attrtype(input_text: str): async def console(ctx: Context): import sys queue = asyncio.Queue() - Utils.stream_input(sys.stdin, queue) + worker = Utils.stream_input(sys.stdin, queue) while not ctx.exit_event.is_set(): try: # I don't get why this while loop is needed. Works fine without it on clients, # but the queue.get() for server never fulfills if the queue is empty when entering the await. while queue.qsize() == 0: await asyncio.sleep(0.05) + if not worker.is_alive(): + return input_text = await queue.get() queue.task_done() ctx.commandprocessor(input_text) diff --git a/Options.py b/Options.py index e106be5ba6b2..960e6c19d1ad 100644 --- a/Options.py +++ b/Options.py @@ -771,7 +771,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)") -class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys): +class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]): default: typing.Dict[str, typing.Any] = {} supports_weighting = False @@ -789,8 +789,14 @@ def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict: def get_option_name(self, value): return ", ".join(f"{key}: {v}" for key, v in value.items()) - def __contains__(self, item): - return item in self.value + def __getitem__(self, item: str) -> typing.Any: + return self.value.__getitem__(item) + + def __iter__(self) -> typing.Iterator[str]: + return self.value.__iter__() + + def __len__(self) -> int: + return self.value.__len__() class ItemDict(OptionDict): diff --git a/PokemonClient.py b/PokemonClient.py index ec653e96ac3e..6b43a53b8ff7 100644 --- a/PokemonClient.py +++ b/PokemonClient.py @@ -176,8 +176,10 @@ async def parse_locations(data: List, ctx: GBContext): elif (location_name_to_id["Fossil - Choice B"] in ctx.checked_locations and location_name_to_id["Fossil - Choice A"] not in ctx.checked_locations): hints.append("Fossil - Choice A") - hints = [location_name_to_id[loc] for loc in hints if loc not in ctx.auto_hints and location_name_to_id[loc] in - ctx.missing_locations and location_name_to_id[loc] not in ctx.locations_checked] + hints = [ + location_name_to_id[loc] for loc in hints if location_name_to_id[loc] not in ctx.auto_hints and + location_name_to_id[loc] in ctx.missing_locations and location_name_to_id[loc] not in ctx.locations_checked + ] if hints: await ctx.send_msgs([{"cmd": "LocationScouts", "locations": hints, "create_as_hint": 2}]) ctx.auto_hints.update(hints) diff --git a/SNIClient.py b/SNIClient.py index 50b557e6d7cd..0909c61382b6 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -68,12 +68,11 @@ def connect_to_snes(self, snes_options: str = "") -> bool: options = snes_options.split() num_options = len(options) - if num_options > 0: - snes_device_number = int(options[0]) - if num_options > 1: snes_address = options[0] snes_device_number = int(options[1]) + elif num_options > 0: + snes_device_number = int(options[0]) self.ctx.snes_reconnect_address = None if self.ctx.snes_connect_task: @@ -565,14 +564,16 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int, PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'} try: for address, data in write_list: - PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]] - # REVIEW: above: `if snes_socket is None: return False` - # Does it need to be checked again? - if ctx.snes_socket is not None: - await ctx.snes_socket.send(dumps(PutAddress_Request)) - await ctx.snes_socket.send(data) - else: - snes_logger.warning(f"Could not send data to SNES: {data}") + while data: + # Divide the write into packets of 256 bytes. + PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]] + if ctx.snes_socket is not None: + await ctx.snes_socket.send(dumps(PutAddress_Request)) + await ctx.snes_socket.send(data[:256]) + address += 256 + data = data[256:] + else: + snes_logger.warning(f"Could not send data to SNES: {data}") except ConnectionClosed: return False diff --git a/Starcraft2Client.py b/Starcraft2Client.py index cdcdb39a0b44..87b50d35063e 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -1,1049 +1,11 @@ from __future__ import annotations -import asyncio -import copy -import ctypes -import logging -import multiprocessing -import os.path -import re -import sys -import typing -import queue -import zipfile -import io -from pathlib import Path +import ModuleUpdate +ModuleUpdate.update() -# CommonClient import first to trigger ModuleUpdater -from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser -from Utils import init_logging, is_windows +from worlds.sc2wol.Client import launch +import Utils if __name__ == "__main__": - init_logging("SC2Client", exception_logger="Client") - -logger = logging.getLogger("Client") -sc2_logger = logging.getLogger("Starcraft2") - -import nest_asyncio -from worlds._sc2common import bot -from worlds._sc2common.bot.data import Race -from worlds._sc2common.bot.main import run_game -from worlds._sc2common.bot.player import Bot -from worlds.sc2wol import SC2WoLWorld -from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups -from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET -from worlds.sc2wol.MissionTables import lookup_id_to_mission -from worlds.sc2wol.Regions import MissionInfo - -import colorama -from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser -from MultiServer import mark_raw - -nest_asyncio.apply() -max_bonus: int = 8 -victory_modulo: int = 100 - - -class StarcraftClientProcessor(ClientCommandProcessor): - ctx: SC2Context - - def _cmd_difficulty(self, difficulty: str = "") -> bool: - """Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal""" - options = difficulty.split() - num_options = len(options) - - if num_options > 0: - difficulty_choice = options[0].lower() - if difficulty_choice == "casual": - self.ctx.difficulty_override = 0 - elif difficulty_choice == "normal": - self.ctx.difficulty_override = 1 - elif difficulty_choice == "hard": - self.ctx.difficulty_override = 2 - elif difficulty_choice == "brutal": - self.ctx.difficulty_override = 3 - else: - self.output("Unable to parse difficulty '" + options[0] + "'") - return False - - self.output("Difficulty set to " + options[0]) - return True - - else: - if self.ctx.difficulty == -1: - self.output("Please connect to a seed before checking difficulty.") - else: - self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][self.ctx.difficulty]) - self.output("To change the difficulty, add the name of the difficulty after the command.") - return False - - def _cmd_disable_mission_check(self) -> bool: - """Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play - the next mission in a chain the other player is doing.""" - self.ctx.missions_unlocked = True - sc2_logger.info("Mission check has been disabled") - return True - - def _cmd_play(self, mission_id: str = "") -> bool: - """Start a Starcraft 2 mission""" - - options = mission_id.split() - num_options = len(options) - - if num_options > 0: - mission_number = int(options[0]) - - self.ctx.play_mission(mission_number) - - else: - sc2_logger.info( - "Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.") - return False - - return True - - def _cmd_available(self) -> bool: - """Get what missions are currently available to play""" - - request_available_missions(self.ctx) - return True - - def _cmd_unfinished(self) -> bool: - """Get what missions are currently available to play and have not had all locations checked""" - - request_unfinished_missions(self.ctx) - return True - - @mark_raw - def _cmd_set_path(self, path: str = '') -> bool: - """Manually set the SC2 install directory (if the automatic detection fails).""" - if path: - os.environ["SC2PATH"] = path - is_mod_installed_correctly() - return True - else: - sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.") - return False - - def _cmd_download_data(self) -> bool: - """Download the most recent release of the necessary files for playing SC2 with - Archipelago. Will overwrite existing files.""" - if "SC2PATH" not in os.environ: - check_game_install_path() - - if os.path.exists(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt"): - with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "r") as f: - current_ver = f.read() - else: - current_ver = None - - tempzip, version = download_latest_release_zip('TheCondor07', 'Starcraft2ArchipelagoData', - current_version=current_ver, force_download=True) - - if tempzip != '': - try: - zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"]) - sc2_logger.info(f"Download complete. Version {version} installed.") - with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "w") as f: - f.write(version) - finally: - os.remove(tempzip) - else: - sc2_logger.warning("Download aborted/failed. Read the log for more information.") - return False - return True - - -class SC2Context(CommonContext): - command_processor = StarcraftClientProcessor - game = "Starcraft 2 Wings of Liberty" - items_handling = 0b111 - difficulty = -1 - all_in_choice = 0 - mission_order = 0 - mission_req_table: typing.Dict[str, MissionInfo] = {} - final_mission: int = 29 - announcements = queue.Queue() - sc2_run_task: typing.Optional[asyncio.Task] = None - missions_unlocked: bool = False # allow launching missions ignoring requirements - current_tooltip = None - last_loc_list = None - difficulty_override = -1 - mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {} - last_bot: typing.Optional[ArchipelagoBot] = None - - def __init__(self, *args, **kwargs): - super(SC2Context, self).__init__(*args, **kwargs) - self.raw_text_parser = RawJSONtoTextParser(self) - - async def server_auth(self, password_requested: bool = False): - if password_requested and not self.password: - await super(SC2Context, self).server_auth(password_requested) - await self.get_username() - await self.send_connect() - - def on_package(self, cmd: str, args: dict): - if cmd in {"Connected"}: - self.difficulty = args["slot_data"]["game_difficulty"] - self.all_in_choice = args["slot_data"]["all_in_map"] - slot_req_table = args["slot_data"]["mission_req"] - # Maintaining backwards compatibility with older slot data - self.mission_req_table = { - mission: MissionInfo( - **{field: value for field, value in mission_info.items() if field in MissionInfo._fields} - ) - for mission, mission_info in slot_req_table.items() - } - self.mission_order = args["slot_data"].get("mission_order", 0) - self.final_mission = args["slot_data"].get("final_mission", 29) - - self.build_location_to_mission_mapping() - - # Looks for the required maps and mods for SC2. Runs check_game_install_path. - maps_present = is_mod_installed_correctly() - if os.path.exists(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt"): - with open(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt", "r") as f: - current_ver = f.read() - if is_mod_update_available("TheCondor07", "Starcraft2ArchipelagoData", current_ver): - sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.") - elif maps_present: - sc2_logger.warning("NOTICE: Your map files may be outdated (version number not found). " - "Run /download_data to update them.") - - - def on_print_json(self, args: dict): - # goes to this world - if "receiving" in args and self.slot_concerns_self(args["receiving"]): - relevant = True - # found in this world - elif "item" in args and self.slot_concerns_self(args["item"].player): - relevant = True - # not related - else: - relevant = False - - if relevant: - self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"]))) - - super(SC2Context, self).on_print_json(args) - - def run_gui(self): - from kvui import GameManager, HoverBehavior, ServerToolTip - from kivy.app import App - from kivy.clock import Clock - from kivy.uix.tabbedpanel import TabbedPanelItem - from kivy.uix.gridlayout import GridLayout - from kivy.lang import Builder - from kivy.uix.label import Label - from kivy.uix.button import Button - from kivy.uix.floatlayout import FloatLayout - from kivy.properties import StringProperty - - class HoverableButton(HoverBehavior, Button): - pass - - class MissionButton(HoverableButton): - tooltip_text = StringProperty("Test") - ctx: SC2Context - - def __init__(self, *args, **kwargs): - super(HoverableButton, self).__init__(*args, **kwargs) - self.layout = FloatLayout() - self.popuplabel = ServerToolTip(text=self.text) - self.layout.add_widget(self.popuplabel) - - def on_enter(self): - self.popuplabel.text = self.tooltip_text - - if self.ctx.current_tooltip: - App.get_running_app().root.remove_widget(self.ctx.current_tooltip) - - if self.tooltip_text == "": - self.ctx.current_tooltip = None - else: - App.get_running_app().root.add_widget(self.layout) - self.ctx.current_tooltip = self.layout - - def on_leave(self): - self.ctx.ui.clear_tooltip() - - @property - def ctx(self) -> CommonContext: - return App.get_running_app().ctx - - class MissionLayout(GridLayout): - pass - - class MissionCategory(GridLayout): - pass - - class SC2Manager(GameManager): - logging_pairs = [ - ("Client", "Archipelago"), - ("Starcraft2", "Starcraft2"), - ] - base_title = "Archipelago Starcraft 2 Client" - - mission_panel = None - last_checked_locations = {} - mission_id_to_button = {} - launching: typing.Union[bool, int] = False # if int -> mission ID - refresh_from_launching = True - first_check = True - ctx: SC2Context - - def __init__(self, ctx): - super().__init__(ctx) - - def clear_tooltip(self): - if self.ctx.current_tooltip: - App.get_running_app().root.remove_widget(self.ctx.current_tooltip) - - self.ctx.current_tooltip = None - - def build(self): - container = super().build() - - panel = TabbedPanelItem(text="Starcraft 2 Launcher") - self.mission_panel = panel.content = MissionLayout() - - self.tabs.add_widget(panel) - - Clock.schedule_interval(self.build_mission_table, 0.5) - - return container - - def build_mission_table(self, dt): - if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or - not self.refresh_from_launching)) or self.first_check: - self.refresh_from_launching = True - - self.mission_panel.clear_widgets() - if self.ctx.mission_req_table: - self.last_checked_locations = self.ctx.checked_locations.copy() - self.first_check = False - - self.mission_id_to_button = {} - categories = {} - available_missions, unfinished_missions = calc_unfinished_missions(self.ctx) - - # separate missions into categories - for mission in self.ctx.mission_req_table: - if not self.ctx.mission_req_table[mission].category in categories: - categories[self.ctx.mission_req_table[mission].category] = [] - - categories[self.ctx.mission_req_table[mission].category].append(mission) - - for category in categories: - category_panel = MissionCategory() - if category.startswith('_'): - category_display_name = '' - else: - category_display_name = category - category_panel.add_widget( - Label(text=category_display_name, size_hint_y=None, height=50, outline_width=1)) - - for mission in categories[category]: - text: str = mission - tooltip: str = "" - mission_id: int = self.ctx.mission_req_table[mission].id - # Map has uncollected locations - if mission in unfinished_missions: - text = f"[color=6495ED]{text}[/color]" - elif mission in available_missions: - text = f"[color=FFFFFF]{text}[/color]" - # Map requirements not met - else: - text = f"[color=a9a9a9]{text}[/color]" - tooltip = f"Requires: " - if self.ctx.mission_req_table[mission].required_world: - tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for - req_mission in - self.ctx.mission_req_table[mission].required_world) - - if self.ctx.mission_req_table[mission].number: - tooltip += " and " - if self.ctx.mission_req_table[mission].number: - tooltip += f"{self.ctx.mission_req_table[mission].number} missions completed" - remaining_location_names: typing.List[str] = [ - self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission) - if loc in self.ctx.missing_locations] - - if mission_id == self.ctx.final_mission: - if mission in available_missions: - text = f"[color=FFBC95]{mission}[/color]" - else: - text = f"[color=D0C0BE]{mission}[/color]" - if tooltip: - tooltip += "\n" - tooltip += "Final Mission" - - if remaining_location_names: - if tooltip: - tooltip += "\n" - tooltip += f"Uncollected locations:\n" - tooltip += "\n".join(remaining_location_names) - - mission_button = MissionButton(text=text, size_hint_y=None, height=50) - mission_button.tooltip_text = tooltip - mission_button.bind(on_press=self.mission_callback) - self.mission_id_to_button[mission_id] = mission_button - category_panel.add_widget(mission_button) - - category_panel.add_widget(Label(text="")) - self.mission_panel.add_widget(category_panel) - - elif self.launching: - self.refresh_from_launching = False - - self.mission_panel.clear_widgets() - self.mission_panel.add_widget(Label(text="Launching Mission: " + - lookup_id_to_mission[self.launching])) - if self.ctx.ui: - self.ctx.ui.clear_tooltip() - - def mission_callback(self, button): - if not self.launching: - mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button) - self.ctx.play_mission(mission_id) - self.launching = mission_id - Clock.schedule_once(self.finish_launching, 10) - - def finish_launching(self, dt): - self.launching = False - - self.ui = SC2Manager(self) - self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") - import pkgutil - data = pkgutil.get_data(SC2WoLWorld.__module__, "Starcraft2.kv").decode() - Builder.load_string(data) - - async def shutdown(self): - await super(SC2Context, self).shutdown() - if self.last_bot: - self.last_bot.want_close = True - if self.sc2_run_task: - self.sc2_run_task.cancel() - - def play_mission(self, mission_id: int): - if self.missions_unlocked or \ - is_mission_available(self, mission_id): - if self.sc2_run_task: - if not self.sc2_run_task.done(): - sc2_logger.warning("Starcraft 2 Client is still running!") - self.sc2_run_task.cancel() # doesn't actually close the game, just stops the python task - if self.slot is None: - sc2_logger.warning("Launching Mission without Archipelago authentication, " - "checks will not be registered to server.") - self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id), - name="Starcraft 2 Launch") - else: - sc2_logger.info( - f"{lookup_id_to_mission[mission_id]} is not currently unlocked. " - f"Use /unfinished or /available to see what is available.") - - def build_location_to_mission_mapping(self): - mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = { - mission_info.id: set() for mission_info in self.mission_req_table.values() - } - - for loc in self.server_locations: - mission_id, objective = divmod(loc - SC2WOL_LOC_ID_OFFSET, victory_modulo) - mission_id_to_location_ids[mission_id].add(objective) - self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in - mission_id_to_location_ids.items()} - - def locations_for_mission(self, mission: str): - mission_id: int = self.mission_req_table[mission].id - objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id] - for objective in objectives: - yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective - - -async def main(): - multiprocessing.freeze_support() - parser = get_base_parser() - parser.add_argument('--name', default=None, help="Slot Name to connect as.") - args = parser.parse_args() - - ctx = SC2Context(args.connect, args.password) - ctx.auth = args.name - if ctx.server_task is None: - ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") - - if gui_enabled: - ctx.run_gui() - ctx.run_cli() - - await ctx.exit_event.wait() - - await ctx.shutdown() - - -maps_table = [ - "ap_traynor01", "ap_traynor02", "ap_traynor03", - "ap_thanson01", "ap_thanson02", "ap_thanson03a", "ap_thanson03b", - "ap_ttychus01", "ap_ttychus02", "ap_ttychus03", "ap_ttychus04", "ap_ttychus05", - "ap_ttosh01", "ap_ttosh02", "ap_ttosh03a", "ap_ttosh03b", - "ap_thorner01", "ap_thorner02", "ap_thorner03", "ap_thorner04", "ap_thorner05s", - "ap_tzeratul01", "ap_tzeratul02", "ap_tzeratul03", "ap_tzeratul04", - "ap_tvalerian01", "ap_tvalerian02a", "ap_tvalerian02b", "ap_tvalerian03" -] - -wol_default_categories = [ - "Mar Sara", "Mar Sara", "Mar Sara", "Colonist", "Colonist", "Colonist", "Colonist", - "Artifact", "Artifact", "Artifact", "Artifact", "Artifact", "Covert", "Covert", "Covert", "Covert", - "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Prophecy", "Prophecy", "Prophecy", "Prophecy", - "Char", "Char", "Char", "Char" -] -wol_default_category_names = [ - "Mar Sara", "Colonist", "Artifact", "Covert", "Rebellion", "Prophecy", "Char" -] - - -def calculate_items(items: typing.List[NetworkItem]) -> typing.List[int]: - network_item: NetworkItem - accumulators: typing.List[int] = [0 for _ in type_flaggroups] - - for network_item in items: - name: str = lookup_id_to_name[network_item.item] - item_data: ItemData = item_table[name] - - # exists exactly once - if item_data.quantity == 1: - accumulators[type_flaggroups[item_data.type]] |= 1 << item_data.number - - # exists multiple times - elif item_data.type == "Upgrade": - accumulators[type_flaggroups[item_data.type]] += 1 << item_data.number - - # sum - else: - accumulators[type_flaggroups[item_data.type]] += item_data.number - - return accumulators - - -def calc_difficulty(difficulty): - if difficulty == 0: - return 'C' - elif difficulty == 1: - return 'N' - elif difficulty == 2: - return 'H' - elif difficulty == 3: - return 'B' - - return 'X' - - -async def starcraft_launch(ctx: SC2Context, mission_id: int): - sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.") - - with DllDirectory(None): - run_game(bot.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), - name="Archipelago", fullscreen=True)], realtime=True) - - -class ArchipelagoBot(bot.bot_ai.BotAI): - game_running: bool = False - mission_completed: bool = False - boni: typing.List[bool] - setup_done: bool - ctx: SC2Context - mission_id: int - want_close: bool = False - can_read_game = False - - last_received_update: int = 0 - - def __init__(self, ctx: SC2Context, mission_id): - self.setup_done = False - self.ctx = ctx - self.ctx.last_bot = self - self.mission_id = mission_id - self.boni = [False for _ in range(max_bonus)] - - super(ArchipelagoBot, self).__init__() - - async def on_step(self, iteration: int): - if self.want_close: - self.want_close = False - await self._client.leave() - return - game_state = 0 - if not self.setup_done: - self.setup_done = True - start_items = calculate_items(self.ctx.items_received) - if self.ctx.difficulty_override >= 0: - difficulty = calc_difficulty(self.ctx.difficulty_override) - else: - difficulty = calc_difficulty(self.ctx.difficulty) - await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {} {}".format( - difficulty, - start_items[0], start_items[1], start_items[2], start_items[3], start_items[4], - start_items[5], start_items[6], start_items[7], start_items[8], start_items[9], - self.ctx.all_in_choice, start_items[10])) - self.last_received_update = len(self.ctx.items_received) - - else: - if not self.ctx.announcements.empty(): - message = self.ctx.announcements.get(timeout=1) - await self.chat_send("SendMessage " + message) - self.ctx.announcements.task_done() - - # Archipelago reads the health - for unit in self.all_own_units(): - if unit.health_max == 38281: - game_state = int(38281 - unit.health) - self.can_read_game = True - - if iteration == 160 and not game_state & 1: - await self.chat_send("SendMessage Warning: Archipelago unable to connect or has lost connection to " + - "Starcraft 2 (This is likely a map issue)") - - if self.last_received_update < len(self.ctx.items_received): - current_items = calculate_items(self.ctx.items_received) - await self.chat_send("UpdateTech {} {} {} {} {} {} {} {}".format( - current_items[0], current_items[1], current_items[2], current_items[3], current_items[4], - current_items[5], current_items[6], current_items[7])) - self.last_received_update = len(self.ctx.items_received) - - if game_state & 1: - if not self.game_running: - print("Archipelago Connected") - self.game_running = True - - if self.can_read_game: - if game_state & (1 << 1) and not self.mission_completed: - if self.mission_id != self.ctx.final_mission: - print("Mission Completed") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id]}]) - self.mission_completed = True - else: - print("Game Complete") - await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}]) - self.mission_completed = True - - for x, completed in enumerate(self.boni): - if not completed and game_state & (1 << (x + 2)): - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}]) - self.boni[x] = True - - else: - await self.chat_send("LostConnection - Lost connection to game.") - - -def request_unfinished_missions(ctx: SC2Context): - if ctx.mission_req_table: - message = "Unfinished Missions: " - unlocks = initialize_blank_mission_dict(ctx.mission_req_table) - unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table) - - _, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks) - - # Removing All-In from location pool - final_mission = lookup_id_to_mission[ctx.final_mission] - if final_mission in unfinished_missions.keys(): - message = f"Final Mission Available: {final_mission}[{ctx.final_mission}]\n" + message - if unfinished_missions[final_mission] == -1: - unfinished_missions.pop(final_mission) - - message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " + - mark_up_objectives( - f"[{len(unfinished_missions[mission])}/" - f"{sum(1 for _ in ctx.locations_for_mission(mission))}]", - ctx, unfinished_locations, mission) - for mission in unfinished_missions) - - if ctx.ui: - ctx.ui.log_panels['All'].on_message_markup(message) - ctx.ui.log_panels['Starcraft2'].on_message_markup(message) - else: - sc2_logger.info(message) - else: - sc2_logger.warning("No mission table found, you are likely not connected to a server.") - - -def calc_unfinished_missions(ctx: SC2Context, unlocks=None): - unfinished_missions = [] - locations_completed = [] - - if not unlocks: - unlocks = initialize_blank_mission_dict(ctx.mission_req_table) - - available_missions = calc_available_missions(ctx, unlocks) - - for name in available_missions: - objectives = set(ctx.locations_for_mission(name)) - if objectives: - objectives_completed = ctx.checked_locations & objectives - if len(objectives_completed) < len(objectives): - unfinished_missions.append(name) - locations_completed.append(objectives_completed) - - else: # infer that this is the final mission as it has no objectives - unfinished_missions.append(name) - locations_completed.append(-1) - - return available_missions, dict(zip(unfinished_missions, locations_completed)) - - -def is_mission_available(ctx: SC2Context, mission_id_to_check): - unfinished_missions = calc_available_missions(ctx) - - return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions) - - -def mark_up_mission_name(ctx: SC2Context, mission, unlock_table): - """Checks if the mission is required for game completion and adds '*' to the name to mark that.""" - - if ctx.mission_req_table[mission].completion_critical: - if ctx.ui: - message = "[color=AF99EF]" + mission + "[/color]" - else: - message = "*" + mission + "*" - else: - message = mission - - if ctx.ui: - unlocks = unlock_table[mission] - - if len(unlocks) > 0: - pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: " - pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].id})" for unlock in unlocks) - pre_message += f"]" - message = pre_message + message + "[/ref]" - - return message - - -def mark_up_objectives(message, ctx, unfinished_locations, mission): - formatted_message = message - - if ctx.ui: - locations = unfinished_locations[mission] - - pre_message = f"[ref={list(ctx.mission_req_table).index(mission) + 30}|" - pre_message += "
".join(location for location in locations) - pre_message += f"]" - formatted_message = pre_message + message + "[/ref]" - - return formatted_message - - -def request_available_missions(ctx: SC2Context): - if ctx.mission_req_table: - message = "Available Missions: " - - # Initialize mission unlock table - unlocks = initialize_blank_mission_dict(ctx.mission_req_table) - - missions = calc_available_missions(ctx, unlocks) - message += \ - ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}" - f"[{ctx.mission_req_table[mission].id}]" - for mission in missions) - - if ctx.ui: - ctx.ui.log_panels['All'].on_message_markup(message) - ctx.ui.log_panels['Starcraft2'].on_message_markup(message) - else: - sc2_logger.info(message) - else: - sc2_logger.warning("No mission table found, you are likely not connected to a server.") - - -def calc_available_missions(ctx: SC2Context, unlocks=None): - available_missions = [] - missions_complete = 0 - - # Get number of missions completed - for loc in ctx.checked_locations: - if loc % victory_modulo == 0: - missions_complete += 1 - - for name in ctx.mission_req_table: - # Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips - if unlocks: - for unlock in ctx.mission_req_table[name].required_world: - unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name) - - if mission_reqs_completed(ctx, name, missions_complete): - available_missions.append(name) - - return available_missions - - -def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int): - """Returns a bool signifying if the mission has all requirements complete and can be done - - Arguments: - ctx -- instance of SC2Context - locations_to_check -- the mission string name to check - missions_complete -- an int of how many missions have been completed - mission_path -- a list of missions that have already been checked -""" - if len(ctx.mission_req_table[mission_name].required_world) >= 1: - # A check for when the requirements are being or'd - or_success = False - - # Loop through required missions - for req_mission in ctx.mission_req_table[mission_name].required_world: - req_success = True - - # Check if required mission has been completed - if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id * - victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations: - if not ctx.mission_req_table[mission_name].or_requirements: - return False - else: - req_success = False - - # Grid-specific logic (to avoid long path checks and infinite recursion) - if ctx.mission_order in (3, 4): - if req_success: - return True - else: - if req_mission is ctx.mission_req_table[mission_name].required_world[-1]: - return False - else: - continue - - # Recursively check required mission to see if it's requirements are met, in case !collect has been done - # Skipping recursive check on Grid settings to speed up checks and avoid infinite recursion - if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete): - if not ctx.mission_req_table[mission_name].or_requirements: - return False - else: - req_success = False - - # If requirement check succeeded mark or as satisfied - if ctx.mission_req_table[mission_name].or_requirements and req_success: - or_success = True - - if ctx.mission_req_table[mission_name].or_requirements: - # Return false if or requirements not met - if not or_success: - return False - - # Check number of missions - if missions_complete >= ctx.mission_req_table[mission_name].number: - return True - else: - return False - else: - return True - - -def initialize_blank_mission_dict(location_table): - unlocks = {} - - for mission in list(location_table): - unlocks[mission] = [] - - return unlocks - - -def check_game_install_path() -> bool: - # First thing: go to the default location for ExecuteInfo. - # An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it. - if is_windows: - # The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow. - # https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555# - import ctypes.wintypes - CSIDL_PERSONAL = 5 # My Documents - SHGFP_TYPE_CURRENT = 0 # Get current, not default value - - buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) - ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf) - documentspath = buf.value - einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt")) - else: - einfo = str(bot.paths.get_home() / Path(bot.paths.USERPATH[bot.paths.PF])) - - # Check if the file exists. - if os.path.isfile(einfo): - - # Open the file and read it, picking out the latest executable's path. - with open(einfo) as f: - content = f.read() - if content: - try: - base = re.search(r" = (.*)Versions", content).group(1) - except AttributeError: - sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, then " - f"try again.") - return False - if os.path.exists(base): - executable = bot.paths.latest_executeble(Path(base).expanduser() / "Versions") - - # Finally, check the path for an actual executable. - # If we find one, great. Set up the SC2PATH. - if os.path.isfile(executable): - sc2_logger.info(f"Found an SC2 install at {base}!") - sc2_logger.debug(f"Latest executable at {executable}.") - os.environ["SC2PATH"] = base - sc2_logger.debug(f"SC2PATH set to {base}.") - return True - else: - sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.") - else: - sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.") - else: - sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. " - f"If that fails, please run /set_path with your SC2 install directory.") - return False - - -def is_mod_installed_correctly() -> bool: - """Searches for all required files.""" - if "SC2PATH" not in os.environ: - check_game_install_path() - - mapdir = os.environ['SC2PATH'] / Path('Maps/ArchipelagoCampaign') - modfile = os.environ["SC2PATH"] / Path("Mods/Archipelago.SC2Mod") - wol_required_maps = [ - "ap_thanson01.SC2Map", "ap_thanson02.SC2Map", "ap_thanson03a.SC2Map", "ap_thanson03b.SC2Map", - "ap_thorner01.SC2Map", "ap_thorner02.SC2Map", "ap_thorner03.SC2Map", "ap_thorner04.SC2Map", "ap_thorner05s.SC2Map", - "ap_traynor01.SC2Map", "ap_traynor02.SC2Map", "ap_traynor03.SC2Map", - "ap_ttosh01.SC2Map", "ap_ttosh02.SC2Map", "ap_ttosh03a.SC2Map", "ap_ttosh03b.SC2Map", - "ap_ttychus01.SC2Map", "ap_ttychus02.SC2Map", "ap_ttychus03.SC2Map", "ap_ttychus04.SC2Map", "ap_ttychus05.SC2Map", - "ap_tvalerian01.SC2Map", "ap_tvalerian02a.SC2Map", "ap_tvalerian02b.SC2Map", "ap_tvalerian03.SC2Map", - "ap_tzeratul01.SC2Map", "ap_tzeratul02.SC2Map", "ap_tzeratul03.SC2Map", "ap_tzeratul04.SC2Map" - ] - needs_files = False - - # Check for maps. - missing_maps = [] - for mapfile in wol_required_maps: - if not os.path.isfile(mapdir / mapfile): - missing_maps.append(mapfile) - if len(missing_maps) >= 19: - sc2_logger.warning(f"All map files missing from {mapdir}.") - needs_files = True - elif len(missing_maps) > 0: - for map in missing_maps: - sc2_logger.debug(f"Missing {map} from {mapdir}.") - sc2_logger.warning(f"Missing {len(missing_maps)} map files.") - needs_files = True - else: # Must be no maps missing - sc2_logger.info(f"All maps found in {mapdir}.") - - # Check for mods. - if os.path.isfile(modfile): - sc2_logger.info(f"Archipelago mod found at {modfile}.") - else: - sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.") - needs_files = True - - # Final verdict. - if needs_files: - sc2_logger.warning(f"Required files are missing. Run /download_data to acquire them.") - return False - else: - return True - - -class DllDirectory: - # Credit to Black Sliver for this code. - # More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw - _old: typing.Optional[str] = None - _new: typing.Optional[str] = None - - def __init__(self, new: typing.Optional[str]): - self._new = new - - def __enter__(self): - old = self.get() - if self.set(self._new): - self._old = old - - def __exit__(self, *args): - if self._old is not None: - self.set(self._old) - - @staticmethod - def get() -> typing.Optional[str]: - if sys.platform == "win32": - n = ctypes.windll.kernel32.GetDllDirectoryW(0, None) - buf = ctypes.create_unicode_buffer(n) - ctypes.windll.kernel32.GetDllDirectoryW(n, buf) - return buf.value - # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific - return None - - @staticmethod - def set(s: typing.Optional[str]) -> bool: - if sys.platform == "win32": - return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0 - # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific - return False - - -def download_latest_release_zip(owner: str, repo: str, current_version: str = None, force_download=False) -> (str, str): - """Downloads the latest release of a GitHub repo to the current directory as a .zip file.""" - import requests - - headers = {"Accept": 'application/vnd.github.v3+json'} - url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" - - r1 = requests.get(url, headers=headers) - if r1.status_code == 200: - latest_version = r1.json()["tag_name"] - sc2_logger.info(f"Latest version: {latest_version}.") - else: - sc2_logger.warning(f"Status code: {r1.status_code}") - sc2_logger.warning(f"Failed to reach GitHub. Could not find download link.") - sc2_logger.warning(f"text: {r1.text}") - return "", current_version - - if (force_download is False) and (current_version == latest_version): - sc2_logger.info("Latest version already installed.") - return "", current_version - - sc2_logger.info(f"Attempting to download version {latest_version} of {repo}.") - download_url = r1.json()["assets"][0]["browser_download_url"] - - 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: - fh.write(r2.content) - sc2_logger.info(f"Successfully downloaded {repo}.zip.") - return f"{repo}.zip", latest_version - else: - sc2_logger.warning(f"Status code: {r2.status_code}") - sc2_logger.warning("Download failed.") - sc2_logger.warning(f"text: {r2.text}") - return "", current_version - - -def is_mod_update_available(owner: str, repo: str, current_version: str) -> bool: - import requests - - headers = {"Accept": 'application/vnd.github.v3+json'} - url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" - - r1 = requests.get(url, headers=headers) - if r1.status_code == 200: - latest_version = r1.json()["tag_name"] - if current_version != latest_version: - return True - else: - return False - - else: - sc2_logger.warning(f"Failed to reach GitHub while checking for updates.") - sc2_logger.warning(f"Status code: {r1.status_code}") - sc2_logger.warning(f"text: {r1.text}") - return False - - -if __name__ == '__main__': - colorama.init() - asyncio.run(main()) - colorama.deinit() + Utils.init_logging("Starcraft2Client", exception_logger="Client") + launch() diff --git a/UndertaleClient.py b/UndertaleClient.py index e0ec642b1fd4..6419707211a6 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -99,6 +99,7 @@ class UndertaleContext(CommonContext): def __init__(self, server_address, password): super().__init__(server_address, password) self.pieces_needed = 0 + self.finished_game = False self.game = "Undertale" self.got_deathlink = False self.syncing = False @@ -239,8 +240,6 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): for ss in set(args["checked_locations"]): f.write(str(ss-12000)+"\n") f.close() - message = [{"cmd": "LocationChecks", "locations": [79067]}] - await ctx.send_msgs(message) elif cmd == "LocationInfo": for l in args["locations"]: locationid = l.location @@ -436,9 +435,9 @@ async def game_watcher(ctx: UndertaleContext): for l in lines: if ctx.server_locations.__contains__(int(l)+12000): sending = sending + [int(l.rstrip('\n'))+12000] + finally: await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending, "create_as_hint": int(2)}]) - finally: os.remove(root+"/"+file) if "check.spot" in file: sending = [] @@ -447,10 +446,8 @@ async def game_watcher(ctx: UndertaleContext): lines = f.readlines() for l in lines: sending = sending+[(int(l.rstrip('\n')))+12000] - message = [{"cmd": "LocationChecks", "locations": sending}] - await ctx.send_msgs(message) finally: - pass + await ctx.send_msgs([{"cmd": "LocationChecks", "locations": sending}]) if "victory" in file and str(ctx.route) in file: victory = True if ".playerspot" in file and "Online" not in ctx.tags: diff --git a/Utils.py b/Utils.py index f3e748d1cc09..159c6cdcb161 100644 --- a/Utils.py +++ b/Utils.py @@ -14,6 +14,7 @@ import importlib import logging +from argparse import Namespace from settings import Settings, get_settings from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union from yaml import load, load_all, dump, SafeLoader @@ -318,12 +319,27 @@ def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> N except Exception as e: logging.debug(f"Could not store data package: {e}") +def get_default_adjuster_settings(game_name: str) -> Namespace: + import LttPAdjuster + adjuster_settings = Namespace() + if game_name == LttPAdjuster.GAME_ALTTP: + return LttPAdjuster.get_argparser().parse_known_args(args=[])[0] -def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]: - adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {}) return adjuster_settings +def get_adjuster_settings_no_defaults(game_name: str) -> Namespace: + return persistent_load().get("adjuster", {}).get(game_name, Namespace()) + + +def get_adjuster_settings(game_name: str) -> Namespace: + adjuster_settings = get_adjuster_settings_no_defaults(game_name) + default_settings = get_default_adjuster_settings(game_name) + + # Fill in any arguments from the argparser that we haven't seen before + return Namespace(**vars(adjuster_settings), **{k:v for k,v in vars(default_settings).items() if k not in vars(adjuster_settings)}) + + @cache_argsless def get_unique_identifier(): uuid = persistent_load().get("client", {}).get("uuid", None) diff --git a/WebHost.py b/WebHost.py index 45d017cf1f67..36645ad27de0 100644 --- a/WebHost.py +++ b/WebHost.py @@ -14,7 +14,7 @@ Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8 -from WebHostLib import register, app as raw_app +from WebHostLib import register, cache, app as raw_app from waitress import serve from WebHostLib.models import db @@ -40,6 +40,7 @@ def get_app(): app.config["HOST_ADDRESS"] = Utils.get_public_ipv4() logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}") + cache.init_app(app) db.bind(**app.config["PONY"]) db.generate_mapping(create_tables=True) return app diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index a59e3aa553f3..441f3272fd3c 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -49,11 +49,11 @@ 'create_db': True } app.config["MAX_ROLL"] = 20 -app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache" +app.config["CACHE_TYPE"] = "SimpleCache" app.config["JSON_AS_ASCII"] = False app.config["HOST_ADDRESS"] = "" -cache = Cache(app) +cache = Cache() Compress(app) diff --git a/WebHostLib/check.py b/WebHostLib/check.py index 0c1e090dbe47..c5dfd9f55693 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -24,8 +24,8 @@ def check(): if 'file' not in request.files: flash('No file part') else: - file = request.files['file'] - options = get_yaml_data(file) + files = request.files.getlist('file') + options = get_yaml_data(files) if isinstance(options, str): flash(options) else: @@ -39,30 +39,33 @@ def mysterycheck(): return redirect(url_for("check"), 301) -def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]: +def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]: options = {} - # if user does not select file, browser also - # submit an empty part without filename - if file.filename == '': - return 'No selected file' - elif file and allowed_file(file.filename): - if file.filename.endswith(".zip"): - - with zipfile.ZipFile(file, 'r') as zfile: - infolist = zfile.infolist() - - if any(file.filename.endswith(".archipelago") for file in infolist): - return Markup("Error: Your .zip file contains an .archipelago file. " - 'Did you mean to host a game?') - - for file in infolist: - if file.filename.endswith(banned_zip_contents): - return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \ - "Your file was deleted." - elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")): - options[file.filename] = zfile.open(file, "r").read() - else: - options = {file.filename: file.read()} + for file in files: + # if user does not select file, browser also + # submit an empty part without filename + if file.filename == '': + return 'No selected file' + elif file.filename in options: + return f'Conflicting files named {file.filename} submitted' + elif file and allowed_file(file.filename): + if file.filename.endswith(".zip"): + + with zipfile.ZipFile(file, 'r') as zfile: + infolist = zfile.infolist() + + if any(file.filename.endswith(".archipelago") for file in infolist): + return Markup("Error: Your .zip file contains an .archipelago file. " + 'Did you mean to host a game?') + + for file in infolist: + if file.filename.endswith(banned_zip_contents): + return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \ + "Your file was deleted." + elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")): + options[file.filename] = zfile.open(file, "r").read() + else: + options[file.filename] = file.read() if not options: return "Did not find a .yaml file to process." return options diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index 91d7594a1f23..ddcc5ffb6c7b 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -64,8 +64,8 @@ def generate(race=False): if 'file' not in request.files: flash('No file part') else: - file = request.files['file'] - options = get_yaml_data(file) + files = request.files.getlist('file') + options = get_yaml_data(files) if isinstance(options, str): flash(options) else: diff --git a/WebHostLib/static/assets/trackerCommon.js b/WebHostLib/static/assets/trackerCommon.js index c08590cbf7db..41c4020dace8 100644 --- a/WebHostLib/static/assets/trackerCommon.js +++ b/WebHostLib/static/assets/trackerCommon.js @@ -14,6 +14,17 @@ const adjustTableHeight = () => { } }; +/** + * Convert an integer number of seconds into a human readable HH:MM format + * @param {Number} seconds + * @returns {string} + */ +const secondsToHours = (seconds) => { + let hours = Math.floor(seconds / 3600); + let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0'); + return `${hours}:${minutes}`; +}; + window.addEventListener('load', () => { const tables = $(".table").DataTable({ paging: false, @@ -27,7 +38,18 @@ window.addEventListener('load', () => { stateLoadCallback: function(settings) { return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`)); }, + footerCallback: function(tfoot, data, start, end, display) { + if (tfoot) { + const activityData = this.api().column('lastActivity:name').data().toArray().filter(x => !isNaN(x)); + Array.from(tfoot?.children).find(td => td.classList.contains('last-activity')).innerText = + (activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None'; + } + }, columnDefs: [ + { + targets: 'last-activity', + name: 'lastActivity' + }, { targets: 'hours', render: function (data, type, row) { @@ -40,11 +62,7 @@ window.addEventListener('load', () => { if (data === "None") return data; - let hours = Math.floor(data / 3600); - let minutes = Math.floor((data - (hours * 3600)) / 60); - - if (minutes < 10) {minutes = "0"+minutes;} - return hours+':'+minutes; + return secondsToHours(data); } }, { @@ -114,11 +132,16 @@ window.addEventListener('load', () => { if (status === "success") { target.find(".table").each(function (i, new_table) { const new_trs = $(new_table).find("tbody>tr"); + const footer_tr = $(new_table).find("tfoot>tr"); const old_table = tables.eq(i); const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); old_table.clear(); - old_table.rows.add(new_trs).draw(); + if (footer_tr.length) { + $(old_table.table).find("tfoot").html(footer_tr); + } + old_table.rows.add(new_trs); + old_table.draw(); $(old_table.settings()[0].nScrollBody).scrollTop(topscroll); $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); }); diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 6e86d470f05c..07157cb57915 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -1134,8 +1134,8 @@ const validateSettings = () => { return; } - // Remove any disabled options Object.keys(settings[game]).forEach((setting) => { + // Remove any disabled options Object.keys(settings[game][setting]).forEach((option) => { if (settings[game][setting][option] === 0) { delete settings[game][setting][option]; @@ -1149,6 +1149,32 @@ const validateSettings = () => { ) { errorMessage = `${game} // ${setting} has no values above zero!`; } + + // Remove weights from options with only one possibility + if ( + Object.keys(settings[game][setting]).length === 1 && + !Array.isArray(settings[game][setting]) && + setting !== 'start_inventory' + ) { + settings[game][setting] = Object.keys(settings[game][setting])[0]; + } + + // Remove empty arrays + else if ( + ['exclude_locations', 'priority_locations', 'local_items', + 'non_local_items', 'start_hints', 'start_location_hints'].includes(setting) && + settings[game][setting].length === 0 + ) { + delete settings[game][setting]; + } + + // Remove empty start inventory + else if ( + setting === 'start_inventory' && + Object.keys(settings[game]['start_inventory']).length === 0 + ) { + delete settings[game]['start_inventory']; + } }); }); @@ -1156,6 +1182,11 @@ const validateSettings = () => { errorMessage = 'You have not chosen a game to play!'; } + // Remove weights if there is only one game + else if (Object.keys(settings.game).length === 1) { + settings.game = Object.keys(settings.game)[0]; + } + // If an error occurred, alert the user and do not export the file if (errorMessage) { userMessage.innerText = errorMessage; diff --git a/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png b/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png new file mode 100644 index 000000000000..8fb366b93ff0 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png differ diff --git a/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png b/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png new file mode 100644 index 000000000000..336dc5f77af2 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png differ diff --git a/WebHostLib/static/static/icons/sc2/advanceballistics.png b/WebHostLib/static/static/icons/sc2/advanceballistics.png new file mode 100644 index 000000000000..1bf7df9fb74c Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/advanceballistics.png differ diff --git a/WebHostLib/static/static/icons/sc2/autoturretblackops.png b/WebHostLib/static/static/icons/sc2/autoturretblackops.png new file mode 100644 index 000000000000..552707831a00 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/autoturretblackops.png differ diff --git a/WebHostLib/static/static/icons/sc2/biomechanicaldrone.png b/WebHostLib/static/static/icons/sc2/biomechanicaldrone.png new file mode 100644 index 000000000000..e7ebf4031619 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/biomechanicaldrone.png differ diff --git a/WebHostLib/static/static/icons/sc2/burstcapacitors.png b/WebHostLib/static/static/icons/sc2/burstcapacitors.png new file mode 100644 index 000000000000..3af9b20a1698 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/burstcapacitors.png differ diff --git a/WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png b/WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png new file mode 100644 index 000000000000..d1c0c6c9a010 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png differ diff --git a/WebHostLib/static/static/icons/sc2/cyclone.png b/WebHostLib/static/static/icons/sc2/cyclone.png new file mode 100644 index 000000000000..d2016116ea3b Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/cyclone.png differ diff --git a/WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png b/WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png new file mode 100644 index 000000000000..351be570d11b Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png differ diff --git a/WebHostLib/static/static/icons/sc2/drillingclaws.png b/WebHostLib/static/static/icons/sc2/drillingclaws.png new file mode 100644 index 000000000000..2b067a6e44d4 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/drillingclaws.png differ diff --git a/WebHostLib/static/static/icons/sc2/emergencythrusters.png b/WebHostLib/static/static/icons/sc2/emergencythrusters.png new file mode 100644 index 000000000000..159fba37c903 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/emergencythrusters.png differ diff --git a/WebHostLib/static/static/icons/sc2/hellionbattlemode.png b/WebHostLib/static/static/icons/sc2/hellionbattlemode.png new file mode 100644 index 000000000000..56bfd98c924c Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/hellionbattlemode.png differ diff --git a/WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png b/WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png new file mode 100644 index 000000000000..40a5991ebb80 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png differ diff --git a/WebHostLib/static/static/icons/sc2/hyperflightrotors.png b/WebHostLib/static/static/icons/sc2/hyperflightrotors.png new file mode 100644 index 000000000000..375325845876 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/hyperflightrotors.png differ diff --git a/WebHostLib/static/static/icons/sc2/hyperfluxor.png b/WebHostLib/static/static/icons/sc2/hyperfluxor.png new file mode 100644 index 000000000000..cdd95bb515be Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/hyperfluxor.png differ diff --git a/WebHostLib/static/static/icons/sc2/impalerrounds.png b/WebHostLib/static/static/icons/sc2/impalerrounds.png new file mode 100644 index 000000000000..b00e0c475827 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/impalerrounds.png differ diff --git a/WebHostLib/static/static/icons/sc2/improvedburstlaser.png b/WebHostLib/static/static/icons/sc2/improvedburstlaser.png new file mode 100644 index 000000000000..8a48e38e874d Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/improvedburstlaser.png differ diff --git a/WebHostLib/static/static/icons/sc2/improvedsiegemode.png b/WebHostLib/static/static/icons/sc2/improvedsiegemode.png new file mode 100644 index 000000000000..f19dad952bb5 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/improvedsiegemode.png differ diff --git a/WebHostLib/static/static/icons/sc2/interferencematrix.png b/WebHostLib/static/static/icons/sc2/interferencematrix.png new file mode 100644 index 000000000000..ced928aa57a9 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/interferencematrix.png differ diff --git a/WebHostLib/static/static/icons/sc2/internalizedtechmodule.png b/WebHostLib/static/static/icons/sc2/internalizedtechmodule.png new file mode 100644 index 000000000000..e97f3db0d29a Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/internalizedtechmodule.png differ diff --git a/WebHostLib/static/static/icons/sc2/jotunboosters.png b/WebHostLib/static/static/icons/sc2/jotunboosters.png new file mode 100644 index 000000000000..25720306e5c2 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/jotunboosters.png differ diff --git a/WebHostLib/static/static/icons/sc2/jumpjets.png b/WebHostLib/static/static/icons/sc2/jumpjets.png new file mode 100644 index 000000000000..dfdfef4052ca Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/jumpjets.png differ diff --git a/WebHostLib/static/static/icons/sc2/lasertargetingsystem.png b/WebHostLib/static/static/icons/sc2/lasertargetingsystem.png new file mode 100644 index 000000000000..c57899b270ff Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/lasertargetingsystem.png differ diff --git a/WebHostLib/static/static/icons/sc2/liberator.png b/WebHostLib/static/static/icons/sc2/liberator.png new file mode 100644 index 000000000000..31507be5fe68 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/liberator.png differ diff --git a/WebHostLib/static/static/icons/sc2/lockdown.png b/WebHostLib/static/static/icons/sc2/lockdown.png new file mode 100644 index 000000000000..a2e7f5dc3e3f Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/lockdown.png differ diff --git a/WebHostLib/static/static/icons/sc2/magfieldaccelerator.png b/WebHostLib/static/static/icons/sc2/magfieldaccelerator.png new file mode 100644 index 000000000000..0272b4b73892 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/magfieldaccelerator.png differ diff --git a/WebHostLib/static/static/icons/sc2/magrailmunitions.png b/WebHostLib/static/static/icons/sc2/magrailmunitions.png new file mode 100644 index 000000000000..ec303498ccdb Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/magrailmunitions.png differ diff --git a/WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png b/WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png new file mode 100644 index 000000000000..1c7ce9d6ab1a Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png differ diff --git a/WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png b/WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png new file mode 100644 index 000000000000..04d68d35dc46 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png differ diff --git a/WebHostLib/static/static/icons/sc2/opticalflare.png b/WebHostLib/static/static/icons/sc2/opticalflare.png new file mode 100644 index 000000000000..f888fd518b99 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/opticalflare.png differ diff --git a/WebHostLib/static/static/icons/sc2/optimizedlogistics.png b/WebHostLib/static/static/icons/sc2/optimizedlogistics.png new file mode 100644 index 000000000000..dcf5fd72da86 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/optimizedlogistics.png differ diff --git a/WebHostLib/static/static/icons/sc2/reapercombatdrugs.png b/WebHostLib/static/static/icons/sc2/reapercombatdrugs.png new file mode 100644 index 000000000000..b9f2f055c265 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/reapercombatdrugs.png differ diff --git a/WebHostLib/static/static/icons/sc2/restoration.png b/WebHostLib/static/static/icons/sc2/restoration.png new file mode 100644 index 000000000000..f5c94e1aeefd Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/restoration.png differ diff --git a/WebHostLib/static/static/icons/sc2/ripwavemissiles.png b/WebHostLib/static/static/icons/sc2/ripwavemissiles.png new file mode 100644 index 000000000000..f68e82039765 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/ripwavemissiles.png differ diff --git a/WebHostLib/static/static/icons/sc2/shreddermissile.png b/WebHostLib/static/static/icons/sc2/shreddermissile.png new file mode 100644 index 000000000000..40899095fe3a Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/shreddermissile.png differ diff --git a/WebHostLib/static/static/icons/sc2/siegetank-spidermines.png b/WebHostLib/static/static/icons/sc2/siegetank-spidermines.png new file mode 100644 index 000000000000..1b9f8cf06097 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/siegetank-spidermines.png differ diff --git a/WebHostLib/static/static/icons/sc2/siegetankrange.png b/WebHostLib/static/static/icons/sc2/siegetankrange.png new file mode 100644 index 000000000000..5aef00a656c9 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/siegetankrange.png differ diff --git a/WebHostLib/static/static/icons/sc2/specialordance.png b/WebHostLib/static/static/icons/sc2/specialordance.png new file mode 100644 index 000000000000..4f7410d7ca9e Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/specialordance.png differ diff --git a/WebHostLib/static/static/icons/sc2/spidermine.png b/WebHostLib/static/static/icons/sc2/spidermine.png new file mode 100644 index 000000000000..bb39cf0bf8ce Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/spidermine.png differ diff --git a/WebHostLib/static/static/icons/sc2/staticempblast.png b/WebHostLib/static/static/icons/sc2/staticempblast.png new file mode 100644 index 000000000000..38f361510775 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/staticempblast.png differ diff --git a/WebHostLib/static/static/icons/sc2/superstimpack.png b/WebHostLib/static/static/icons/sc2/superstimpack.png new file mode 100644 index 000000000000..0fba8ce5749a Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/superstimpack.png differ diff --git a/WebHostLib/static/static/icons/sc2/targetingoptics.png b/WebHostLib/static/static/icons/sc2/targetingoptics.png new file mode 100644 index 000000000000..057a40f08e30 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/targetingoptics.png differ diff --git a/WebHostLib/static/static/icons/sc2/terran-cloak-color.png b/WebHostLib/static/static/icons/sc2/terran-cloak-color.png new file mode 100644 index 000000000000..44d1bb9541fb Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/terran-cloak-color.png differ diff --git a/WebHostLib/static/static/icons/sc2/terran-emp-color.png b/WebHostLib/static/static/icons/sc2/terran-emp-color.png new file mode 100644 index 000000000000..972b828c75e2 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/terran-emp-color.png differ diff --git a/WebHostLib/static/static/icons/sc2/terrandefendermodestructureattack.png b/WebHostLib/static/static/icons/sc2/terrandefendermodestructureattack.png new file mode 100644 index 000000000000..9d5982655183 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/terrandefendermodestructureattack.png differ diff --git a/WebHostLib/static/static/icons/sc2/thorsiegemode.png b/WebHostLib/static/static/icons/sc2/thorsiegemode.png new file mode 100644 index 000000000000..a298fb57de5a Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/thorsiegemode.png differ diff --git a/WebHostLib/static/static/icons/sc2/transformationservos.png b/WebHostLib/static/static/icons/sc2/transformationservos.png new file mode 100644 index 000000000000..f7f0524ac15c Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/transformationservos.png differ diff --git a/WebHostLib/static/static/icons/sc2/valkyrie.png b/WebHostLib/static/static/icons/sc2/valkyrie.png new file mode 100644 index 000000000000..9cbf339b10db Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/valkyrie.png differ diff --git a/WebHostLib/static/static/icons/sc2/warpjump.png b/WebHostLib/static/static/icons/sc2/warpjump.png new file mode 100644 index 000000000000..ff0a7b1af4aa Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/warpjump.png differ diff --git a/WebHostLib/static/static/icons/sc2/widowmine-attackrange.png b/WebHostLib/static/static/icons/sc2/widowmine-attackrange.png new file mode 100644 index 000000000000..8f5e09c6a593 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/widowmine-attackrange.png differ diff --git a/WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png b/WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png new file mode 100644 index 000000000000..7097db05e6c0 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png differ diff --git a/WebHostLib/static/static/icons/sc2/widowmine.png b/WebHostLib/static/static/icons/sc2/widowmine.png new file mode 100644 index 000000000000..802c49a83d88 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/widowmine.png differ diff --git a/WebHostLib/static/static/icons/sc2/widowminehidden.png b/WebHostLib/static/static/icons/sc2/widowminehidden.png new file mode 100644 index 000000000000..e568742e8a50 Binary files /dev/null and b/WebHostLib/static/static/icons/sc2/widowminehidden.png differ diff --git a/WebHostLib/static/styles/player-settings.css b/WebHostLib/static/styles/player-settings.css index 9ba47d5fd02d..e6e0c292922a 100644 --- a/WebHostLib/static/styles/player-settings.css +++ b/WebHostLib/static/styles/player-settings.css @@ -5,7 +5,8 @@ html{ } #player-settings{ - max-width: 1000px; + box-sizing: border-box; + max-width: 1024px; margin-left: auto; margin-right: auto; background-color: rgba(0, 0, 0, 0.15); @@ -163,6 +164,11 @@ html{ background-color: #ffef00; /* Same as .interactive in globalStyles.css */ } +#player-settings table .randomize-button[data-tooltip]::after { + left: unset; + right: 0; +} + #player-settings table label{ display: block; min-width: 200px; @@ -177,18 +183,31 @@ html{ vertical-align: top; } -@media all and (max-width: 1000px), all and (orientation: portrait){ +@media all and (max-width: 1024px) { + #player-settings { + border-radius: 0; + } + #player-settings #game-options{ justify-content: flex-start; flex-wrap: wrap; } - #player-settings .left, #player-settings .right{ - flex-grow: unset; + #player-settings .left, + #player-settings .right { + margin: 0; + } + + #game-options table { + margin-bottom: 0; } #game-options table label{ display: block; min-width: 200px; } + + #game-options table tr td { + width: 50%; + } } diff --git a/WebHostLib/static/styles/sc2wolTracker.css b/WebHostLib/static/styles/sc2wolTracker.css index b68668ecf60e..a7d8bd28c4f8 100644 --- a/WebHostLib/static/styles/sc2wolTracker.css +++ b/WebHostLib/static/styles/sc2wolTracker.css @@ -9,7 +9,7 @@ border-top-left-radius: 4px; border-top-right-radius: 4px; padding: 3px 3px 10px; - width: 500px; + width: 710px; background-color: #525494; } @@ -34,10 +34,12 @@ max-height: 40px; border: 1px solid #000000; filter: grayscale(100%) contrast(75%) brightness(20%); + background-color: black; } #inventory-table img.acquired{ filter: none; + background-color: black; } #inventory-table div.counted-item { @@ -52,7 +54,7 @@ } #location-table{ - width: 500px; + width: 710px; border-left: 2px solid #000000; border-right: 2px solid #000000; border-bottom: 2px solid #000000; diff --git a/WebHostLib/static/styles/tracker.css b/WebHostLib/static/styles/tracker.css index 0e00553c72c8..0cc2ede59fe3 100644 --- a/WebHostLib/static/styles/tracker.css +++ b/WebHostLib/static/styles/tracker.css @@ -55,16 +55,16 @@ table.dataTable thead{ font-family: LexendDeca-Regular, sans-serif; } -table.dataTable tbody{ +table.dataTable tbody, table.dataTable tfoot{ background-color: #dce2bd; font-family: LexendDeca-Light, sans-serif; } -table.dataTable tbody tr:hover{ +table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{ background-color: #e2eabb; } -table.dataTable tbody td{ +table.dataTable tbody td, table.dataTable tfoot td{ padding: 4px 6px; } @@ -97,10 +97,14 @@ table.dataTable thead th.lower-row{ top: 46px; } -table.dataTable tbody td{ +table.dataTable tbody td, table.dataTable tfoot td{ border: 1px solid #bba967; } +table.dataTable tfoot td{ + font-weight: bold; +} + div.dataTables_scrollBody{ background-color: inherit !important; } diff --git a/WebHostLib/templates/check.html b/WebHostLib/templates/check.html index 04b51340b513..8a3da7db472a 100644 --- a/WebHostLib/templates/check.html +++ b/WebHostLib/templates/check.html @@ -17,9 +17,9 @@

Upload Yaml

- +
- +
diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index dd25a908049d..33f8dbc09e6c 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -203,10 +203,10 @@

Generate Game{% if race %} (Race Mode){% endif %}

- +
- + diff --git a/WebHostLib/templates/multiFactorioTracker.html b/WebHostLib/templates/multiFactorioTracker.html index e8fa7b152cf2..faca756ee975 100644 --- a/WebHostLib/templates/multiFactorioTracker.html +++ b/WebHostLib/templates/multiFactorioTracker.html @@ -27,14 +27,14 @@ {% endblock %} {% block custom_table_row scoped %} {% if games[player] == "Factorio" %} -{% set player_inventory = inventory[team][player] %} -{% set prog_science = player_inventory[custom_items["progressive-science-pack"]] %} -{% if player_inventory[custom_items["logistic-science-pack"]] or prog_science %}✔{% endif %} -{% if player_inventory[custom_items["military-science-pack"]] or prog_science > 1%}✔{% endif %} -{% if player_inventory[custom_items["chemical-science-pack"]] or prog_science > 2%}✔{% endif %} -{% if player_inventory[custom_items["production-science-pack"]] or prog_science > 3%}✔{% endif %} -{% if player_inventory[custom_items["utility-science-pack"]] or prog_science > 4%}✔{% endif %} -{% if player_inventory[custom_items["space-science-pack"]] or prog_science > 5%}✔{% endif %} +{% set player_inventory = named_inventory[team][player] %} +{% set prog_science = player_inventory["progressive-science-pack"] %} +{% if player_inventory["logistic-science-pack"] or prog_science %}✔{% endif %} +{% if player_inventory["military-science-pack"] or prog_science > 1%}✔{% endif %} +{% if player_inventory["chemical-science-pack"] or prog_science > 2%}✔{% endif %} +{% if player_inventory["production-science-pack"] or prog_science > 3%}✔{% endif %} +{% if player_inventory["utility-science-pack"] or prog_science > 4%}✔{% endif %} +{% if player_inventory["space-science-pack"] or prog_science > 5%}✔{% endif %} {% else %} ❌ ❌ diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html index 2232cd0fd1c1..40d89eb4c6cc 100644 --- a/WebHostLib/templates/multiTracker.html +++ b/WebHostLib/templates/multiTracker.html @@ -37,7 +37,7 @@ {% endblock %} Checks % - Last
Activity + Last
Activity @@ -64,6 +64,19 @@ {%- endfor -%} + {% if not self.custom_table_headers() | trim %} + + + + Total + All Games + {{ completed_worlds }}/{{ players|length }} Complete + {{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }} + {{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }} + + + + {% endif %} {% endfor %} diff --git a/WebHostLib/templates/sc2wolTracker.html b/WebHostLib/templates/sc2wolTracker.html index af27e30b27f2..49c31a579544 100644 --- a/WebHostLib/templates/sc2wolTracker.html +++ b/WebHostLib/templates/sc2wolTracker.html @@ -11,7 +11,7 @@
- @@ -26,7 +26,7 @@ --> - @@ -37,120 +37,266 @@ + + + - - - - - - + + + + + - + + + + + + - - + + + + + + + + + + + + + + + + - + + - - - - - + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + - - - - - + + + + + + + + + + - - - - - - - - - - + + + + + + + + + - + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + - + + + + + + + + - - - + + + + + + + + + + + + + + - - - - - - + + + + + + + + - @@ -165,36 +311,18 @@ - - - - - - - - - - + - - - - - - - - - - - + - diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index d3fd0fb0364e..4261c27e096f 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -11,7 +11,7 @@ from MultiServer import Context, get_saving_second from NetUtils import SlotType, NetworkSlot from Utils import restricted_loads -from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package +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 @@ -990,6 +990,7 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict 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", @@ -1034,15 +1035,36 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict "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", @@ -1052,14 +1074,35 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict "Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png", "Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png", - "Cerberus Mine (Vulture)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.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", @@ -1069,25 +1112,77 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict "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", - "Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.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", @@ -1109,14 +1204,15 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict "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": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png", - "Fortified Bunker": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_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": "https://static.wikia.nocookie.net/starcraft/images/d/d3/SC2_Lab_BioSteel_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", @@ -1132,40 +1228,71 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict "Nothing": "", } - sc2wol_location_ids = { - "Liberation Day": [SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 101, SC2WOL_LOC_ID_OFFSET + 102, SC2WOL_LOC_ID_OFFSET + 103, SC2WOL_LOC_ID_OFFSET + 104, SC2WOL_LOC_ID_OFFSET + 105, SC2WOL_LOC_ID_OFFSET + 106], - "The Outlaws": [SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 201], - "Zero Hour": [SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 301, SC2WOL_LOC_ID_OFFSET + 302, SC2WOL_LOC_ID_OFFSET + 303], - "Evacuation": [SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 401, SC2WOL_LOC_ID_OFFSET + 402, SC2WOL_LOC_ID_OFFSET + 403], - "Outbreak": [SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 501, SC2WOL_LOC_ID_OFFSET + 502], - "Safe Haven": [SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 601, SC2WOL_LOC_ID_OFFSET + 602, SC2WOL_LOC_ID_OFFSET + 603], - "Haven's Fall": [SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 701, SC2WOL_LOC_ID_OFFSET + 702, SC2WOL_LOC_ID_OFFSET + 703], - "Smash and Grab": [SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 801, SC2WOL_LOC_ID_OFFSET + 802, SC2WOL_LOC_ID_OFFSET + 803, SC2WOL_LOC_ID_OFFSET + 804], - "The Dig": [SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 901, SC2WOL_LOC_ID_OFFSET + 902, SC2WOL_LOC_ID_OFFSET + 903], - "The Moebius Factor": [SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1003, SC2WOL_LOC_ID_OFFSET + 1004, SC2WOL_LOC_ID_OFFSET + 1005, SC2WOL_LOC_ID_OFFSET + 1006, SC2WOL_LOC_ID_OFFSET + 1007, SC2WOL_LOC_ID_OFFSET + 1008], - "Supernova": [SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1101, SC2WOL_LOC_ID_OFFSET + 1102, SC2WOL_LOC_ID_OFFSET + 1103, SC2WOL_LOC_ID_OFFSET + 1104], - "Maw of the Void": [SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1201, SC2WOL_LOC_ID_OFFSET + 1202, SC2WOL_LOC_ID_OFFSET + 1203, SC2WOL_LOC_ID_OFFSET + 1204, SC2WOL_LOC_ID_OFFSET + 1205], - "Devil's Playground": [SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1301, SC2WOL_LOC_ID_OFFSET + 1302], - "Welcome to the Jungle": [SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1401, SC2WOL_LOC_ID_OFFSET + 1402, SC2WOL_LOC_ID_OFFSET + 1403], - "Breakout": [SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1501, SC2WOL_LOC_ID_OFFSET + 1502], - "Ghost of a Chance": [SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1601, SC2WOL_LOC_ID_OFFSET + 1602, SC2WOL_LOC_ID_OFFSET + 1603, SC2WOL_LOC_ID_OFFSET + 1604, SC2WOL_LOC_ID_OFFSET + 1605], - "The Great Train Robbery": [SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1701, SC2WOL_LOC_ID_OFFSET + 1702, SC2WOL_LOC_ID_OFFSET + 1703], - "Cutthroat": [SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1801, SC2WOL_LOC_ID_OFFSET + 1802, SC2WOL_LOC_ID_OFFSET + 1803, SC2WOL_LOC_ID_OFFSET + 1804], - "Engine of Destruction": [SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 1901, SC2WOL_LOC_ID_OFFSET + 1902, SC2WOL_LOC_ID_OFFSET + 1903, SC2WOL_LOC_ID_OFFSET + 1904, SC2WOL_LOC_ID_OFFSET + 1905], - "Media Blitz": [SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2001, SC2WOL_LOC_ID_OFFSET + 2002, SC2WOL_LOC_ID_OFFSET + 2003, SC2WOL_LOC_ID_OFFSET + 2004], - "Piercing the Shroud": [SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2101, SC2WOL_LOC_ID_OFFSET + 2102, SC2WOL_LOC_ID_OFFSET + 2103, SC2WOL_LOC_ID_OFFSET + 2104, SC2WOL_LOC_ID_OFFSET + 2105], - "Whispers of Doom": [SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2201, SC2WOL_LOC_ID_OFFSET + 2202, SC2WOL_LOC_ID_OFFSET + 2203], - "A Sinister Turn": [SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2301, SC2WOL_LOC_ID_OFFSET + 2302, SC2WOL_LOC_ID_OFFSET + 2303], - "Echoes of the Future": [SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2401, SC2WOL_LOC_ID_OFFSET + 2402], - "In Utter Darkness": [SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2501, SC2WOL_LOC_ID_OFFSET + 2502], - "Gates of Hell": [SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2601], - "Belly of the Beast": [SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2701, SC2WOL_LOC_ID_OFFSET + 2702, SC2WOL_LOC_ID_OFFSET + 2703], - "Shatter the Sky": [SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2801, SC2WOL_LOC_ID_OFFSET + 2802, SC2WOL_LOC_ID_OFFSET + 2803, SC2WOL_LOC_ID_OFFSET + 2804, SC2WOL_LOC_ID_OFFSET + 2805], + "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), } display_data = {} + # 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, @@ -1173,7 +1300,15 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict "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 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"], @@ -1181,14 +1316,27 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict "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 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(' ', '_') + 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 = { @@ -1220,12 +1368,12 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict 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) + 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, @@ -1366,6 +1514,10 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s for playernumber in range(1, len(team) + 1) if playernumber not in groups} for teamnumber, team in enumerate(names)} + 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)} + hints = {team: set() for team in range(len(names))} if room.multisave: multisave = restricted_loads(room.multisave) @@ -1390,11 +1542,14 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s 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] == 30: # Goal Completed + completed_worlds += 1 long_player_names = player_names.copy() for (team, player), alias in multisave.get("name_aliases", {}).items(): player_names[team, player] = alias @@ -1410,14 +1565,18 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s activity_timers=activity_timers, video=video, hints=hints, long_player_names=long_player_names, multisave=multisave, precollected_items=precollected_items, groups=groups, - locations=locations, games=games, states=states, + locations=locations, total_locations=total_locations, games=games, states=states, + completed_worlds=completed_worlds, custom_locations=custom_locations, custom_items=custom_items, ) -def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]: - inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data} - for teamnumber, team_data in data["checks_done"].items()} +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() + } groups = data["groups"] @@ -1436,6 +1595,17 @@ def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, 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 + + return collections.Counter({mapping.get(item_id, None): count for item_id, count in inventory.items()}) + + @app.route('/tracker/') @cache.memoize(timeout=60) # multisave is currently created at most every minute def get_multiworld_tracker(tracker: UUID): @@ -1447,18 +1617,22 @@ def get_multiworld_tracker(tracker: UUID): return render_template("multiTracker.html", **data) +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) -@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) - - data["inventory"] = _get_inventory_data(data) - data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio") + 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") - return render_template("multiFactorioTracker.html", **data) + return render_template("multiFactorioTracker.html", **data) @app.route('/tracker//A Link to the Past') @@ -1588,5 +1762,7 @@ def attribute_item(team: int, recipient: int, item: int): multi_trackers: typing.Dict[str, typing.Callable] = { "A Link to the Past": get_LttP_multiworld_tracker, - "Factorio": get_Factorio_multiworld_tracker, } + +if "Factorio" in games: + multi_trackers["Factorio"] = get_Factorio_multiworld_tracker diff --git a/ZillionClient.py b/ZillionClient.py index 92585d3168a7..7d32a722615e 100644 --- a/ZillionClient.py +++ b/ZillionClient.py @@ -423,9 +423,9 @@ def log_no_spam(msg: str) -> None: async_start(ctx.send_connect()) log_no_spam("logging in to server...") await asyncio.wait(( - ctx.got_slot_data.wait(), - ctx.exit_event.wait(), - asyncio.sleep(6) + asyncio.create_task(ctx.got_slot_data.wait()), + asyncio.create_task(ctx.exit_event.wait()), + asyncio.create_task(asyncio.sleep(6)) ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets else: # not correct seed name log_no_spam("incorrect seed - did you mix up roms?") @@ -447,9 +447,9 @@ def log_no_spam(msg: str) -> None: ctx.known_name = name async_start(ctx.connect()) await asyncio.wait(( - ctx.got_room_info.wait(), - ctx.exit_event.wait(), - asyncio.sleep(6) + asyncio.create_task(ctx.got_room_info.wait()), + asyncio.create_task(ctx.exit_event.wait()), + asyncio.create_task(asyncio.sleep(6)) ), return_when=asyncio.FIRST_COMPLETED) else: # no name found in game if not help_message_shown: diff --git a/inno_setup.iss b/inno_setup.iss index a15165813d1d..147cd74dca07 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -116,6 +116,7 @@ Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion; +Source: "{#source_path}\ArchipelagoLauncher(DEBUG).exe"; DestDir: "{app}"; Flags: ignoreversion; Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio @@ -142,7 +143,7 @@ Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall [Icons] Name: "{group}\{#MyAppName} Folder"; Filename: "{app}"; Name: "{group}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe" -Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server +Name: "{group}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Components: server Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio @@ -163,7 +164,7 @@ Name: "{group}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUnder Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"; Tasks: desktopicon -Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server +Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Tasks: desktopicon; Components: server Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft diff --git a/setup.py b/setup.py index b5b1af3a327a..ce35c0f1cc5d 100644 --- a/setup.py +++ b/setup.py @@ -80,7 +80,6 @@ "Raft", "Secret of Evermore", "Slay the Spire", - "Starcraft 2 Wings of Liberty", "Sudoku", "Super Mario 64", "VVVVVV", @@ -91,6 +90,7 @@ # LogicMixin is broken before 3.10 import revamp if sys.version_info < (3,10): non_apworlds.add("Hollow Knight") + non_apworlds.add("Starcraft 2 Wings of Liberty") def download_SNI(): print("Updating SNI") @@ -185,13 +185,22 @@ def resolve_icon(icon_name: str): exes = [ cx_Freeze.Executable( - script=f'{c.script_name}.py', + script=f"{c.script_name}.py", target_name=c.frozen_name + (".exe" if is_windows else ""), icon=resolve_icon(c.icon), base="Win32GUI" if is_windows and not c.cli else None ) for c in components if c.script_name and c.frozen_name ] +if is_windows: + # create a duplicate Launcher for Windows, which has a working stdout/stderr, for debugging and --help + c = next(component for component in components if component.script_name == "Launcher") + exes.append(cx_Freeze.Executable( + script=f"{c.script_name}.py", + target_name=f"{c.frozen_name}(DEBUG).exe", + icon=resolve_icon(c.icon), + )) + extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"] extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index a74d88d10c27..217269aa9927 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -309,8 +309,8 @@ def post_fill(self) -> None: This happens before progression balancing, so the items may not be in their final locations yet.""" def generate_output(self, output_directory: str) -> None: - """This method gets called from a threadpool, do not use world.random here. - If you need any last-second randomization, use MultiWorld.per_slot_randoms[slot] instead.""" + """This method gets called from a threadpool, do not use multiworld.random here. + If you need any last-second randomization, use self.random instead.""" pass def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot diff --git a/worlds/_sc2common/bot/maps.py b/worlds/_sc2common/bot/maps.py index f14b5af9009e..29ce9f658164 100644 --- a/worlds/_sc2common/bot/maps.py +++ b/worlds/_sc2common/bot/maps.py @@ -8,18 +8,31 @@ def get(name: str) -> Map: - # Iterate through 2 folder depths for map_dir in (p for p in Paths.MAPS.iterdir()): - if map_dir.is_dir(): - for map_file in (p for p in map_dir.iterdir()): - if Map.matches_target_map_name(map_file, name): - return Map(map_file) - elif Map.matches_target_map_name(map_dir, name): - return Map(map_dir) + map = find_map_in_dir(name, map_dir) + if map is not None: + return map raise KeyError(f"Map '{name}' was not found. Please put the map file in \"/StarCraft II/Maps/\".") +# Go deeper +def find_map_in_dir(name, path): + if Map.matches_target_map_name(path, name): + return Map(path) + + if path.name.endswith("SC2Map"): + return None + + if path.is_dir(): + for childPath in (p for p in path.iterdir()): + map = find_map_in_dir(name, childPath) + if map is not None: + return map + + return None + + class Map: def __init__(self, path: Path): diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index 30100f57fcaf..7ac24fde9f28 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -581,31 +581,25 @@ async def game_watcher(self, ctx): def get_alttp_settings(romfile: str): import LttPAdjuster - last_settings = Utils.get_adjuster_settings(GAME_ALTTP) - base_settings = LttPAdjuster.get_argparser().parse_known_args(args=[])[0] - allow_list = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap", - "uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes", - "reduceflashing", "deathlink", "allowcollect", "oof"} - - for option_name in allow_list: - # set new defaults since last_settings were created - if not hasattr(last_settings, option_name): - setattr(last_settings, option_name, getattr(base_settings, option_name)) - adjustedromfile = '' - if last_settings: + if vars(Utils.get_adjuster_settings_no_defaults(GAME_ALTTP)): + last_settings = Utils.get_adjuster_settings(GAME_ALTTP) + + allow_list = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap", + "uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes", + "reduceflashing", "deathlink", "allowcollect", "oof"} choice = 'no' - if not hasattr(last_settings, 'auto_apply') or 'ask' in last_settings.auto_apply: + if 'ask' in last_settings.auto_apply: printed_options = {name: value for name, value in vars(last_settings).items() if name in allow_list} - if hasattr(last_settings, "sprite_pool"): - sprite_pool = {} - for sprite in last_settings.sprite_pool: - if sprite in sprite_pool: - sprite_pool[sprite] += 1 - else: - sprite_pool[sprite] = 1 - if sprite_pool: - printed_options["sprite_pool"] = sprite_pool + + sprite_pool = {} + for sprite in last_settings.sprite_pool: + if sprite in sprite_pool: + sprite_pool[sprite] += 1 + else: + sprite_pool[sprite] = 1 + if sprite_pool: + printed_options["sprite_pool"] = sprite_pool import pprint from CommonClient import gui_enabled @@ -685,17 +679,17 @@ def onButtonClick(answer: str = 'no'): choice = 'yes' if 'yes' in choice: + import LttPAdjuster from worlds.alttp.Rom import get_base_rom_path last_settings.rom = romfile last_settings.baserom = get_base_rom_path() last_settings.world = None - if hasattr(last_settings, "sprite_pool"): + if last_settings.sprite_pool: from LttPAdjuster import AdjusterWorld last_settings.world = AdjusterWorld(getattr(last_settings, "sprite_pool")) adjusted = True - import LttPAdjuster _, adjustedromfile = LttPAdjuster.adjust(last_settings) if hasattr(last_settings, "world"): diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 09c63aca01d2..ce4a941ead1b 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -32,7 +32,6 @@ def set_rules(world): 'WARNING! Seeds generated under this logic often require major glitches and may be impossible!') if world.players == 1: - world.get_region('Menu', player).can_reach_private = lambda state: True no_logic_rules(world, player) for exit in world.get_region('Menu', player).exits: exit.hide_path = True @@ -196,7 +195,6 @@ def global_rules(world, player): add_item_rule(world.get_location(prize_location, player), lambda item: item.name in crystals_and_pendants and item.player == player) # determines which S&Q locations are available - hide from paths since it isn't an in-game location - world.get_region('Menu', player).can_reach_private = lambda state: True for exit in world.get_region('Menu', player).exits: exit.hide_path = True diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 64fc45a3e413..8815fae092e6 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -782,6 +782,32 @@ def get_pre_fill_items(self): res.append(item) return res + def fill_slot_data(self): + slot_data = {} + if not self.multiworld.is_race: + # all of these option are NOT used by the SNI- or Text-Client. + # they are used by the alttp-poptracker pack (https://github.com/StripesOO7/alttp-ap-poptracker-pack) + # for convenient auto-tracking of the generated settings and adjusting the tracker accordingly + + slot_options = ["crystals_needed_for_gt", "crystals_needed_for_ganon", "open_pyramid", + "bigkey_shuffle", "smallkey_shuffle", "compass_shuffle", "map_shuffle", + "progressive", "swordless", "retro_bow", "retro_caves", "shop_item_slots", + "boss_shuffle", "pot_shuffle", "enemy_shuffle"] + + slot_data = {option_name: getattr(self.multiworld, option_name)[self.player].value for option_name in slot_options} + + slot_data.update({ + 'mode': self.multiworld.mode[self.player], + 'goal': self.multiworld.goal[self.player], + 'dark_room_logic': self.multiworld.dark_room_logic[self.player], + 'mm_medalion': self.multiworld.required_medallions[self.player][0], + 'tr_medalion': self.multiworld.required_medallions[self.player][1], + 'shop_shuffle': self.multiworld.shop_shuffle[self.player], + 'entrance_shuffle': self.multiworld.shuffle[self.player] + } + ) + return slot_data + def get_same_seed(world, seed_def: tuple) -> str: seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {}) diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py index 251794dd266b..248ff645bc12 100644 --- a/worlds/blasphemous/Rules.py +++ b/worlds/blasphemous/Rules.py @@ -255,23 +255,23 @@ def opened_botss_ladder(state: CollectionState, player: int) -> bool: def upwarp_skips_allowed(logic: int) -> bool: - return True if logic >= 2 else False + return logic >= 2 def mourning_skips_allowed(logic: int) -> bool: - return True if logic >= 2 else False + return logic >= 2 def enemy_skips_allowed(logic: int, enemy: int) -> bool: - return True if logic >= 2 and enemy == 0 else False + return logic >= 2 and enemy == 0 -#def unknown_skips_allowed(): -# return False +def obscure_skips_allowed(logic): + return logic >= 2 -#def precise_skips_allowed(): -# return False +def precise_skips_allowed(logic): + return logic >= 2 def can_beat_boss(state: CollectionState, boss: str, logic: int, player: int) -> bool: @@ -367,25 +367,25 @@ def has_boss_strength(name: str) -> bool: elif boss == "Graveyard": return ( has_boss_strength("amanecida") - and state.has_all({"D01BZ07S01[Santos]", "D02Z03S23[E]", "D02Z02S14[W]", "Wall Climb Ability"}, player) + and state.has_all({"D01Z06S01[Santos]", "D02Z03S23[E]", "D02Z02S14[W]", "Wall Climb Ability"}, player) ) elif boss == "Jondo": return ( has_boss_strength("amanecida") - and state.has("D01BZ07S01[Santos]", player) + and state.has("D01Z06S01[Santos]", player) and state.has_any({"D20Z01S05[W]", "D20Z01S05[E]"}, player) and state.has_any({"D03Z01S03[W]", "D03Z01S03[SW]"}, player) ) elif boss == "Patio": return ( has_boss_strength("amanecida") - and state.has_all({"D01BZ07S01[Santos]", "D06Z01S18[E]"}, player) + and state.has_all({"D01Z06S01[Santos]", "D06Z01S18[E]"}, player) and state.has_any({"D04Z01S04[W]", "D04Z01S04[E]", "D04Z01S04[Cherubs]"}, player) ) elif boss == "Wall": return ( has_boss_strength("amanecida") - and state.has_all({"D01BZ07S01[Santos]", "D09BZ01S01[Cell24]"}, player) + and state.has_all({"D01Z06S01[Santos]", "D09BZ01S01[Cell24]"}, player) and state.has_any({"D09Z01S01[W]", "D09Z01S01[E]"}, player) ) elif boss == "Hall": @@ -412,7 +412,7 @@ def guilt_rooms(state: CollectionState, player: int, number: int) -> bool: total: int = sum(state.has(item, player) for item in doors) - return True if total >= number else False + return total >= number def sword_rooms(state: CollectionState, player: int, number: int) -> bool: @@ -428,7 +428,7 @@ def sword_rooms(state: CollectionState, player: int, number: int) -> bool: total: int = sum(state.has_any(items, player) for items in doors) - return True if total >= number else False + return total >= number def redento(state: CollectionState, world, player: int, number: int) -> bool: @@ -479,7 +479,7 @@ def amanecida_rooms(state: CollectionState, logic: int, player: int, number: int total = sum(can_beat_boss(state, boss, logic, player) for boss in bosses) - return True if total >= number else False + return total >= number def chalice_rooms(state: CollectionState, player: int, number: int) -> bool: @@ -491,7 +491,7 @@ def chalice_rooms(state: CollectionState, player: int, number: int) -> bool: total: int = sum(state.has_any(items, player) for items in doors) - return True if total >= number else False + return total >= number def rules(blasphemousworld): @@ -575,12 +575,13 @@ def rules(blasphemousworld): state.has("RodeGOTPElevator", player) or pillar(state, player) or state.has("Cante Jondo of the Three Sisters", player) - or state.has("D01Z02S03[NW]", player) and ( - can_cross_gap(state, logic, player, 2) + or state.has("Purified Hand of the Nun", player) + or state.has("D01Z02S03[NW]", player) + and ( + can_cross_gap(state, logic, player, 1) or state.has("Lorquiana", player) or aubade(state, player) or state.has("Cantina of the Blue Rose", player) - or can_air_stall(state, logic, player) or charge_beam(state, player) ) )) @@ -705,6 +706,7 @@ def rules(blasphemousworld): or aubade(state, player) or charge_beam(state, player) or state.has_any({"Lorquiana", "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", "Cloistered Ruby"}, player) + or precise_skips_allowed(logic) )) # Doors set_rule(world.get_entrance("D01Z03S07[-Cherubs]", player), @@ -926,10 +928,24 @@ def rules(blasphemousworld): # D01Z05S25 (Desecrated Cistern) # Items set_rule(world.get_location("DC: Elevator shaft ledge", player), - lambda state: state.has("Linen of Golden Thread", player)) + lambda state: ( + state.has("Linen of Golden Thread", player) + or ( + state.has("Purified Hand of the Nun", player) + and state.has_any({"D01Z05S25[SW]", "D01Z05S25[SE]", "D01Z05S25[NE]"}, player) + ) + )) set_rule(world.get_location("DC: Elevator shaft Child of Moonlight", player), lambda state: ( state.has("Linen of Golden Thread", player) + or ( + obscure_skips_allowed(logic) + and state.has_any({"D01Z05S25[SW]", "D01Z05S25[SE]", "D01Z05S25[NE]"}, player) + and ( + aubade(state, player) + or state.has("Cantina of the Blue Rose", player) + ) + ) or ( pillar(state, player) and ( @@ -995,27 +1011,33 @@ def rules(blasphemousworld): lambda state: state.has("D01Z05S25[EchoesE]", player)) add_rule(world.get_entrance("D01Z05S25[EchoesW]", player), lambda state: ( - ( - state.has("D01Z05S25[EchoesE]", player) - and ( - state.has("Blood Perpetuated in Sand", player) - or can_cross_gap(state, logic, player, 8) - ) + state.has("D01Z05S25[EchoesE]", player) + and ( + state.has("Blood Perpetuated in Sand", player) + or can_cross_gap(state, logic, player, 8) + ) + or state.has("Linen of Golden Thread", player) + and ( + can_cross_gap(state, logic, player, 5) + or can_air_stall(state, logic, player) + and state.has("Blood Perpetuated in Sand", player) ) - or state.has_all({"Linen of Golden Thread", "Purified Hand of the Nun"}, player) )) set_rule(world.get_entrance("D01Z05S25[EchoesE]", player), lambda state: state.has("D01Z05S25[EchoesW]", player)) add_rule(world.get_entrance("D01Z05S25[EchoesE]", player), lambda state: ( - ( - state.has("D01Z05S25[EchoesW]", player) - and ( - state.has("Blood Perpetuated in Sand", player) - or can_cross_gap(state, logic, player, 8) - ) + state.has("D01Z05S25[EchoesW]", player) + and ( + state.has("Blood Perpetuated in Sand", player) + or can_cross_gap(state, logic, player, 8) + ) + or state.has("Linen of Golden Thread", player) + and ( + can_cross_gap(state, logic, player, 5) + or can_air_stall(state, logic, player) + and state.has("Blood Perpetuated in Sand", player) ) - or state.has_all({"Linen of Golden Thread", "Purified Hand of the Nun"}, player) )) @@ -1193,16 +1215,14 @@ def rules(blasphemousworld): # Items set_rule(world.get_location("WOTW: Underground ledge", player), lambda state: ( - state.has_all({"Wall Climb Ability", "Blood Perpetuated in Sand"}, player) - and ( - state.has("Dash Ability", player) - or state.has("D02Z01S06[Cherubs]", player) - ) - or state.has("Purified Hand of the Nun", player) + state.has("Wall Climb Ability", player) and ( - state.has("D02Z01S06[Cherubs]", player) - or state.has("D02Z01S06[E]", player) - or state.has_any({"Wall Climb Ability", "Dash Ability"}, player) + state.has("Purified Hand of the Nun", player) + or state.has("Blood Perpetuated in Sand", player) + and ( + state.has("Dash Ability", player) + or state.has("D02Z01S06[Cherubs]", player) + ) ) )) set_rule(world.get_location("WOTW: Underground Child of Moonlight", player), @@ -1210,20 +1230,26 @@ def rules(blasphemousworld): ( state.has("D02Z01S06[W]", player) or state.has("Dash Ability", player) - or state.has_all({"Purified Hand of the Nun", "Wall Climb Ability"}, player) + or state.has("Purified Hand of the Nun", player) + and state.has("Wall Climb Ability", player) ) and ( pillar(state, player) or state.has("Cante Jondo of the Three Sisters", player) + or can_dive_laser(state, logic, player) ) or ( - state.has("D02Z01S06[W]", player) - or state.has_any({"Purified Hand of the Nun", "Dash Ability"}, player) + state.has("Wall Climb Ability", player) + and ( + state.has("D02Z01S06[W]", player) + or state.has("Purified Hand of the Nun", player) + or state.has("Dash Ability", player) + ) ) - and state.has("Wall Climb Ability", player) and ( - state.has_any({"Lorquiana", "Cantina of the Blue Rose"}, player) + state.has("Lorquiana", player) or aubade(state, player) + or state.has("Cantina of the Blue Rose", player) or can_air_stall(state, logic, player) ) )) @@ -1353,17 +1379,13 @@ def rules(blasphemousworld): )) set_rule(world.get_location("GotP: Upper east shaft", player), lambda state: ( - ( - can_climb_on_root(state, player) - and state.has("Purified Hand of the Nun", player) - ) - or ( - state.has("Blood Perpetuated in Sand", player) - and ( - state.has("Purified Hand of the Nun", player) - or can_climb_on_root(state, player) - ) + can_climb_on_root(state, player) + and ( + state.has("D02Z02S03[NE]", player) + or state.has("Purified Hand of the Nun", player) + or state.has("Blood Perpetuated in Sand", player) ) + or state.has_all({"Blood Perpetuated in Sand", "Purified Hand of the Nun"}, player) )) # Doors set_rule(world.get_entrance("D02Z02S03[NW]", player), @@ -1549,6 +1571,7 @@ def rules(blasphemousworld): set_rule(world.get_location("GotP: Shop cave Child of Moonlight", player), lambda state: ( state.has("D02Z02S08[CherubsR]", player) + or can_dive_laser(state, logic, player) or state.has("Blood Perpetuated in Sand", player) or pillar(state, player) or can_cross_gap(state, logic, player, 8) @@ -2055,13 +2078,6 @@ def rules(blasphemousworld): lambda state: broke_jondo_bell_e(state, logic, enemy, player)) - # D03Z02S07 (Jondo) - # Items - set_rule(world.get_location("Jondo: Lower west lift alcove", player), - lambda state: state.has("Dash Ability", player)) - # No doors - - # D03Z02S08 (Jondo) # Items set_rule(world.get_location("Jondo: Lower west bell alcove", player), @@ -2116,29 +2132,36 @@ def rules(blasphemousworld): # Items set_rule(world.get_location("Jondo: Spike tunnel statue", player), lambda state: ( - state.has("Dash Ability", player) + state.has("D03Z02S11[W]", player) + and state.has("Purified Hand of the Nun", player) + or state.has("D03Z02S11[E]", player) + and state.has("Dash Ability", player) and ( - state.has_any({"Purified Hand of the Nun", "Wall Climb Ability"}, player) - or state.has("D03Z02S11[E]", player) - and can_cross_gap(state, logic, player, 2) + state.has("Wall Climb Ability", player) + or can_cross_gap(state, logic, player, 2) + or precise_skips_allowed(logic) + and can_cross_gap(state, logic, player, 1) ) )) set_rule(world.get_location("Jondo: Spike tunnel Child of Moonlight", player), lambda state: ( - state.has("Dash Ability", player) + state.has("D03Z02S11[W]", player) and ( - state.has_any({"Purified Hand of the Nun", "Wall Climb Ability"}, player) - or state.has("D03Z02S11[W]", player) + state.has("Purified Hand of the Nun", player) + or state.has("Dash Ability", player) and ( - can_cross_gap(state, logic, player, 2) + state.has("Wall Climb Ability", player) + or can_cross_gap(state, logic, player, 2) and can_enemy_bounce(logic, enemy) or can_cross_gap(state, logic, player, 3) ) - or state.has("D03Z02S11[E]", player) - and ( - can_cross_gap(state, logic, player, 1) - or can_enemy_bounce(logic, enemy) - ) + ) + or state.has("D03Z02S11[E]", player) + and state.has("Dash Ability", player) + and ( + can_cross_gap(state, logic, player, 1) + or state.has("Wall Climb Ability", player) + or can_enemy_bounce(logic, enemy) ) )) # Doors @@ -2146,14 +2169,21 @@ def rules(blasphemousworld): lambda state: ( state.has("Dash Ability", player) and ( - state.has_any({"Purified Hand of the Nun", "Wall Climb Ability"}, player) + state.has("Wall Climb Ability", player) or can_cross_gap(state, logic, player, 2) + or precise_skips_allowed(logic) + and can_cross_gap(state, logic, player, 1) ) )) set_rule(world.get_entrance("D03Z02S11[E]", player), lambda state: ( state.has("Dash Ability", player) - and state.has_any({"Purified Hand of the Nun", "Wall Climb Ability"}, player) + and ( + state.has("Wall Climb Ability", player) + or state.has("Purified Hand of the Nun", player) + or can_cross_gap(state, logic, player, 2) + and can_enemy_bounce(logic, enemy) + ) )) # D03Z02S13 (Jondo) @@ -2224,7 +2254,11 @@ def rules(blasphemousworld): )) set_rule(world.get_entrance("D03Z03S04[NE]", player), lambda state: ( - state.has("Wall Climb Ability", player) + ( + state.has("Wall Climb Ability", player) + or state.has("Purified Hand of the Nun", player) + and can_enemy_bounce(logic, enemy) + ) and ( state.has("D03Z03S04[NW]", player) or state.has("D03Z03S04[E]", player) @@ -2280,7 +2314,12 @@ def rules(blasphemousworld): set_rule(world.get_location("GA: Miasma room treasure", player), lambda state: state.has("Wall Climb Ability", player)) set_rule(world.get_location("GA: Miasma room Child of Moonlight", player), - lambda state: state.has("Wall Climb Ability", player)) + lambda state: ( + state.has("Wall Climb Ability", player) + or can_cross_gap(state, logic, player, 11) + and state.has("Taranto to my Sister", player) + and obscure_skips_allowed(logic) + )) # No doors @@ -2412,6 +2451,8 @@ def rules(blasphemousworld): # Items set_rule(world.get_location("PotSS: 4th meeting with Redento", player), lambda state: redento(state, blasphemousworld, player, 4)) + set_rule(world.get_location("PotSS: Amanecida of the Chiselled Steel", player), + lambda state: can_beat_boss(state, "Patio", logic, player)) # No doors @@ -2579,8 +2620,11 @@ def rules(blasphemousworld): # Items set_rule(world.get_location("MoM: East chandelier platform", player), lambda state: ( - state.has("Blood Perpetuated in Sand", player) - or can_cross_gap(state, logic, player, 3) + state.has("Dash Ability", player) + and ( + state.has("Blood Perpetuated in Sand", player) + or can_cross_gap(state, logic, player, 3) + ) )) # No doors @@ -2764,14 +2808,19 @@ def rules(blasphemousworld): # Items set_rule(world.get_location("LotNW: Elevator Child of Moonlight", player), lambda state: ( - state.has("Zarabanda of the Safe Haven", player) - or state.has("Blood Perpetuated in Sand", player) + state.has("Blood Perpetuated in Sand", player) and ( can_walk_on_root(state, player) or state.has("Purified Hand of the Nun", player) or can_cross_gap(state, logic, player, 5) and pillar(state, player) ) + or obscure_skips_allowed(logic) + and ( + state.has("Zarabanda of the Safe Haven", player) + or aubade(state, player) + or state.has("Cantina of the Blue Rose", player) + ) )) # Doors set_rule(world.get_entrance("D05Z01S21[-Cherubs]", player), @@ -3784,6 +3833,11 @@ def rules(blasphemousworld): and ( state.has("D09Z01S09[NW]", player) or state.has("D09Z01S09[Cell19]", player) + or state.has("Purified Hand of the Nun", player) + and ( + can_air_stall(state, logic, player) + or can_dawn_jump(state, logic, player) + ) ) )) # Doors @@ -3803,6 +3857,14 @@ def rules(blasphemousworld): lambda state: ( state.has("D09Z01S09[Cell19]", player) or state.has("Dash Ability", player) + and ( + state.has("D09Z01S09[Cell24]", player) + or state.has("Purified Hand of the Nun", player) + and ( + can_air_stall(state, logic, player) + or can_dawn_jump(state, logic, player) + ) + ) )) set_rule(world.get_entrance("D09Z01S09[E]", player), lambda state: ( @@ -3817,8 +3879,19 @@ def rules(blasphemousworld): or state.has("D09Z01S09[Cell19]", player) )) add_rule(world.get_entrance("D09Z01S09[Cell24]", player), - lambda state: state.has("Dash Ability", player)) - set_rule(world.get_entrance("D09Z01S09[Cell24]", player), + lambda state: ( + state.has("Dash Ability", player) + and ( + state.has("D09Z01S09[NW]", player) + or state.has("D09Z01S09[Cell19]", player) + or state.has("Purified Hand of the Nun", player) + and ( + can_air_stall(state, logic, player) + or can_dawn_jump(state, logic, player) + ) + ) + )) + set_rule(world.get_entrance("D09Z01S09[Cell19]", player), lambda state: ( state.has("D09Z01S09[NW]", player) or state.has("D09Z01S09[Cell24]", player) @@ -3827,6 +3900,14 @@ def rules(blasphemousworld): lambda state: ( state.has("D09Z01S09[NW]", player) or state.has("Dash Ability", player) + and ( + state.has("D09Z01S09[Cell24]", player) + or state.has("Purified Hand of the Nun", player) + and ( + can_air_stall(state, logic, player) + or can_dawn_jump(state, logic, player) + ) + ) )) set_rule(world.get_entrance("D09Z01S09[Cell20]", player), lambda state: ( diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index d53965d9751f..9abcd81b20e1 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -52,6 +52,9 @@ def __init__(self, multiworld, player): def set_rules(self): rules(self) + for door in door_table: + add_rule(self.multiworld.get_location(door["Id"], self.player), + lambda state: state.can_reach(self.get_connected_door(door["Id"])), self.player) def create_item(self, name: str) -> "BlasphemousItem": @@ -301,7 +304,6 @@ def create_regions(self) -> None: event = BlasphemousLocation(player, door["Id"], None, region) event.show_in_spoiler = False event.place_locked_item(self.create_event(door["Id"])) - add_rule(event, lambda state: state.can_reach(self.get_connected_door(door["Id"])), player) region.locations.append(event) victory = Location(player, "His Holiness Escribar", None, world.get_region("D07Z01S03", player)) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index faf6c2812177..5d845e3ccce1 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -502,6 +502,15 @@ def fill_slot_data(self) -> Dict[str, object]: slot_data = { "options": { + "enable_weapon_locations": self.multiworld.enable_weapon_locations[self.player].value, + "enable_shield_locations": self.multiworld.enable_shield_locations[self.player].value, + "enable_armor_locations": self.multiworld.enable_armor_locations[self.player].value, + "enable_ring_locations": self.multiworld.enable_ring_locations[self.player].value, + "enable_spell_locations": self.multiworld.enable_spell_locations[self.player].value, + "enable_key_locations": self.multiworld.enable_key_locations[self.player].value, + "enable_boss_locations": self.multiworld.enable_boss_locations[self.player].value, + "enable_npc_locations": self.multiworld.enable_npc_locations[self.player].value, + "enable_misc_locations": self.multiworld.enable_misc_locations[self.player].value, "auto_equip": self.multiworld.auto_equip[self.player].value, "lock_equip": self.multiworld.lock_equip[self.player].value, "no_weapon_requirements": self.multiworld.no_weapon_requirements[self.player].value, diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md index 3ad8236ccfae..e844925df1ea 100644 --- a/worlds/dark_souls_3/docs/en_Dark Souls III.md +++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md @@ -7,20 +7,22 @@ config file. ## What does randomization do to this game? -In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are -randomized. -An option is available from the settings page to also randomize the upgrade materials, the Estus shards and the -consumables. -Another option is available to randomize the level of the generated weapons(from +0 to +10/+5) +Items that can be picked up from static corpses, taken from chests, or earned from defeating enemies or NPCs can be +randomized. Common pickups like titanite shards or firebombs can be randomized as "progressive" items. That is, the +location "Titanite Shard #5" is the fifth titanite shard you pick up, no matter where it was from. This is also what +happens when you randomize Estus Shards and Undead Bone Shards. -To beat the game you need to collect the 4 "Cinders of a Lord" randomized in the multiworld -and kill the final boss "Soul of Cinder" +It's also possible to randomize the upgrade level of weapons and shields as well as their infusions (if they can have +one). Additionally, there are settings that can make the randomized experience more convenient or more interesting, such as +removing weapon requirements or auto-equipping whatever equipment you most recently received. + +The goal is to find the four "Cinders of a Lord" items randomized into the multiworld and defeat the Soul of Cinder. ## What Dark Souls III items can appear in other players' worlds? -Every unique item from Dark Souls III can appear in other player's worlds, such as a piece of armor, an upgraded weapon, -or a key item. +Practically anything can be found in other worlds including pieces of armor, upgraded weapons, key items, consumables, +spells, upgrade materials, etc... ## What does another world's item look like in Dark Souls III? -In Dark Souls III, items which need to be sent to other worlds appear as a Prism Stone. +In Dark Souls III, items which are sent to other worlds appear as Prism Stones. diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index a0000b0ce475..462e1416d9e6 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -216,5 +216,8 @@ def create_item(self, name: str, force_non_progression=False) -> Item: return created_item + def get_filler_item_name(self) -> str: + return self.multiworld.random.choice(list(junk_table.keys())) + def set_rules(self): set_rules(self.multiworld, self.player) diff --git a/worlds/doom_1993/Regions.py b/worlds/doom_1993/Regions.py index cce777fe13f5..58626e62ae25 100644 --- a/worlds/doom_1993/Regions.py +++ b/worlds/doom_1993/Regions.py @@ -421,17 +421,18 @@ class RegionDict(TypedDict, total=False): "connects_to_hub":True, "episode":3, "connections":[ - "Warrens (E3M9) Red", "Warrens (E3M9) Blue", "Warrens (E3M9) Blue trigger"]}, {"name":"Warrens (E3M9) Red", "connects_to_hub":False, "episode":3, - "connections":["Warrens (E3M9) Main"]}, + "connections":[]}, {"name":"Warrens (E3M9) Blue", "connects_to_hub":False, "episode":3, - "connections":["Warrens (E3M9) Main"]}, + "connections":[ + "Warrens (E3M9) Main", + "Warrens (E3M9) Red"]}, {"name":"Warrens (E3M9) Blue trigger", "connects_to_hub":False, "episode":3, diff --git a/worlds/doom_1993/Rules.py b/worlds/doom_1993/Rules.py index 57a266825c09..6f24112cbefa 100644 --- a/worlds/doom_1993/Rules.py +++ b/worlds/doom_1993/Rules.py @@ -374,16 +374,14 @@ def set_episode3_rules(player, world): state.has("Plasma gun", player, 1)) and (state.has("Rocket launcher", player, 1) or state.has("BFG9000", player, 1))) - set_rule(world.get_entrance("Warrens (E3M9) Main -> Warrens (E3M9) Red", player), lambda state: - state.has("Warrens (E3M9) - Red skull key", player, 1)) set_rule(world.get_entrance("Warrens (E3M9) Main -> Warrens (E3M9) Blue", player), lambda state: state.has("Warrens (E3M9) - Blue skull key", player, 1)) set_rule(world.get_entrance("Warrens (E3M9) Main -> Warrens (E3M9) Blue trigger", player), lambda state: state.has("Warrens (E3M9) - Blue skull key", player, 1)) - set_rule(world.get_entrance("Warrens (E3M9) Red -> Warrens (E3M9) Main", player), lambda state: - state.has("Warrens (E3M9) - Red skull key", player, 1)) set_rule(world.get_entrance("Warrens (E3M9) Blue -> Warrens (E3M9) Main", player), lambda state: state.has("Warrens (E3M9) - Blue skull key", player, 1)) + set_rule(world.get_entrance("Warrens (E3M9) Blue -> Warrens (E3M9) Red", player), lambda state: + state.has("Warrens (E3M9) - Red skull key", player, 1)) def set_episode4_rules(player, world): diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 0331c2d013ea..2b579658fc7d 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -146,8 +146,8 @@ class TechTreeLayout(Choice): class TechTreeInformation(Choice): """How much information should be displayed in the tech tree. - None: No indication what a research unlocks - Advancement: Indicators which researches unlock items that are considered logical advancements + None: No indication of what a research unlocks. + Advancement: Indicates if a research unlocks an item that is considered logical advancement, but not who it is for. Full: Labels with exact names and recipients of unlocked items; all researches are prefilled into the !hint command. """ display_name = "Technology Tree Information" diff --git a/worlds/ff1/docs/en_Final Fantasy.md b/worlds/ff1/docs/en_Final Fantasy.md index a62b5ec12605..29d4d29f8094 100644 --- a/worlds/ff1/docs/en_Final Fantasy.md +++ b/worlds/ff1/docs/en_Final Fantasy.md @@ -8,19 +8,19 @@ website: [FF1R Website](https://finalfantasyrandomizer.com/) ## What does randomization do to this game? -A better questions is what isn't randomized at this point. Enemies stats and spell, character spells, shop inventory and -boss stats and spells are all commonly randomized. Unlike most other randomizers it is also most standard to shuffle -progression items and non-progression items into separate pools and then redistribute them to their respective -locations. So, for example, Princess Sarah may have the CANOE instead of the LUTE; however, she will never have a Heal -Pot or some armor. There are plenty of other things that can be randomized on the main randomizer -site: [FF1R Website](https://finalfantasyrandomizer.com/) +Enemy stats and spell, boss stats and spells, character spells, and shop inventories are all commonly randomized. Unlike +most other randomizers, it is standard to shuffle progression items and non-progression items into separate pools +and then redistribute them to their respective locations. For example, Princess Sarah may have the CANOE instead +of the LUTE; however, she will never have a Heal Pot or armor. + +Plenty of other things to be randomized can be found on the main randomizer site: +[FF1R Website](https://finalfantasyrandomizer.com/) ## What Final Fantasy items can appear in other players' worlds? -All items can appear in other players worlds. This includes consumables, shards, weapons, armor and, of course, key -items. +All items can appear in other players worlds, including consumables, shards, weapons, armor, and key items. ## What does another world's item look like in Final Fantasy -All local and remote items appear the same. It will say that you received an item and then BOTH the client log and the +All local and remote items appear the same. Final Fantasy will say that you received an item, then BOTH the client log and the emulator will display what was found external to the in-game text box. diff --git a/worlds/ff1/docs/multiworld_en.md b/worlds/ff1/docs/multiworld_en.md index 51fcd9b7bfc4..d3dc457f01be 100644 --- a/worlds/ff1/docs/multiworld_en.md +++ b/worlds/ff1/docs/multiworld_en.md @@ -32,14 +32,14 @@ Generate a game by going to the site and performing the following steps: prefer, or it is your first time we suggest starting with the 'Shard Hunt' preset (which requires you to collect a number of shards to go to the end dungeon) or the 'Beginner' preset if you prefer to kill the original fiends. 2. Go to the `Goal` tab and ensure `Archipelago` is enabled. Set your player name to any name that represents you. -3. Upload you `Final Fantasy(USA).nes` (and click `Remember ROM` for the future!) +3. Upload your `Final Fantasy(USA).nes` (and click `Remember ROM` for the future!) 4. Press the `NEW` button beside `Seed` a few times 5. Click `GENERATE ROM` -It should download two files. One is the `*.nes` file which your emulator will run and the other is the yaml file +It should download two files. One is the `*.nes` file which your emulator will run, and the other is the yaml file required by Archipelago.gg -At this point you are ready to join the multiworld. If you are uncertain on how to generate, host or join a multiworld +At this point, you are ready to join the multiworld. If you are uncertain on how to generate, host, or join a multiworld, please refer to the [game agnostic setup guide](/tutorial/Archipelago/setup/en). ## Running the Client Program and Connecting to the Server @@ -67,7 +67,7 @@ Once the Archipelago server has been hosted: ## Play the game -When the client shows both NES and server are connected you are good to go. You can check the connection status of the +When the client shows both NES and server are connected, you are good to go. You can check the connection status of the NES at any time by running `/nes` ### Other Client Commands diff --git a/worlds/hk/docs/setup_en.md b/worlds/hk/docs/setup_en.md index e25e6bc4ac6f..adf975ff515e 100644 --- a/worlds/hk/docs/setup_en.md +++ b/worlds/hk/docs/setup_en.md @@ -4,13 +4,10 @@ * Download and unzip the Scarab+ Mod Manager from the [Scarab+ website](https://themulhima.github.io/Scarab/). * A legal copy of Hollow Knight. -## Optional Software -* Archipelago Map Mod from Scarab+ - * Ensure that both RandoMapMod and MapChanger are uninstalled or disabled as they are incompatible with Archipelago Map Mod. - -## Installing the Archipelago Mod using Scarab +## Installing the Archipelago Mod using Scarab+ 1. Launch Scarab+ and ensure it locates your Hollow Knight installation directory. 2. Click the "Install" button near the "Archipelago" mod entry. + * If desired, also install "Archipelago Map Mod" to use as an in-game tracker. 3. Launch the game, you're all set! ### What to do if Scarab+ fails to find your XBox Game Pass installation directory diff --git a/worlds/kh2/docs/en_Kingdom Hearts 2.md b/worlds/kh2/docs/en_Kingdom Hearts 2.md index d132b29ca4dd..8258a099cc95 100644 --- a/worlds/kh2/docs/en_Kingdom Hearts 2.md +++ b/worlds/kh2/docs/en_Kingdom Hearts 2.md @@ -2,7 +2,7 @@

Changes from the vanilla game

-This randomizer takes Kingdom Hearts 2 and randomizes the locations of the items for a more dynamic play experience. The items that randomize currently are all items within Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels. This allows abilities that Sora would normally have to also be placed on Keyblades with random stats. With several options on ways to finish the game. +This randomizer creates a more dynamic play experience by randomizing the locations of most items in Kingdom Hearts 2. Currently all items within Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels are randomized. This allows abilities that Sora would normally have to be placed on Keyblades with random stats. Additionally, there are several options for ways to finish the game, allowing for different goals beyond beating the final boss.

Where is the settings page

@@ -12,12 +12,18 @@ The [player settings page for this game](../player-settings) contains all the op

What is randomized in this game?

-The Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels. +- Chests +- Popups +- Get Bonuses +- Form Levels +- Sora's Levels +- Keyblade Stats +- Keyblade Abilities

What Kingdom Hearts 2 items can appear in other players' worlds?

-Every item in the game with the exception being party members' abilities. +Every item in the game except for party members' abilities.

What is The Garden of Assemblage "GoA"?

@@ -37,10 +43,10 @@ It is added to your inventory. If you obtain magic, you will need to pause your

What Happens if I die before Room Saving?

-When you die in Kingdom Hearts 2, you are reverted to the last non-boss room you entered and your status is reverted to what it was at that time. However, in archipelago, any item that you have sent/received will not be taken away from the player, any chest you have opened will remain open, and you will keep your level but lose the expereince. Unlike vanilla Kingdom Hearts 2. +When you die in vanilla Kingdom Hearts 2, you are reverted to the last non-boss room you entered and your status is reverted to what it was at that time. However, in archipelago, any item that you have sent/received will not be taken away from the player, any chest you have opened will remain open, and you will keep your level, but lose the experience. -For example, if you are fighting Roxas and you receive Reflect Element and you die fighting Roxas, you will keep that reflect. You will still need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. +For example, if you are fighting Roxas, receive Reflect Element, then die mid-fight, you will keep that Reflect Element. You will still need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.

Customization options:

@@ -49,13 +55,12 @@ For example, if you are fighting Roxas and you receive Reflect Element and you d 1. Obtain Three Proofs. 2. Obtain a desired amount of Lucky Emblems. 3. Obtain a desired amount of Bounties that are on late locations. -- Customize how many World Locking Items You Need to Progress in that World. -- Customize the Amount of World Locking Items You Start With. -- Customize how many locations you want on Sora's Levels. -- Customize the EXP Multiplier of everything that affects Sora. -- Customize the Available Abilities on Keyblades. -- Customize the level of Progressive Movement (Growth Abilities) you start with. -- Customize the amount of Progressive Movement (Growth Abilities) you start with. +- Customize how many World-Locking Items you need to progress in that world. +- Customize the amount of World-Locking Items you start with. +- Customize how many of Sora's Levels are locations. +- Customize the EXP multiplier for Sora, his Drive Forms, and his Summons. +- Customize the available abilities on keyblades. +- Customize the amount and level of progressive movement (Growth Abilities) you start with. - Customize start inventory, i.e., begin every run with certain items or spells of your choice.

Quality of life:

diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index 1d03c6fbddb2..733ab314da81 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -249,6 +249,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m all_items = multiworld.get_items() our_items = [item for item in all_items if item.player == player_id and item.location and item.code is not None and item.location.show_in_spoiler] our_useful_items = [item for item in our_items if ItemClassification.progression in item.classification] + def gen_hint(): chance = rnd.uniform(0, 1) if chance < JUNK_HINT: @@ -267,10 +268,15 @@ def gen_hint(): location_name = location.ladxr_item.metadata.name else: location_name = location.name + hint = f"{name} {location.item} is at {location_name}" if location.player != player_id: hint += f" in {multiworld.player_name[location.player]}'s world" + # Cap hint size at 85 + # Realistically we could go bigger but let's be safe instead + hint = hint[:85] + return hint hints.addHints(rom, rnd, gen_hint) diff --git a/worlds/ladx/LADXR/pointerTable.py b/worlds/ladx/LADXR/pointerTable.py index 6b56b6ff4449..9b8d49466c02 100644 --- a/worlds/ladx/LADXR/pointerTable.py +++ b/worlds/ladx/LADXR/pointerTable.py @@ -59,12 +59,14 @@ def __init__(self, rom, info): self.__storage = [{"bank": self.__storage[0]["bank"], "start": self.__storage[0]["start"], "end": self.__storage[-1]["end"]}] if "expand_to_end_of_bank" in info and info["expand_to_end_of_bank"]: for st in self.__storage: - expand = True - for st2 in self.__storage: - if st["bank"] == st2["bank"] and st["end"] < st2["end"]: - expand = False - if expand: - st["end"] = 0x4000 + if info["expand_to_end_of_bank"] == True or st["bank"] in info["expand_to_end_of_bank"]: + expand = True + for st2 in self.__storage: + if st["bank"] == st2["bank"] and st["end"] < st2["end"]: + expand = False + if expand: + st["end"] = 0x4000 + self.storage = self.__storage # for s in sorted(self.__storage, key=lambda s: (s["bank"], s["start"])): # print(self.__class__.__name__, s) diff --git a/worlds/ladx/LADXR/romTables.py b/worlds/ladx/LADXR/romTables.py index 577fa7da684b..3192443685d7 100644 --- a/worlds/ladx/LADXR/romTables.py +++ b/worlds/ladx/LADXR/romTables.py @@ -13,6 +13,7 @@ def __init__(self, rom): "pointers_bank": 0x1C, "banks_addr": 0x741, "banks_bank": 0x1C, + "expand_to_end_of_bank": {0x09} }) @@ -185,6 +186,7 @@ def __init__(self, filename, patches=None): # Ability to patch any text in the game with different text self.texts = Texts(self) + # Ability to modify rooms self.entities = Entities(self) self.rooms_overworld_top = RoomsOverworldTop(self) @@ -202,6 +204,9 @@ def __init__(self, filename, patches=None): self.itemNames = {} def save(self, filename, *, name=None): + # Assert special handling of bank 9 expansion is fine + for i in range(0x3d42, 0x4000): + assert self.banks[9][i] == 0, self.banks[9][i] self.texts.store(self) self.entities.store(self) self.rooms_overworld_top.store(self) diff --git a/worlds/messenger/Regions.py b/worlds/messenger/Regions.py index 2bfd3cab8433..88579a9704fa 100644 --- a/worlds/messenger/Regions.py +++ b/worlds/messenger/Regions.py @@ -74,7 +74,7 @@ "Underworld": ["Under Entrance Mega Shard", "Hot Tub Mega Shard", "Projectile Pit Mega Shard"], "Forlorn Temple": ["Sunny Day Mega Shard", "Down Under Mega Shard"], "Sunken Shrine": ["Mega Shard of the Moon", "Beginner's Mega Shard", "Mega Shard of the Stars", "Mega Shard of the Sun"], - "RIviere Turquoise Entrance": ["Waterfall Mega Shard"], + "Riviere Turquoise Entrance": ["Waterfall Mega Shard"], "Riviere Turquoise": ["Quick Restock Mega Shard 1", "Quick Restock Mega Shard 2"], "Elemental Skylands": ["Earth Mega Shard", "Water Mega Shard"], } diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index f36444870ddb..b37f23749df5 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -1,5 +1,5 @@ import logging -from typing import Dict, Any, Optional +from typing import Dict, Any, List, Optional from BaseClasses import Tutorial, ItemClassification, CollectionState, Item, MultiWorld from worlds.AutoWorld import World, WebWorld @@ -71,6 +71,7 @@ class MessengerWorld(World): total_shards: int shop_prices: Dict[str, int] figurine_prices: Dict[str, int] + _filler_items: List[str] def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) @@ -130,14 +131,13 @@ def create_items(self) -> None: itempool += seals remaining_fill = len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool) - filler_pool = dict(list(FILLER.items())[2:]) if remaining_fill < 10 else FILLER - itempool += [self.create_item(filler_item) - for filler_item in - self.random.choices( - list(filler_pool), - weights=list(filler_pool.values()), - k=remaining_fill - )] + if remaining_fill < 10: + self._filler_items = self.random.choices( + list(FILLER)[2:], + weights=list(FILLER.values())[2:], + k=remaining_fill + ) + itempool += [self.create_filler() for _ in range(remaining_fill)] self.multiworld.itempool += itempool @@ -167,7 +167,13 @@ def fill_slot_data(self) -> Dict[str, Any]: } def get_filler_item_name(self) -> str: - return "Time Shard" + if not getattr(self, "_filler_items", None): + self._filler_items = [name for name in self.random.choices( + list(FILLER), + weights=list(FILLER.values()), + k=20 + )] + return self._filler_items.pop(0) def create_item(self, name: str) -> MessengerItem: item_id: Optional[int] = self.item_name_to_id.get(name, None) diff --git a/worlds/messenger/test/TestLocations.py b/worlds/messenger/test/TestLocations.py new file mode 100644 index 000000000000..ccb358568ccf --- /dev/null +++ b/worlds/messenger/test/TestLocations.py @@ -0,0 +1,16 @@ +from . import MessengerTestBase +from ..SubClasses import MessengerLocation + + +class LocationsTest(MessengerTestBase): + options = { + "shuffle_shards": "true", + } + + @property + def run_default_tests(self) -> bool: + return False + + def testLocationsExist(self): + for location in self.multiworld.worlds[1].location_name_to_id: + self.assertIsInstance(self.multiworld.get_location(location, self.player), MessengerLocation) diff --git a/worlds/minecraft/docs/en_Minecraft.md b/worlds/minecraft/docs/en_Minecraft.md index 2d4f063b79a9..1ef347983bc4 100644 --- a/worlds/minecraft/docs/en_Minecraft.md +++ b/worlds/minecraft/docs/en_Minecraft.md @@ -29,82 +29,82 @@ sequence either by skipping it or watching hit play out. ## Which recipes are locked? * Archery - * Bow - * Arrow - * Crossbow + * Bow + * Arrow + * Crossbow * Brewing - * Blaze Powder - * Brewing Stand + * Blaze Powder + * Brewing Stand * Enchanting - * Enchanting Table - * Bookshelf + * Enchanting Table + * Bookshelf * Bucket * Flint & Steel * All Beds * Bottles * Shield * Fishing Rod - * Fishing Rod - * Carrot on a Stick - * Warped Fungus on a Stick + * Fishing Rod + * Carrot on a Stick + * Warped Fungus on a Stick * Campfire - * Campfire - * Soul Campfire + * Campfire + * Soul Campfire * Spyglass * Lead * Progressive Weapons - * Tier I - * Stone Sword - * Stone Axe - * Tier II - * Iron Sword - * Iron Axe - * Tier III - * Diamond Sword - * Diamond Axe + * Tier I + * Stone Sword + * Stone Axe + * Tier II + * Iron Sword + * Iron Axe + * Tier III + * Diamond Sword + * Diamond Axe * Progessive Tools - * Tier I - * Stone Shovel - * Stone Hoe - * Tier II - * Iron Shovel - * Iron Hoe - * Tier III - * Diamond Shovel - * Diamond Hoe - * Netherite Ingot + * Tier I + * Stone Shovel + * Stone Hoe + * Tier II + * Iron Shovel + * Iron Hoe + * Tier III + * Diamond Shovel + * Diamond Hoe + * Netherite Ingot * Progressive Armor - * Tier I - * Iron Helmet - * Iron Chestplate - * Iron Leggings - * Iron Boots - * Tier II - * Diamond Helmet - * Diamond Chestplate - * Diamond Leggings - * Diamond Boots + * Tier I + * Iron Helmet + * Iron Chestplate + * Iron Leggings + * Iron Boots + * Tier II + * Diamond Helmet + * Diamond Chestplate + * Diamond Leggings + * Diamond Boots * Progressive Resource Crafting - * Tier I - * Iron Ingot from Nuggets - * Iron Nugget - * Gold Ingot from Nuggets - * Gold Nugget - * Furnace - * Blast Furnace - * Tier II - * Redstone - * Redstone Block - * Glowstone - * Iron Ingot from Iron Block - * Iron Block - * Gold Ingot from Gold Block - * Gold Block - * Diamond - * Diamond Block - * Netherite Block - * Netherite Ingot from Netherite Block - * Anvil - * Emerald - * Emerald Block - * Copper Block + * Tier I + * Iron Ingot from Nuggets + * Iron Nugget + * Gold Ingot from Nuggets + * Gold Nugget + * Furnace + * Blast Furnace + * Tier II + * Redstone + * Redstone Block + * Glowstone + * Iron Ingot from Iron Block + * Iron Block + * Gold Ingot from Gold Block + * Gold Block + * Diamond + * Diamond Block + * Netherite Block + * Netherite Ingot from Netherite Block + * Anvil + * Emerald + * Emerald Block + * Copper Block diff --git a/worlds/musedash/Items.py b/worlds/musedash/Items.py index 84dd1c555df5..be229228bd40 100644 --- a/worlds/musedash/Items.py +++ b/worlds/musedash/Items.py @@ -8,10 +8,9 @@ class SongData(NamedTuple): code: Optional[int] song_is_free: bool streamer_mode: bool - easy: str = Optional[int] - hard: int = Optional[int] - master: int = Optional[int] - secret: int = Optional[int] + easy: Optional[int] + hard: Optional[int] + master: Optional[int] class AlbumData(NamedTuple): diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py index 54e0f57ccd6d..7812e28b7a8c 100644 --- a/worlds/musedash/MuseDashCollection.py +++ b/worlds/musedash/MuseDashCollection.py @@ -1,5 +1,6 @@ from .Items import SongData, AlbumData from typing import Dict, List, Optional +from collections import ChainMap def load_text_file(name: str) -> str: @@ -9,20 +10,22 @@ def load_text_file(name: str) -> str: class MuseDashCollections: """Contains all the data of Muse Dash, loaded from MuseDashData.txt.""" + STARTING_CODE = 2900000 - MUSIC_SHEET_CODE: int + MUSIC_SHEET_NAME: str = "Music Sheet" + MUSIC_SHEET_CODE: int = STARTING_CODE FREE_ALBUMS = [ "Default Music", "Budget Is Burning: Nano Core", - "Budget is Burning Vol.1" + "Budget Is Burning Vol.1", ] DIFF_OVERRIDES = [ "MuseDash ka nanika hi", "Rush-Hour", "Find this Month's Featured Playlist", - "PeroPero in the Universe" + "PeroPero in the Universe", ] album_items: Dict[str, AlbumData] = {} @@ -31,43 +34,43 @@ class MuseDashCollections: song_locations: Dict[str, int] = {} vfx_trap_items: Dict[str, int] = { - "Bad Apple Trap": 1, - "Pixelate Trap": 2, - "Random Wave Trap": 3, - "Shadow Edge Trap": 4, - "Chromatic Aberration Trap": 5, - "Background Freeze Trap": 6, - "Gray Scale Trap": 7, + "Bad Apple Trap": STARTING_CODE + 1, + "Pixelate Trap": STARTING_CODE + 2, + "Random Wave Trap": STARTING_CODE + 3, + "Shadow Edge Trap": STARTING_CODE + 4, + "Chromatic Aberration Trap": STARTING_CODE + 5, + "Background Freeze Trap": STARTING_CODE + 6, + "Gray Scale Trap": STARTING_CODE + 7, } sfx_trap_items: Dict[str, int] = { - "Nyaa SFX Trap": 8, - "Error SFX Trap": 9, + "Nyaa SFX Trap": STARTING_CODE + 8, + "Error SFX Trap": STARTING_CODE + 9, } - def __init__(self, start_item_id: int, items_per_location: int): - self.MUSIC_SHEET_CODE = start_item_id + item_names_to_id = ChainMap({}, sfx_trap_items, vfx_trap_items) + location_names_to_id = ChainMap(song_locations, album_locations) - self.vfx_trap_items = {k: (v + start_item_id) for (k, v) in self.vfx_trap_items.items()} - self.sfx_trap_items = {k: (v + start_item_id) for (k, v) in self.sfx_trap_items.items()} - - item_id_index = start_item_id + 50 - location_id_index = start_item_id + def __init__(self) -> None: + self.item_names_to_id[self.MUSIC_SHEET_NAME] = self.MUSIC_SHEET_CODE + item_id_index = self.STARTING_CODE + 50 full_file = load_text_file("MuseDashData.txt") - + seen_albums = set() for line in full_file.splitlines(): line = line.strip() sections = line.split("|") - if sections[2] not in self.album_items: - self.album_items[sections[2]] = AlbumData(item_id_index) + album = sections[2] + if album not in seen_albums: + seen_albums.add(album) + self.album_items[album] = AlbumData(item_id_index) item_id_index += 1 # Data is in the format 'Song|UID|Album|StreamerMode|EasyDiff|HardDiff|MasterDiff|SecretDiff' song_name = sections[0] # [1] is used in the client copy to make sure item id's match. - song_is_free = sections[2] in self.FREE_ALBUMS + song_is_free = album in self.FREE_ALBUMS steamer_mode = sections[3] == "True" if song_name in self.DIFF_OVERRIDES: @@ -85,17 +88,19 @@ def __init__(self, start_item_id: int, items_per_location: int): diff_of_easy, diff_of_hard, diff_of_master) item_id_index += 1 + self.item_names_to_id.update({name: data.code for name, data in self.song_items.items()}) + self.item_names_to_id.update({name: data.code for name, data in self.album_items.items()}) + + location_id_index = self.STARTING_CODE for name in self.album_items.keys(): - for i in range(0, items_per_location): - new_name = f"{name}-{i}" - self.album_locations[new_name] = location_id_index - location_id_index += 1 + self.album_locations[f"{name}-0"] = location_id_index + self.album_locations[f"{name}-1"] = location_id_index + 1 + location_id_index += 2 for name in self.song_items.keys(): - for i in range(0, items_per_location): - new_name = f"{name}-{i}" - self.song_locations[new_name] = location_id_index - location_id_index += 1 + self.song_locations[f"{name}-0"] = location_id_index + self.song_locations[f"{name}-1"] = location_id_index + 1 + location_id_index += 2 def get_songs_with_settings(self, dlc_songs: bool, streamer_mode_active: bool, diff_lower: int, diff_higher: int) -> List[str]: diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index a7a5e67e7b2f..8d6c3f375314 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -464,4 +464,8 @@ Songs Are Judged 90% by Chorus feat. Mameko|64-3|COSMIC RADIO PEROLIST|True|6|8| Kawai Splendid Space Thief|64-4|COSMIC RADIO PEROLIST|False|6|8|10|11 Night City Runway|64-5|COSMIC RADIO PEROLIST|True|4|6|8| Chaos Shotgun feat. ChumuNote|64-6|COSMIC RADIO PEROLIST|True|6|8|10| -mew mew magical summer|64-7|COSMIC RADIO PEROLIST|False|5|8|10|11 \ No newline at end of file +mew mew magical summer|64-7|COSMIC RADIO PEROLIST|False|5|8|10|11 +BrainDance|65-0|Neon Abyss|True|3|6|9| +My Focus!|65-1|Neon Abyss|True|5|7|10| +ABABABA BURST|65-2|Neon Abyss|True|5|7|9| +ULTRA HIGHER|65-3|Neon Abyss|True|4|7|10| \ No newline at end of file diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index 790f09acdc21..78b9c253d593 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -40,24 +40,14 @@ class MuseDashWorld(World): game = "Muse Dash" option_definitions = musedash_options topology_present = False - data_version = 7 + data_version = 9 web = MuseDashWebWorld() - music_sheet_name: str = "Music Sheet" - # Necessary Data - md_collection = MuseDashCollections(2900000, 2) - - item_name_to_id = { - name: data.code for name, data in md_collection.album_items.items() | md_collection.song_items.items() - } - item_name_to_id[music_sheet_name] = md_collection.MUSIC_SHEET_CODE - for item in md_collection.sfx_trap_items.items() | md_collection.vfx_trap_items.items(): - item_name_to_id[item[0]] = item[1] + md_collection = MuseDashCollections() - location_name_to_id = { - name: id for name, id in md_collection.album_locations.items() | md_collection.song_locations.items() - } + item_name_to_id = {name: code for name, code in md_collection.item_names_to_id.items()} + location_name_to_id = {name: code for name, code in md_collection.location_names_to_id.items()} # Working Data victory_song_name: str = "" @@ -165,7 +155,7 @@ def create_song_pool(self, available_song_keys: List[str]): self.location_count = minimum_location_count def create_item(self, name: str) -> Item: - if name == self.music_sheet_name: + if name == self.md_collection.MUSIC_SHEET_NAME: return MuseDashFixedItem(name, ItemClassification.progression_skip_balancing, self.md_collection.MUSIC_SHEET_CODE, self.player) @@ -177,11 +167,12 @@ def create_item(self, name: str) -> Item: if trap: return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player) - song = self.md_collection.song_items.get(name) - if song: - return MuseDashSongItem(name, self.player, song) + album = self.md_collection.album_items.get(name) + if album: + return MuseDashSongItem(name, self.player, album) - return MuseDashFixedItem(name, ItemClassification.filler, None, self.player) + song = self.md_collection.song_items.get(name) + return MuseDashSongItem(name, self.player, song) def create_items(self) -> None: song_keys_in_pool = self.included_songs.copy() @@ -191,7 +182,7 @@ def create_items(self) -> None: # First add all goal song tokens for _ in range(0, item_count): - self.multiworld.itempool.append(self.create_item(self.music_sheet_name)) + self.multiworld.itempool.append(self.create_item(self.md_collection.MUSIC_SHEET_NAME)) # Then add all traps trap_count = self.get_trap_count() @@ -255,7 +246,7 @@ def create_regions(self) -> None: def set_rules(self) -> None: self.multiworld.completion_condition[self.player] = lambda state: \ - state.has(self.music_sheet_name, self.player, self.get_music_sheet_win_count()) + state.has(self.md_collection.MUSIC_SHEET_NAME, self.player, self.get_music_sheet_win_count()) def get_available_traps(self) -> List[str]: dlc_songs = self.multiworld.allow_just_as_planned_dlc_songs[self.player] diff --git a/worlds/musedash/test/TestCollection.py b/worlds/musedash/test/TestCollection.py new file mode 100644 index 000000000000..23348af104b5 --- /dev/null +++ b/worlds/musedash/test/TestCollection.py @@ -0,0 +1,49 @@ +import unittest +from ..MuseDashCollection import MuseDashCollections + + +class CollectionsTest(unittest.TestCase): + REMOVED_SONGS = [ + "CHAOS Glitch", + "FM 17314 SUGAR RADIO", + ] + + def test_all_names_are_ascii(self) -> None: + bad_names = list() + collection = MuseDashCollections() + for name in collection.song_items.keys(): + for c in name: + # This is taken directly from OoT. Represents the generally excepted characters. + if (0x20 <= ord(c) < 0x7e): + continue + + bad_names.append(name) + break + + self.assertEqual(len(bad_names), 0, f"Muse Dash has {len(bad_names)} songs with non-ASCII characters.\n{bad_names}") + + def test_ids_dont_change(self) -> None: + collection = MuseDashCollections() + itemsBefore = {name: code for name, code in collection.item_names_to_id.items()} + locationsBefore = {name: code for name, code in collection.location_names_to_id.items()} + + collection.__init__() + itemsAfter = {name: code for name, code in collection.item_names_to_id.items()} + locationsAfter = {name: code for name, code in collection.location_names_to_id.items()} + + self.assertDictEqual(itemsBefore, itemsAfter, "Item ID changed after secondary init.") + self.assertDictEqual(locationsBefore, locationsAfter, "Location ID changed after secondary init.") + + def test_free_dlc_included_in_base_songs(self) -> None: + collection = MuseDashCollections() + songs = collection.get_songs_with_settings(False, False, 0, 11) + + self.assertIn("Glimmer", songs, "Budget Is Burning Vol.1 is not being included in base songs") + self.assertIn("Out of Sense", songs, "Budget Is Burning: Nano Core is not being included in base songs") + + def test_remove_songs_are_not_generated(self) -> None: + collection = MuseDashCollections() + songs = collection.get_songs_with_settings(True, False, 0, 11) + + for song_name in self.REMOVED_SONGS: + self.assertNotIn(song_name, songs, f"Song '{song_name}' wasn't removed correctly.") diff --git a/worlds/musedash/test/TestDifficultyRanges.py b/worlds/musedash/test/TestDifficultyRanges.py index f43b67793520..58817d0fc3ef 100644 --- a/worlds/musedash/test/TestDifficultyRanges.py +++ b/worlds/musedash/test/TestDifficultyRanges.py @@ -9,8 +9,8 @@ def test_all_difficulty_ranges(self) -> None: difficulty_max = self.multiworld.song_difficulty_max[1] def test_range(inputRange, lower, upper): - assert inputRange[0] == lower and inputRange[1] == upper, \ - f"Output incorrect. Got: {inputRange[0]} to {inputRange[1]}. Expected: {lower} to {upper}" + self.assertEqual(inputRange[0], lower) + self.assertEqual(inputRange[1], upper) songs = muse_dash_world.md_collection.get_songs_with_settings(True, False, inputRange[0], inputRange[1]) for songKey in songs: @@ -24,7 +24,7 @@ def test_range(inputRange, lower, upper): if (song.master is not None and inputRange[0] <= song.master <= inputRange[1]): continue - assert False, f"Invalid song '{songKey}' was given for range '{inputRange[0]} to {inputRange[1]}'" + self.fail(f"Invalid song '{songKey}' was given for range '{inputRange[0]} to {inputRange[1]}'") #auto ranges difficulty_choice.value = 0 @@ -65,5 +65,5 @@ def test_songs_have_difficulty(self) -> None: for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES: song = muse_dash_world.md_collection.song_items[song_name] - assert song.easy is not None and song.hard is not None and song.master is not None, \ - f"Song '{song_name}' difficulty not set when it should be." + self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None, + f"Song '{song_name}' difficulty not set when it should be.") diff --git a/worlds/musedash/test/TestNames.py b/worlds/musedash/test/TestNames.py deleted file mode 100644 index 0629afc62a39..000000000000 --- a/worlds/musedash/test/TestNames.py +++ /dev/null @@ -1,18 +0,0 @@ -import unittest -from ..MuseDashCollection import MuseDashCollections - - -class NamesTest(unittest.TestCase): - def test_all_names_are_ascii(self) -> None: - bad_names = list() - collection = MuseDashCollections(0, 1) - for name in collection.song_items.keys(): - for c in name: - # This is taken directly from OoT. Represents the generally excepted characters. - if (0x20 <= ord(c) < 0x7e): - continue - - bad_names.append(name) - break - - assert len(bad_names) == 0, f"Muse Dash has {len(bad_names)} songs with non-ASCII characters.\n{bad_names}" diff --git a/worlds/musedash/test/TestPlandoSettings.py b/worlds/musedash/test/TestPlandoSettings.py index a6bd8ad27351..4b23a4afa90a 100644 --- a/worlds/musedash/test/TestPlandoSettings.py +++ b/worlds/musedash/test/TestPlandoSettings.py @@ -1,7 +1,7 @@ from . import MuseDashTestBase -class TestIncludedSongSizeDoesntGrow(MuseDashTestBase): +class TestPlandoSettings(MuseDashTestBase): options = { "additional_song_count": 15, "allow_just_as_planned_dlc_songs": True, @@ -14,14 +14,14 @@ class TestIncludedSongSizeDoesntGrow(MuseDashTestBase): def test_included_songs_didnt_grow_item_count(self) -> None: muse_dash_world = self.multiworld.worlds[1] - assert len(muse_dash_world.included_songs) == 15, \ - f"Logical songs size grew when it shouldn't. Expected 15. Got {len(muse_dash_world.included_songs)}" + self.assertEqual(len(muse_dash_world.included_songs), 15, + f"Logical songs size grew when it shouldn't. Expected 15. Got {len(muse_dash_world.included_songs)}") def test_included_songs_plando(self) -> None: muse_dash_world = self.multiworld.worlds[1] songs = muse_dash_world.included_songs.copy() songs.append(muse_dash_world.victory_song_name) - assert "Operation Blade" in songs, "Logical songs is missing a plando song: Operation Blade" - assert "Autumn Moods" in songs, "Logical songs is missing a plando song: Autumn Moods" - assert "Fireflies" in songs, "Logical songs is missing a plando song: Fireflies" \ No newline at end of file + self.assertIn("Operation Blade", songs, "Logical songs is missing a plando song: Operation Blade") + self.assertIn("Autumn Moods", songs, "Logical songs is missing a plando song: Autumn Moods") + self.assertIn("Fireflies", songs, "Logical songs is missing a plando song: Fireflies") \ No newline at end of file diff --git a/worlds/musedash/test/TestRemovedSongs.py b/worlds/musedash/test/TestRemovedSongs.py deleted file mode 100644 index 838c64b5dc59..000000000000 --- a/worlds/musedash/test/TestRemovedSongs.py +++ /dev/null @@ -1,25 +0,0 @@ -from . import MuseDashTestBase - - -class TestRemovedSongs(MuseDashTestBase): - options = { - "starting_song_count": 10, - "allow_just_as_planned_dlc_songs": True, - "additional_song_count": 500, - } - - removed_songs = [ - "CHAOS Glitch", - "FM 17314 SUGAR RADIO" - ] - - def test_remove_songs_are_not_generated(self) -> None: - # This test is done on a world where every song should be added. - muse_dash_world = self.multiworld.worlds[1] - - for song_name in self.removed_songs: - assert song_name not in muse_dash_world.starting_songs, \ - f"Song '{song_name}' was included into the starting songs when it shouldn't." - - assert song_name not in muse_dash_world.included_songs, \ - f"Song '{song_name}' was included into the included songs when it shouldn't." diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index dc462223205c..2c70f284160a 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -69,7 +69,7 @@ class PokemonRedBlueWorld(World): settings: typing.ClassVar[PokemonSettings] data_version = 9 - required_client_version = (0, 3, 9) + required_client_version = (0, 4, 2) topology_present = True @@ -138,7 +138,7 @@ def encode_name(name, t): if self.multiworld.key_items_only[self.player]: self.multiworld.trainersanity[self.player] = self.multiworld.trainersanity[self.player].from_text("off") - self.multiworld.dexsanity[self.player] = self.multiworld.dexsanity[self.player].from_text("false") + self.multiworld.dexsanity[self.player].value = 0 self.multiworld.randomize_hidden_items[self.player] = \ self.multiworld.randomize_hidden_items[self.player].from_text("off") @@ -263,6 +263,8 @@ def stage_fill_hook(cls, multiworld, progitempool, usefulitempool, filleritempoo break else: unplaced_items.append(item) + else: + raise FillError(f"Pokemon Red and Blue local item fill failed for player {loc.player}: could not place {item.name}") progitempool += [item for item in unplaced_items if item.advancement] usefulitempool += [item for item in unplaced_items if item.useful] filleritempool += [item for item in unplaced_items if (not item.advancement) and (not item.useful)] @@ -520,10 +522,12 @@ def number_of_zones(mon): for location in locations: if not location.can_reach(all_state): pokedex.locations.remove(location) + if location in self.local_locs: + self.local_locs.remove(location) self.dexsanity_table[poke_data.pokemon_dex[location.name.split(" - ")[1]] - 1] = False remove_items += 1 - for _ in range(remove_items - 5): + for _ in range(remove_items): balls.append(balls.pop(0)) for ball in balls: try: @@ -713,6 +717,15 @@ def fill_slot_data(self) -> dict: "death_link": self.multiworld.death_link[self.player].value, "prizesanity": self.multiworld.prizesanity[self.player].value, "key_items_only": self.multiworld.key_items_only[self.player].value, + "poke_doll_skip": self.multiworld.poke_doll_skip[self.player].value, + "bicycle_gate_skips": self.multiworld.bicycle_gate_skips[self.player].value, + "stonesanity": self.multiworld.stonesanity[self.player].value, + "door_shuffle": self.multiworld.door_shuffle[self.player].value, + "warp_tile_shuffle": self.multiworld.warp_tile_shuffle[self.player].value, + "dark_rock_tunnel_logic": self.multiworld.dark_rock_tunnel_logic[self.player].value, + "split_card_key": self.multiworld.split_card_key[self.player].value, + "all_elevators_locked": self.multiworld.all_elevators_locked[self.player].value, + } diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index 603252a894a9..b7bdda7fbbed 100644 Binary files a/worlds/pokemon_rb/basepatch_blue.bsdiff4 and b/worlds/pokemon_rb/basepatch_blue.bsdiff4 differ diff --git a/worlds/pokemon_rb/basepatch_red.bsdiff4 b/worlds/pokemon_rb/basepatch_red.bsdiff4 index 9b9c93c520a6..51440789fd47 100644 Binary files a/worlds/pokemon_rb/basepatch_red.bsdiff4 and b/worlds/pokemon_rb/basepatch_red.bsdiff4 differ diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md index 535054182707..daefd6b2f7eb 100644 --- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -20,7 +20,7 @@ Many baseline changes are made to the game, including: * PC item storage increased to 64 slots (up from 50). * You can hold B to run (or bike extra fast!). * You can hold select while talking to a trainer to re-battle them. -* You can select "Pallet Warp" below the "Continue" option to warp to Pallet Towna s you load your save. +* You can select "Pallet Warp" below the "Continue" option to warp to Pallet Town as you load your save. * Mew can be encountered at the S.S. Anne dock truck. This can be randomized depending on your settings. * The S.S. Anne will never depart. * Seafoam Islands entrances are swapped. This means you need Strength to travel through from Cinnabar Island to Fuchsia diff --git a/worlds/pokemon_rb/logic.py b/worlds/pokemon_rb/logic.py index 87398c7267fe..cbe28e0ddb47 100644 --- a/worlds/pokemon_rb/logic.py +++ b/worlds/pokemon_rb/logic.py @@ -53,7 +53,7 @@ def has_key_items(state, count, player): "Hideout Key", "Card Key 2F", "Card Key 3F", "Card Key 4F", "Card Key 5F", "Card Key 6F", "Card Key 7F", "Card Key 8F", "Card Key 9F", "Card Key 10F", "Card Key 11F", "Exp. All", "Fire Stone", "Thunder Stone", "Water Stone", - "Leaf Stone"] if state.has(item, player)]) + "Leaf Stone", "Moon Stone"] if state.has(item, player)]) + min(state.count("Progressive Card Key", player), 10)) return key_items >= count diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index 4176e94fde7d..794977d32d36 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -297,8 +297,8 @@ class DexSanity(SpecialRange): range_start = 0 range_end = 100 special_range_names = { - "false": 0, - "true": 100 + "disabled": 0, + "full": 100 } diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index d82e5bb9537e..cc788dd2ba5c 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -1681,7 +1681,6 @@ def create_regions(self): connect(multiworld, player, "Fuchsia City", "Fuchsia Fishing", lambda state: state.has("Super Rod", player), one_way=True) connect(multiworld, player, "Pallet Town", "Old Rod Fishing", lambda state: state.has("Old Rod", player), one_way=True) connect(multiworld, player, "Pallet Town", "Good Rod Fishing", lambda state: state.has("Good Rod", player), one_way=True) - connect(multiworld, player, "Cinnabar Lab Fossil Room", "Good Rod Fishing", one_way=True) connect(multiworld, player, "Cinnabar Lab Fossil Room", "Fossil Level", lambda state: logic.fossil_checks(state, 1, player), one_way=True) connect(multiworld, player, "Route 5 Gate-N", "Route 5 Gate-S", lambda state: logic.can_pass_guards(state, player)) connect(multiworld, player, "Route 6 Gate-N", "Route 6 Gate-S", lambda state: logic.can_pass_guards(state, player)) diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index ef63e6b526d1..4b191d91765b 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -238,18 +238,19 @@ def generate_output(self, output_directory: str): data[address] = 0 if "Elevator" in connected_map_name else warp_to_ids[i] data[address + 1] = map_ids[connected_map_name] - for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM", - "Vermilion Gym - Lt. Surge TM", "Celadon Gym - Erika TM", - "Fuchsia Gym - Koga TM", "Saffron Gym - Sabrina TM", - "Cinnabar Gym - Blaine TM", "Viridian Gym - Giovanni TM")): - item_name = self.multiworld.get_location(gym_leader, self.player).item.name - if item_name.startswith("TM"): - try: - tm = int(item_name[2:4]) - move = poke_data.moves[self.local_tms[tm - 1]]["id"] - data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move - except KeyError: - pass + if not self.multiworld.key_items_only[self.player]: + for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM", + "Vermilion Gym - Lt. Surge TM", "Celadon Gym - Erika TM", + "Fuchsia Gym - Koga TM", "Saffron Gym - Sabrina TM", + "Cinnabar Gym - Blaine TM", "Viridian Gym - Giovanni TM")): + item_name = self.multiworld.get_location(gym_leader, self.player).item.name + if item_name.startswith("TM"): + try: + tm = int(item_name[2:4]) + move = poke_data.moves[self.local_tms[tm - 1]]["id"] + data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move + except KeyError: + pass def set_trade_mon(address, loc): mon = self.multiworld.get_location(loc, self.player).item.name @@ -353,9 +354,9 @@ def set_trade_mon(address, loc): if self.multiworld.old_man[self.player] == "open_viridian_city": data[rom_addresses['Option_Old_Man']] = 0x11 data[rom_addresses['Option_Old_Man_Lying']] = 0x15 - data[rom_addresses['Option_Route3_Guard_A']] = self.multiworld.route_3_condition[self.player].value + data[rom_addresses['Option_Route3_Guard_B']] = self.multiworld.route_3_condition[self.player].value if self.multiworld.route_3_condition[self.player] == "open": - data[rom_addresses['Option_Route3_Guard_B']] = 0x11 + data[rom_addresses['Option_Route3_Guard_A']] = 0x11 if not self.multiworld.robbed_house_officer[self.player]: data[rom_addresses['Option_Trashed_House_Guard_A']] = 0x15 data[rom_addresses['Option_Trashed_House_Guard_B']] = 0x11 diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index e9dc206eb3d9..9c6621523cd1 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -3,8 +3,8 @@ "Option_Pitch_Black_Rock_Tunnel": 0x758, "Option_Blind_Trainers": 0x30c3, "Option_Trainersanity1": 0x3153, - "Option_Split_Card_Key": 0x3e09, - "Option_Fix_Combat_Bugs": 0x3e0a, + "Option_Split_Card_Key": 0x3e0c, + "Option_Fix_Combat_Bugs": 0x3e0d, "Option_Lose_Money": 0x40d4, "Base_Stats_Mew": 0x4260, "Title_Mon_First": 0x4373, @@ -1164,22 +1164,22 @@ "Prize_Mon_E": 0x52944, "Prize_Mon_F": 0x52946, "Start_Inventory": 0x52a7b, - "Map_Fly_Location": 0x52c77, - "Reset_A": 0x52d27, - "Reset_B": 0x52d53, - "Reset_C": 0x52d7f, - "Reset_D": 0x52dab, - "Reset_E": 0x52dd7, - "Reset_F": 0x52e03, - "Reset_G": 0x52e2f, - "Reset_H": 0x52e5b, - "Reset_I": 0x52e87, - "Reset_J": 0x52eb3, - "Reset_K": 0x52edf, - "Reset_L": 0x52f0b, - "Reset_M": 0x52f37, - "Reset_N": 0x52f63, - "Reset_O": 0x52f8f, + "Map_Fly_Location": 0x52c6f, + "Reset_A": 0x52d1b, + "Reset_B": 0x52d47, + "Reset_C": 0x52d73, + "Reset_D": 0x52d9f, + "Reset_E": 0x52dcb, + "Reset_F": 0x52df7, + "Reset_G": 0x52e23, + "Reset_H": 0x52e4f, + "Reset_I": 0x52e7b, + "Reset_J": 0x52ea7, + "Reset_K": 0x52ed3, + "Reset_L": 0x52eff, + "Reset_M": 0x52f2b, + "Reset_N": 0x52f57, + "Reset_O": 0x52f83, "Warps_Route2": 0x54026, "Missable_Route_2_Item_1": 0x5404a, "Missable_Route_2_Item_2": 0x54051, diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index a7bce8891835..7c638f50b34d 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -40,6 +40,10 @@ class RiskOfRainWorld(World): web = RiskOfWeb() total_revivals: int + def __init__(self, multiworld: "MultiWorld", player: int): + super().__init__(multiworld, player) + self.junk_pool: Dict[str, int] = {} + def generate_early(self) -> None: # figure out how many revivals should exist in the pool if self.multiworld.goal[self.player] == "classic": @@ -87,38 +91,6 @@ def create_items(self) -> None: self.multiworld.push_precollected(self.create_item(unlock[0])) environments_pool.pop(unlock[0]) - # if presets are enabled generate junk_pool from the selected preset - pool_option = self.multiworld.item_weights[self.player].value - junk_pool: Dict[str, int] = {} - if self.multiworld.item_pool_presets[self.player]: - # generate chaos weights if the preset is chosen - if pool_option == ItemWeights.option_chaos: - for name, max_value in item_pool_weights[pool_option].items(): - junk_pool[name] = self.multiworld.random.randint(0, max_value) - else: - junk_pool = item_pool_weights[pool_option].copy() - else: # generate junk pool from user created presets - junk_pool = { - "Item Scrap, Green": self.multiworld.green_scrap[self.player].value, - "Item Scrap, Red": self.multiworld.red_scrap[self.player].value, - "Item Scrap, Yellow": self.multiworld.yellow_scrap[self.player].value, - "Item Scrap, White": self.multiworld.white_scrap[self.player].value, - "Common Item": self.multiworld.common_item[self.player].value, - "Uncommon Item": self.multiworld.uncommon_item[self.player].value, - "Legendary Item": self.multiworld.legendary_item[self.player].value, - "Boss Item": self.multiworld.boss_item[self.player].value, - "Lunar Item": self.multiworld.lunar_item[self.player].value, - "Void Item": self.multiworld.void_item[self.player].value, - "Equipment": self.multiworld.equipment[self.player].value - } - - # remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled - if not (self.multiworld.enable_lunar[self.player] or pool_option == ItemWeights.option_lunartic): - junk_pool.pop("Lunar Item") - # remove void items from the pool - if not (self.multiworld.dlc_sotv[self.player] or pool_option == ItemWeights.option_void): - junk_pool.pop("Void Item") - # Generate item pool itempool: List = [] # Add revive items for the player @@ -128,7 +100,6 @@ def create_items(self) -> None: for env_name, _ in environments_pool.items(): itempool += [env_name] - nonjunk_item_count = len(itempool) if self.multiworld.goal[self.player] == "classic": # classic mode total_locations = self.multiworld.total_locations[self.player].value @@ -144,10 +115,11 @@ def create_items(self) -> None: dlc_sotv=self.multiworld.dlc_sotv[self.player].value ) ) - junk_item_count = total_locations - nonjunk_item_count + # Create junk items + self.junk_pool = self.create_junk_pool() # Fill remaining items with randomly generated junk - itempool += self.multiworld.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()), - k=junk_item_count) + while len(itempool) < total_locations: + itempool.append(self.get_filler_item_name()) # Convert itempool into real items itempool = list(map(lambda name: self.create_item(name), itempool)) @@ -156,6 +128,49 @@ def create_items(self) -> None: def set_rules(self) -> None: set_rules(self.multiworld, self.player) + def get_filler_item_name(self) -> str: + if not self.junk_pool: + self.junk_pool = self.create_junk_pool() + weights = [data for data in self.junk_pool.values()] + filler = self.multiworld.random.choices([filler for filler in self.junk_pool.keys()], weights, + k=1)[0] + return filler + + def create_junk_pool(self) -> Dict: + # if presets are enabled generate junk_pool from the selected preset + pool_option = self.multiworld.item_weights[self.player].value + junk_pool: Dict[str, int] = {} + if self.multiworld.item_pool_presets[self.player]: + # generate chaos weights if the preset is chosen + if pool_option == ItemWeights.option_chaos: + for name, max_value in item_pool_weights[pool_option].items(): + junk_pool[name] = self.multiworld.random.randint(0, max_value) + else: + junk_pool = item_pool_weights[pool_option].copy() + else: # generate junk pool from user created presets + junk_pool = { + "Item Scrap, Green": self.multiworld.green_scrap[self.player].value, + "Item Scrap, Red": self.multiworld.red_scrap[self.player].value, + "Item Scrap, Yellow": self.multiworld.yellow_scrap[self.player].value, + "Item Scrap, White": self.multiworld.white_scrap[self.player].value, + "Common Item": self.multiworld.common_item[self.player].value, + "Uncommon Item": self.multiworld.uncommon_item[self.player].value, + "Legendary Item": self.multiworld.legendary_item[self.player].value, + "Boss Item": self.multiworld.boss_item[self.player].value, + "Lunar Item": self.multiworld.lunar_item[self.player].value, + "Void Item": self.multiworld.void_item[self.player].value, + "Equipment": self.multiworld.equipment[self.player].value + } + + # remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled + if not (self.multiworld.enable_lunar[self.player] or pool_option == ItemWeights.option_lunartic): + junk_pool.pop("Lunar Item") + # remove void items from the pool + if not (self.multiworld.dlc_sotv[self.player] or pool_option == ItemWeights.option_void): + junk_pool.pop("Void Item") + + return junk_pool + def create_regions(self) -> None: if self.multiworld.goal[self.player] == "classic": @@ -213,7 +228,7 @@ def create_item(self, name: str) -> Item: classification = ItemClassification.trap # Only check for an item to be a environment unlock if those are known to be in the pool. - # This should shave down comparions. + # This should shave down comparisons. elif name in environment_ALL_table.keys(): if name in {"Hidden Realm: Bulwark's Ambry", "Hidden Realm: Gilded Coast,"}: diff --git a/worlds/ror2/docs/en_Risk of Rain 2.md b/worlds/ror2/docs/en_Risk of Rain 2.md index ca22d1a44d70..d30edf888944 100644 --- a/worlds/ror2/docs/en_Risk of Rain 2.md +++ b/worlds/ror2/docs/en_Risk of Rain 2.md @@ -8,7 +8,7 @@ config file. ## What does randomization do to this game? Risk of Rain is already a random game, by virtue of being a roguelite. The Archipelago mod implements pure multiworld -functionality in which certain chests (made clear via a location check progress bar) will send an item out to the +functionality in which certain chests will send an item out to the multiworld. The items that _would have been_ in those chests will be returned to the Risk of Rain player via grants by other players in other worlds. @@ -16,28 +16,30 @@ There are two modes in risk of rain. Classic Mode and Explore Mode Classic Mode: - - Classic mode implements pure multiworld -functionality in which certain chests (made clear via a location check progress bar) will send an item out to the -multiworld. The items that _would have been_ in those chests will be returned to the Risk of Rain player via grants by -other players in other worlds. + - Certain chests (made clear via a location check progress bar) will send an item out to the + multiworld. The location of these chests do not matter, since all environments share a unified location pool. Explore Mode: - - Just like in Classic mode chests will send out an item to the multiworld. The difference is that each environment - will have a set amount that can be sent out and shrines along with other things that will need to be checked. - Also, each environment is an item and, you'll need it to be able to access it. + - Chests will continue to work as they did in Classic Mode, the difference being that each environment + will have a set amount of items that can be sent out. In addition, shrines, radio scanners, newt altars, + and scavenger bags will need to be checked, depending on your settings. + This mode also makes each environment an item. In order to access a particular stage, you'll need it to be + sent in the multiworld. ## What is the goal of Risk of Rain 2 in Archipelago? -Just like in the original game, any way to "beat the game" counts as a win. Alternatively, if you are new to the game and +Just like in the original game, any way to "beat the game" counts as a win. This means beating one of the bosses +on Commencement, The Planetarium, or A Moment, Whole. Alternatively, if you are new to the game and aren't very confident in being able to "beat the game", you can set **Final Stage Death is Win** to true -(You can turn this on in your player settings.) This will make it so if you die on either Commencement or The Planetarium, -it will count as your goal, and **Obliterating yourself** will count as well. +(You can turn this on in your player settings.) This will make it so dying on either Commencement or The Planetarium, +or **obliterating yourself in A Moment, Fractured** will count as your goal. **You do not need to complete all the location checks** to win; any item you don't collect may be released if the server options allow. If you die before you accomplish your goal, you can start a new run. You will start the run with any items that you -received from other players. Any items that you picked up the "normal" way will be lost. +received from other players. However, these items will be randomized within their rarity at the start of each run. +Any items that you picked up the "normal" way will be lost. Note, you can play Simulacrum mode as part of an Archipelago, but you can't achieve any of the victory conditions in Simulacrum. So you could, for example, collect most of your items through a Simulacrum run(only works in classic mode), @@ -72,10 +74,10 @@ The Risk of Rain items are: Each item grants you a random in-game item from the category it belongs to. -When an item is granted by another world to the Risk of Rain player (one of the items listed above) then a random +When an item is granted by another world to the Risk of Rain player then a random in-game item of that tier will appear in the Risk of Rain player's inventory. If the item grant is an `Equipment` and -the player already has an equipment item equipped then the _item that was equipped_ will be dropped on the ground and _ -the new equipment_ will take it's place. (If you want the old one back, pick it up.) +the player already has an equipment item equipped then the _item that was equipped_ will be dropped on the ground and +_the new equipment_ will take it's place. Explore Mode items are: @@ -93,10 +95,10 @@ Dlc_Sotv items * `Sulfur Pools` * `Void Locus` -When a explore item is granted it will unlock that environment and will now be accessible to progress to victory! The -game will still pick randomly which environment is next but it will first check to see if they are available. If you have -them unlocked it will weight the game to have a ***higher chance*** to go to one you have checks versus one you have -already completed. You will still not be able to goto a stage 3 environment from a stage 1 environment. +When an explore item is granted, it will unlock that environment and will now be accessible! The +game will still pick randomly which environment is next, but it will first check to see if they are available. If you have +multiple of the next environments unlocked, it will weight the game to have a ***higher chance*** to go to one you +have checks in versus one you have already completed. You will still be unable to go to a stage 3 environment from a stage 1 environment. @@ -108,9 +110,9 @@ to 250** items. The number of items will be randomized between all players, so y item pickup step based on how many items the other players in the multiworld have. (Around 100 seems to be a good ballpark if you want to have a similar number of items to most other games.) -In explore mode the amount of checks base on how many **chests, shrines, scavengers, radio scanners and, newt altars** -are in the pool. With just the base game the numbers are **52 to 516** and with the dlc its **60 to 660** with -everything on default being **216** +In explore mode, the amount of checks are based on how many **chests, shrines, scavengers, radio scanners, and newt altars** +are in the pool. With just the base game, checks can range from **52 to 516**, with the DLC expanding it to **60 to 660**. +Leaving everything on default, the total number of checks comes out to **216** locations. After you have completed the specified number of checks, you won't send anything else to the multiworld. You can receive up to the specified number of randomized items from the multiworld as the players find them. In either case, @@ -120,12 +122,15 @@ you can continue to collect items as normal in Risk of Rain 2 if you've already When the Risk of Rain player fills up their location check bar then the next spawned item will become an item grant for another player's world (or possibly get sent back to yourself). The item in Risk of Rain will disappear in a poof of -smoke and the grant will automatically go out to the multiworld. +smoke and the grant will automatically go out to the multiworld. Additionally, you will see a message in the chat saying +what item you sent out. If the message does not appear, this likely means that another game has collected their items from you. ## What is the item pickup step? -The item pickup step is a YAML setting which allows you to set how many items you need to spawn before the _next_ item -that is spawned disappears (in a poof of smoke) and goes out to the multiworld. +The item pickup step is a setting in the YAML which allows you to set how many items you need to spawn before the _next_ item +that is spawned disappears (in a poof of smoke) and goes out to the multiworld. For instance, an item step of **1** means that +every other chest will send an item to the multiworld. An item step of **2** means that every third chest sends out an item +just as an item step of **0** would send an item on **each chest.** ## Is Archipelago compatible with other Risk of Rain 2 mods? diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index 69ad8b8180ff..496d18fa379c 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -422,7 +422,7 @@ def create_item(self, name: str, force_non_progression=False, goal=0) -> Item: return created_item def get_filler_item_name(self) -> str: - self.multiworld.random.choice(junk_table.keys()) + return self.multiworld.random.choice(list(junk_table.keys())) def set_rules(self): set_rules(self.multiworld, self.player, self.gate_bosses, self.boss_rush_map, self.mission_map, self.mission_count_map) diff --git a/worlds/sc2wol/Client.py b/worlds/sc2wol/Client.py new file mode 100644 index 000000000000..c544cf0c5519 --- /dev/null +++ b/worlds/sc2wol/Client.py @@ -0,0 +1,1201 @@ +from __future__ import annotations + +import asyncio +import copy +import ctypes +import logging +import multiprocessing +import os.path +import re +import sys +import typing +import queue +import zipfile +import io +import random +from pathlib import Path + +# CommonClient import first to trigger ModuleUpdater +from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser +from Utils import init_logging, is_windows + +if __name__ == "__main__": + init_logging("SC2Client", exception_logger="Client") + +logger = logging.getLogger("Client") +sc2_logger = logging.getLogger("Starcraft2") + +import nest_asyncio +from worlds._sc2common import bot +from worlds._sc2common.bot.data import Race +from worlds._sc2common.bot.main import run_game +from worlds._sc2common.bot.player import Bot +from worlds.sc2wol import SC2WoLWorld +from worlds.sc2wol.Items import lookup_id_to_name, get_full_item_list, ItemData, type_flaggroups, upgrade_numbers +from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET +from worlds.sc2wol.MissionTables import lookup_id_to_mission +from worlds.sc2wol.Regions import MissionInfo + +import colorama +from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser, JSONtoTextParser, JSONMessagePart +from MultiServer import mark_raw + +loop = asyncio.get_event_loop_policy().new_event_loop() +nest_asyncio.apply(loop) +max_bonus: int = 13 +victory_modulo: int = 100 + +# GitHub repo where the Map/mod data is hosted for /download_data command +DATA_REPO_OWNER = "Ziktofel" +DATA_REPO_NAME = "Archipelago-SC2-data" +DATA_API_VERSION = "API2" + + +# Data version file path. +# This file is used to tell if the downloaded data are outdated +# Associated with /download_data command +def get_metadata_file(): + return os.environ["SC2PATH"] + os.sep + "ArchipelagoSC2Metadata.txt" + + +class StarcraftClientProcessor(ClientCommandProcessor): + ctx: SC2Context + + def _cmd_difficulty(self, difficulty: str = "") -> bool: + """Overrides the current difficulty set for the world. Takes the argument casual, normal, hard, or brutal""" + options = difficulty.split() + num_options = len(options) + + if num_options > 0: + difficulty_choice = options[0].lower() + if difficulty_choice == "casual": + self.ctx.difficulty_override = 0 + elif difficulty_choice == "normal": + self.ctx.difficulty_override = 1 + elif difficulty_choice == "hard": + self.ctx.difficulty_override = 2 + elif difficulty_choice == "brutal": + self.ctx.difficulty_override = 3 + else: + self.output("Unable to parse difficulty '" + options[0] + "'") + return False + + self.output("Difficulty set to " + options[0]) + return True + + else: + if self.ctx.difficulty == -1: + self.output("Please connect to a seed before checking difficulty.") + else: + current_difficulty = self.ctx.difficulty + if self.ctx.difficulty_override >= 0: + current_difficulty = self.ctx.difficulty_override + self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][current_difficulty]) + self.output("To change the difficulty, add the name of the difficulty after the command.") + return False + + + def _cmd_game_speed(self, game_speed: str = "") -> bool: + """Overrides the current game speed for the world. + Takes the arguments default, slower, slow, normal, fast, faster""" + options = game_speed.split() + num_options = len(options) + + if num_options > 0: + speed_choice = options[0].lower() + if speed_choice == "default": + self.ctx.game_speed_override = 0 + elif speed_choice == "slower": + self.ctx.game_speed_override = 1 + elif speed_choice == "slow": + self.ctx.game_speed_override = 2 + elif speed_choice == "normal": + self.ctx.game_speed_override = 3 + elif speed_choice == "fast": + self.ctx.game_speed_override = 4 + elif speed_choice == "faster": + self.ctx.game_speed_override = 5 + else: + self.output("Unable to parse game speed '" + options[0] + "'") + return False + + self.output("Game speed set to " + options[0]) + return True + + else: + if self.ctx.game_speed == -1: + self.output("Please connect to a seed before checking game speed.") + else: + current_speed = self.ctx.game_speed + if self.ctx.game_speed_override >= 0: + current_speed = self.ctx.game_speed_override + self.output("Current game speed: " + + ["Default", "Slower", "Slow", "Normal", "Fast", "Faster"][current_speed]) + self.output("To change the game speed, add the name of the speed after the command," + " or Default to select based on difficulty.") + return False + + def _cmd_color(self, color: str = "") -> bool: + player_colors = [ + "White", "Red", "Blue", "Teal", + "Purple", "Yellow", "Orange", "Green", + "LightPink", "Violet", "LightGrey", "DarkGreen", + "Brown", "LightGreen", "DarkGrey", "Pink", + "Rainbow", "Random", "Default" + ] + match_colors = [player_color.lower() for player_color in player_colors] + if color: + if color.lower() not in match_colors: + self.output(color + " is not a valid color. Available colors: " + ', '.join(player_colors)) + return False + if color.lower() == "random": + color = random.choice(player_colors[:16]) + self.ctx.player_color = match_colors.index(color.lower()) + self.output("Color set to " + player_colors[self.ctx.player_color]) + else: + self.output("Current player color: " + player_colors[self.ctx.player_color]) + self.output("To change your colors, add the name of the color after the command.") + self.output("Available colors: " + ', '.join(player_colors)) + + def _cmd_disable_mission_check(self) -> bool: + """Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play + the next mission in a chain the other player is doing.""" + self.ctx.missions_unlocked = True + sc2_logger.info("Mission check has been disabled") + return True + + def _cmd_play(self, mission_id: str = "") -> bool: + """Start a Starcraft 2 mission""" + + options = mission_id.split() + num_options = len(options) + + if num_options > 0: + mission_number = int(options[0]) + + self.ctx.play_mission(mission_number) + + else: + sc2_logger.info( + "Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.") + return False + + return True + + def _cmd_available(self) -> bool: + """Get what missions are currently available to play""" + + request_available_missions(self.ctx) + return True + + def _cmd_unfinished(self) -> bool: + """Get what missions are currently available to play and have not had all locations checked""" + + request_unfinished_missions(self.ctx) + return True + + @mark_raw + def _cmd_set_path(self, path: str = '') -> bool: + """Manually set the SC2 install directory (if the automatic detection fails).""" + if path: + os.environ["SC2PATH"] = path + is_mod_installed_correctly() + return True + else: + sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.") + return False + + def _cmd_download_data(self) -> bool: + """Download the most recent release of the necessary files for playing SC2 with + Archipelago. Will overwrite existing files.""" + if "SC2PATH" not in os.environ: + check_game_install_path() + + if os.path.exists(get_metadata_file()): + with open(get_metadata_file(), "r") as f: + metadata = f.read() + else: + metadata = None + + tempzip, metadata = download_latest_release_zip(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, + metadata=metadata, force_download=True) + + if tempzip != '': + try: + zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"]) + sc2_logger.info(f"Download complete. Package installed.") + with open(get_metadata_file(), "w") as f: + f.write(metadata) + finally: + os.remove(tempzip) + else: + sc2_logger.warning("Download aborted/failed. Read the log for more information.") + return False + return True + + +class SC2JSONtoTextParser(JSONtoTextParser): + def __init__(self, ctx): + self.handlers = { + "ItemSend": self._handle_color, + "ItemCheat": self._handle_color, + "Hint": self._handle_color, + } + super().__init__(ctx) + + def _handle_color(self, node: JSONMessagePart): + codes = node["color"].split(";") + buffer = "".join(self.color_code(code) for code in codes if code in self.color_codes) + return buffer + self._handle_text(node) + '' + + def color_code(self, code: str): + return '' + + +class SC2Context(CommonContext): + command_processor = StarcraftClientProcessor + game = "Starcraft 2 Wings of Liberty" + items_handling = 0b111 + difficulty = -1 + game_speed = -1 + all_in_choice = 0 + mission_order = 0 + player_color = 2 + mission_req_table: typing.Dict[str, MissionInfo] = {} + final_mission: int = 29 + announcements = queue.Queue() + sc2_run_task: typing.Optional[asyncio.Task] = None + missions_unlocked: bool = False # allow launching missions ignoring requirements + generic_upgrade_missions = 0 + generic_upgrade_research = 0 + generic_upgrade_items = 0 + current_tooltip = None + last_loc_list = None + difficulty_override = -1 + game_speed_override = -1 + mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {} + last_bot: typing.Optional[ArchipelagoBot] = None + + def __init__(self, *args, **kwargs): + super(SC2Context, self).__init__(*args, **kwargs) + self.raw_text_parser = SC2JSONtoTextParser(self) + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(SC2Context, self).server_auth(password_requested) + await self.get_username() + await self.send_connect() + + def on_package(self, cmd: str, args: dict): + if cmd in {"Connected"}: + self.difficulty = args["slot_data"]["game_difficulty"] + if "game_speed" in args["slot_data"]: + self.game_speed = args["slot_data"]["game_speed"] + else: + self.game_speed = 0 + self.all_in_choice = args["slot_data"]["all_in_map"] + slot_req_table = args["slot_data"]["mission_req"] + # Maintaining backwards compatibility with older slot data + self.mission_req_table = { + mission: MissionInfo( + **{field: value for field, value in mission_info.items() if field in MissionInfo._fields} + ) + for mission, mission_info in slot_req_table.items() + } + self.mission_order = args["slot_data"].get("mission_order", 0) + self.final_mission = args["slot_data"].get("final_mission", 29) + self.player_color = args["slot_data"].get("player_color", 2) + self.generic_upgrade_missions = args["slot_data"].get("generic_upgrade_missions", 0) + self.generic_upgrade_items = args["slot_data"].get("generic_upgrade_items", 0) + self.generic_upgrade_research = args["slot_data"].get("generic_upgrade_research", 0) + + self.build_location_to_mission_mapping() + + # Looks for the required maps and mods for SC2. Runs check_game_install_path. + maps_present = is_mod_installed_correctly() + if os.path.exists(get_metadata_file()): + with open(get_metadata_file(), "r") as f: + current_ver = f.read() + sc2_logger.debug(f"Current version: {current_ver}") + if is_mod_update_available(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, current_ver): + sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.") + elif maps_present: + sc2_logger.warning("NOTICE: Your map files may be outdated (version number not found). " + "Run /download_data to update them.") + + + def on_print_json(self, args: dict): + # goes to this world + if "receiving" in args and self.slot_concerns_self(args["receiving"]): + relevant = True + # found in this world + elif "item" in args and self.slot_concerns_self(args["item"].player): + relevant = True + # not related + else: + relevant = False + + if relevant: + self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"]))) + + super(SC2Context, self).on_print_json(args) + + def run_gui(self): + from kvui import GameManager, HoverBehavior, ServerToolTip + from kivy.app import App + from kivy.clock import Clock + from kivy.uix.tabbedpanel import TabbedPanelItem + from kivy.uix.gridlayout import GridLayout + from kivy.lang import Builder + from kivy.uix.label import Label + from kivy.uix.button import Button + from kivy.uix.floatlayout import FloatLayout + from kivy.properties import StringProperty + + class HoverableButton(HoverBehavior, Button): + pass + + class MissionButton(HoverableButton): + tooltip_text = StringProperty("Test") + ctx: SC2Context + + def __init__(self, *args, **kwargs): + super(HoverableButton, self).__init__(*args, **kwargs) + self.layout = FloatLayout() + self.popuplabel = ServerToolTip(text=self.text) + self.layout.add_widget(self.popuplabel) + + def on_enter(self): + self.popuplabel.text = self.tooltip_text + + if self.ctx.current_tooltip: + App.get_running_app().root.remove_widget(self.ctx.current_tooltip) + + if self.tooltip_text == "": + self.ctx.current_tooltip = None + else: + App.get_running_app().root.add_widget(self.layout) + self.ctx.current_tooltip = self.layout + + def on_leave(self): + self.ctx.ui.clear_tooltip() + + @property + def ctx(self) -> CommonContext: + return App.get_running_app().ctx + + class MissionLayout(GridLayout): + pass + + class MissionCategory(GridLayout): + pass + + class SC2Manager(GameManager): + logging_pairs = [ + ("Client", "Archipelago"), + ("Starcraft2", "Starcraft2"), + ] + base_title = "Archipelago Starcraft 2 Client" + + mission_panel = None + last_checked_locations = {} + mission_id_to_button = {} + launching: typing.Union[bool, int] = False # if int -> mission ID + refresh_from_launching = True + first_check = True + ctx: SC2Context + + def __init__(self, ctx): + super().__init__(ctx) + + def clear_tooltip(self): + if self.ctx.current_tooltip: + App.get_running_app().root.remove_widget(self.ctx.current_tooltip) + + self.ctx.current_tooltip = None + + def build(self): + container = super().build() + + panel = TabbedPanelItem(text="Starcraft 2 Launcher") + self.mission_panel = panel.content = MissionLayout() + + self.tabs.add_widget(panel) + + Clock.schedule_interval(self.build_mission_table, 0.5) + + return container + + def build_mission_table(self, dt): + if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or + not self.refresh_from_launching)) or self.first_check: + self.refresh_from_launching = True + + self.mission_panel.clear_widgets() + if self.ctx.mission_req_table: + self.last_checked_locations = self.ctx.checked_locations.copy() + self.first_check = False + + self.mission_id_to_button = {} + categories = {} + available_missions, unfinished_missions = calc_unfinished_missions(self.ctx) + + # separate missions into categories + for mission in self.ctx.mission_req_table: + if not self.ctx.mission_req_table[mission].category in categories: + categories[self.ctx.mission_req_table[mission].category] = [] + + categories[self.ctx.mission_req_table[mission].category].append(mission) + + for category in categories: + category_panel = MissionCategory() + if category.startswith('_'): + category_display_name = '' + else: + category_display_name = category + category_panel.add_widget( + Label(text=category_display_name, size_hint_y=None, height=50, outline_width=1)) + + for mission in categories[category]: + text: str = mission + tooltip: str = "" + mission_id: int = self.ctx.mission_req_table[mission].id + # Map has uncollected locations + if mission in unfinished_missions: + text = f"[color=6495ED]{text}[/color]" + elif mission in available_missions: + text = f"[color=FFFFFF]{text}[/color]" + # Map requirements not met + else: + text = f"[color=a9a9a9]{text}[/color]" + tooltip = f"Requires: " + if self.ctx.mission_req_table[mission].required_world: + tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for + req_mission in + self.ctx.mission_req_table[mission].required_world) + + if self.ctx.mission_req_table[mission].number: + tooltip += " and " + if self.ctx.mission_req_table[mission].number: + tooltip += f"{self.ctx.mission_req_table[mission].number} missions completed" + remaining_location_names: typing.List[str] = [ + self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission) + if loc in self.ctx.missing_locations] + + if mission_id == self.ctx.final_mission: + if mission in available_missions: + text = f"[color=FFBC95]{mission}[/color]" + else: + text = f"[color=D0C0BE]{mission}[/color]" + if tooltip: + tooltip += "\n" + tooltip += "Final Mission" + + if remaining_location_names: + if tooltip: + tooltip += "\n" + tooltip += f"Uncollected locations:\n" + tooltip += "\n".join(remaining_location_names) + + mission_button = MissionButton(text=text, size_hint_y=None, height=50) + mission_button.tooltip_text = tooltip + mission_button.bind(on_press=self.mission_callback) + self.mission_id_to_button[mission_id] = mission_button + category_panel.add_widget(mission_button) + + category_panel.add_widget(Label(text="")) + self.mission_panel.add_widget(category_panel) + + elif self.launching: + self.refresh_from_launching = False + + self.mission_panel.clear_widgets() + self.mission_panel.add_widget(Label(text="Launching Mission: " + + lookup_id_to_mission[self.launching])) + if self.ctx.ui: + self.ctx.ui.clear_tooltip() + + def mission_callback(self, button): + if not self.launching: + mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button) + if self.ctx.play_mission(mission_id): + self.launching = mission_id + Clock.schedule_once(self.finish_launching, 10) + + def finish_launching(self, dt): + self.launching = False + + self.ui = SC2Manager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + import pkgutil + data = pkgutil.get_data(SC2WoLWorld.__module__, "Starcraft2.kv").decode() + Builder.load_string(data) + + async def shutdown(self): + await super(SC2Context, self).shutdown() + if self.last_bot: + self.last_bot.want_close = True + if self.sc2_run_task: + self.sc2_run_task.cancel() + + def play_mission(self, mission_id: int) -> bool: + if self.missions_unlocked or \ + is_mission_available(self, mission_id): + if self.sc2_run_task: + if not self.sc2_run_task.done(): + sc2_logger.warning("Starcraft 2 Client is still running!") + self.sc2_run_task.cancel() # doesn't actually close the game, just stops the python task + if self.slot is None: + sc2_logger.warning("Launching Mission without Archipelago authentication, " + "checks will not be registered to server.") + self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id), + name="Starcraft 2 Launch") + return True + else: + sc2_logger.info( + f"{lookup_id_to_mission[mission_id]} is not currently unlocked. " + f"Use /unfinished or /available to see what is available.") + return False + + def build_location_to_mission_mapping(self): + mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = { + mission_info.id: set() for mission_info in self.mission_req_table.values() + } + + for loc in self.server_locations: + mission_id, objective = divmod(loc - SC2WOL_LOC_ID_OFFSET, victory_modulo) + mission_id_to_location_ids[mission_id].add(objective) + self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in + mission_id_to_location_ids.items()} + + def locations_for_mission(self, mission: str): + mission_id: int = self.mission_req_table[mission].id + objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id] + for objective in objectives: + yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective + + +async def main(): + multiprocessing.freeze_support() + parser = get_base_parser() + parser.add_argument('--name', default=None, help="Slot Name to connect as.") + args = parser.parse_args() + + ctx = SC2Context(args.connect, args.password) + ctx.auth = args.name + if ctx.server_task is None: + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + await ctx.exit_event.wait() + + await ctx.shutdown() + + +maps_table = [ + "ap_liberation_day", "ap_the_outlaws", "ap_zero_hour", + "ap_evacuation", "ap_outbreak", "ap_safe_haven", "ap_havens_fall", + "ap_smash_and_grab", "ap_the_dig", "ap_the_moebius_factor", "ap_supernova", "ap_maw_of_the_void", + "ap_devils_playground", "ap_welcome_to_the_jungle", "ap_breakout", "ap_ghost_of_a_chance", + "ap_the_great_train_robbery", "ap_cutthroat", "ap_engine_of_destruction", "ap_media_blitz", "ap_piercing_the_shroud", + "ap_whispers_of_doom", "ap_a_sinister_turn", "ap_echoes_of_the_future", "ap_in_utter_darkness", + "ap_gates_of_hell", "ap_belly_of_the_beast", "ap_shatter_the_sky", "ap_all_in" +] + +wol_default_categories = [ + "Mar Sara", "Mar Sara", "Mar Sara", "Colonist", "Colonist", "Colonist", "Colonist", + "Artifact", "Artifact", "Artifact", "Artifact", "Artifact", "Covert", "Covert", "Covert", "Covert", + "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Prophecy", "Prophecy", "Prophecy", "Prophecy", + "Char", "Char", "Char", "Char" +] +wol_default_category_names = [ + "Mar Sara", "Colonist", "Artifact", "Covert", "Rebellion", "Prophecy", "Char" +] + + +def calculate_items(ctx: SC2Context) -> typing.List[int]: + items = ctx.items_received + network_item: NetworkItem + accumulators: typing.List[int] = [0 for _ in type_flaggroups] + + for network_item in items: + name: str = lookup_id_to_name[network_item.item] + item_data: ItemData = get_full_item_list()[name] + + # exists exactly once + if item_data.quantity == 1: + accumulators[type_flaggroups[item_data.type]] |= 1 << item_data.number + + # exists multiple times + elif item_data.type == "Upgrade" or item_data.type == "Progressive Upgrade": + flaggroup = type_flaggroups[item_data.type] + + # Generic upgrades apply only to Weapon / Armor upgrades + if item_data.type != "Upgrade" or ctx.generic_upgrade_items == 0: + accumulators[flaggroup] += 1 << item_data.number + else: + for bundled_number in upgrade_numbers[item_data.number]: + accumulators[flaggroup] += 1 << bundled_number + + # sum + else: + accumulators[type_flaggroups[item_data.type]] += item_data.number + + # Upgrades from completed missions + if ctx.generic_upgrade_missions > 0: + upgrade_flaggroup = type_flaggroups["Upgrade"] + num_missions = ctx.generic_upgrade_missions * len(ctx.mission_req_table) + amounts = [ + num_missions // 100, + 2 * num_missions // 100, + 3 * num_missions // 100 + ] + upgrade_count = 0 + completed = len([id for id in ctx.mission_id_to_location_ids if SC2WOL_LOC_ID_OFFSET + victory_modulo * id in ctx.checked_locations]) + for amount in amounts: + if completed >= amount: + upgrade_count += 1 + # Equivalent to "Progressive Weapon/Armor Upgrade" item + for bundled_number in upgrade_numbers[5]: + accumulators[upgrade_flaggroup] += upgrade_count << bundled_number + + return accumulators + + +def calc_difficulty(difficulty): + if difficulty == 0: + return 'C' + elif difficulty == 1: + return 'N' + elif difficulty == 2: + return 'H' + elif difficulty == 3: + return 'B' + + return 'X' + + +async def starcraft_launch(ctx: SC2Context, mission_id: int): + sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.") + + with DllDirectory(None): + run_game(bot.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), + name="Archipelago", fullscreen=True)], realtime=True) + + +class ArchipelagoBot(bot.bot_ai.BotAI): + game_running: bool = False + mission_completed: bool = False + boni: typing.List[bool] + setup_done: bool + ctx: SC2Context + mission_id: int + want_close: bool = False + can_read_game = False + last_received_update: int = 0 + + def __init__(self, ctx: SC2Context, mission_id): + self.setup_done = False + self.ctx = ctx + self.ctx.last_bot = self + self.mission_id = mission_id + self.boni = [False for _ in range(max_bonus)] + + super(ArchipelagoBot, self).__init__() + + async def on_step(self, iteration: int): + if self.want_close: + self.want_close = False + await self._client.leave() + return + game_state = 0 + if not self.setup_done: + self.setup_done = True + start_items = calculate_items(self.ctx) + if self.ctx.difficulty_override >= 0: + difficulty = calc_difficulty(self.ctx.difficulty_override) + else: + difficulty = calc_difficulty(self.ctx.difficulty) + if self.ctx.game_speed_override >= 0: + game_speed = self.ctx.game_speed_override + else: + game_speed = self.ctx.game_speed + await self.chat_send("?SetOptions {} {} {} {}".format( + difficulty, + self.ctx.generic_upgrade_research, + self.ctx.all_in_choice, + game_speed + )) + await self.chat_send("?GiveResources {} {} {}".format( + start_items[8], + start_items[9], + start_items[10] + )) + await self.chat_send("?GiveTerranTech {} {} {} {} {} {} {} {} {} {}".format( + start_items[0], start_items[1], start_items[2], start_items[3], start_items[4], + start_items[5], start_items[6], start_items[12], start_items[13], start_items[14])) + await self.chat_send("?GiveProtossTech {}".format(start_items[7])) + await self.chat_send("?SetColor rr " + str(self.ctx.player_color)) # TODO: Add faction color options + await self.chat_send("?LoadFinished") + self.last_received_update = len(self.ctx.items_received) + + else: + if not self.ctx.announcements.empty(): + message = self.ctx.announcements.get(timeout=1) + await self.chat_send("?SendMessage " + message) + self.ctx.announcements.task_done() + + # Archipelago reads the health + for unit in self.all_own_units(): + if unit.health_max == 38281: + game_state = int(38281 - unit.health) + self.can_read_game = True + + if iteration == 160 and not game_state & 1: + await self.chat_send("?SendMessage Warning: Archipelago unable to connect or has lost connection to " + + "Starcraft 2 (This is likely a map issue)") + + if self.last_received_update < len(self.ctx.items_received): + current_items = calculate_items(self.ctx) + await self.chat_send("?GiveTerranTech {} {} {} {} {} {} {} {} {} {}".format( + current_items[0], current_items[1], current_items[2], current_items[3], current_items[4], + current_items[5], current_items[6], current_items[12], current_items[13], current_items[14])) + await self.chat_send("?GiveProtossTech {}".format(current_items[7])) + self.last_received_update = len(self.ctx.items_received) + + if game_state & 1: + if not self.game_running: + print("Archipelago Connected") + self.game_running = True + + if self.can_read_game: + if game_state & (1 << 1) and not self.mission_completed: + if self.mission_id != self.ctx.final_mission: + print("Mission Completed") + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', + "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id]}]) + self.mission_completed = True + else: + print("Game Complete") + await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}]) + self.mission_completed = True + + for x, completed in enumerate(self.boni): + if not completed and game_state & (1 << (x + 2)): + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', + "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}]) + self.boni[x] = True + + else: + await self.chat_send("?SendMessage LostConnection - Lost connection to game.") + + +def request_unfinished_missions(ctx: SC2Context): + if ctx.mission_req_table: + message = "Unfinished Missions: " + unlocks = initialize_blank_mission_dict(ctx.mission_req_table) + unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table) + + _, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks) + + # Removing All-In from location pool + final_mission = lookup_id_to_mission[ctx.final_mission] + if final_mission in unfinished_missions.keys(): + message = f"Final Mission Available: {final_mission}[{ctx.final_mission}]\n" + message + if unfinished_missions[final_mission] == -1: + unfinished_missions.pop(final_mission) + + message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " + + mark_up_objectives( + f"[{len(unfinished_missions[mission])}/" + f"{sum(1 for _ in ctx.locations_for_mission(mission))}]", + ctx, unfinished_locations, mission) + for mission in unfinished_missions) + + if ctx.ui: + ctx.ui.log_panels['All'].on_message_markup(message) + ctx.ui.log_panels['Starcraft2'].on_message_markup(message) + else: + sc2_logger.info(message) + else: + sc2_logger.warning("No mission table found, you are likely not connected to a server.") + + +def calc_unfinished_missions(ctx: SC2Context, unlocks=None): + unfinished_missions = [] + locations_completed = [] + + if not unlocks: + unlocks = initialize_blank_mission_dict(ctx.mission_req_table) + + available_missions = calc_available_missions(ctx, unlocks) + + for name in available_missions: + objectives = set(ctx.locations_for_mission(name)) + if objectives: + objectives_completed = ctx.checked_locations & objectives + if len(objectives_completed) < len(objectives): + unfinished_missions.append(name) + locations_completed.append(objectives_completed) + + else: # infer that this is the final mission as it has no objectives + unfinished_missions.append(name) + locations_completed.append(-1) + + return available_missions, dict(zip(unfinished_missions, locations_completed)) + + +def is_mission_available(ctx: SC2Context, mission_id_to_check): + unfinished_missions = calc_available_missions(ctx) + + return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions) + + +def mark_up_mission_name(ctx: SC2Context, mission, unlock_table): + """Checks if the mission is required for game completion and adds '*' to the name to mark that.""" + + if ctx.mission_req_table[mission].completion_critical: + if ctx.ui: + message = "[color=AF99EF]" + mission + "[/color]" + else: + message = "*" + mission + "*" + else: + message = mission + + if ctx.ui: + unlocks = unlock_table[mission] + + if len(unlocks) > 0: + pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: " + pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].id})" for unlock in unlocks) + pre_message += f"]" + message = pre_message + message + "[/ref]" + + return message + + +def mark_up_objectives(message, ctx, unfinished_locations, mission): + formatted_message = message + + if ctx.ui: + locations = unfinished_locations[mission] + + pre_message = f"[ref={list(ctx.mission_req_table).index(mission) + 30}|" + pre_message += "
".join(location for location in locations) + pre_message += f"]" + formatted_message = pre_message + message + "[/ref]" + + return formatted_message + + +def request_available_missions(ctx: SC2Context): + if ctx.mission_req_table: + message = "Available Missions: " + + # Initialize mission unlock table + unlocks = initialize_blank_mission_dict(ctx.mission_req_table) + + missions = calc_available_missions(ctx, unlocks) + message += \ + ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}" + f"[{ctx.mission_req_table[mission].id}]" + for mission in missions) + + if ctx.ui: + ctx.ui.log_panels['All'].on_message_markup(message) + ctx.ui.log_panels['Starcraft2'].on_message_markup(message) + else: + sc2_logger.info(message) + else: + sc2_logger.warning("No mission table found, you are likely not connected to a server.") + + +def calc_available_missions(ctx: SC2Context, unlocks=None): + available_missions = [] + missions_complete = 0 + + # Get number of missions completed + for loc in ctx.checked_locations: + if loc % victory_modulo == 0: + missions_complete += 1 + + for name in ctx.mission_req_table: + # Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips + if unlocks: + for unlock in ctx.mission_req_table[name].required_world: + unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name) + + if mission_reqs_completed(ctx, name, missions_complete): + available_missions.append(name) + + return available_missions + + +def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int): + """Returns a bool signifying if the mission has all requirements complete and can be done + + Arguments: + ctx -- instance of SC2Context + locations_to_check -- the mission string name to check + missions_complete -- an int of how many missions have been completed + mission_path -- a list of missions that have already been checked +""" + if len(ctx.mission_req_table[mission_name].required_world) >= 1: + # A check for when the requirements are being or'd + or_success = False + + # Loop through required missions + for req_mission in ctx.mission_req_table[mission_name].required_world: + req_success = True + + # Check if required mission has been completed + if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id * + victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations: + if not ctx.mission_req_table[mission_name].or_requirements: + return False + else: + req_success = False + + # Grid-specific logic (to avoid long path checks and infinite recursion) + if ctx.mission_order in (3, 4): + if req_success: + return True + else: + if req_mission is ctx.mission_req_table[mission_name].required_world[-1]: + return False + else: + continue + + # Recursively check required mission to see if it's requirements are met, in case !collect has been done + # Skipping recursive check on Grid settings to speed up checks and avoid infinite recursion + if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete): + if not ctx.mission_req_table[mission_name].or_requirements: + return False + else: + req_success = False + + # If requirement check succeeded mark or as satisfied + if ctx.mission_req_table[mission_name].or_requirements and req_success: + or_success = True + + if ctx.mission_req_table[mission_name].or_requirements: + # Return false if or requirements not met + if not or_success: + return False + + # Check number of missions + if missions_complete >= ctx.mission_req_table[mission_name].number: + return True + else: + return False + else: + return True + + +def initialize_blank_mission_dict(location_table): + unlocks = {} + + for mission in list(location_table): + unlocks[mission] = [] + + return unlocks + + +def check_game_install_path() -> bool: + # First thing: go to the default location for ExecuteInfo. + # An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it. + if is_windows: + # The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow. + # https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555# + import ctypes.wintypes + CSIDL_PERSONAL = 5 # My Documents + SHGFP_TYPE_CURRENT = 0 # Get current, not default value + + buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) + ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf) + documentspath = buf.value + einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt")) + else: + einfo = str(bot.paths.get_home() / Path(bot.paths.USERPATH[bot.paths.PF])) + + # Check if the file exists. + if os.path.isfile(einfo): + + # Open the file and read it, picking out the latest executable's path. + with open(einfo) as f: + content = f.read() + if content: + try: + base = re.search(r" = (.*)Versions", content).group(1) + except AttributeError: + sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, then " + f"try again.") + return False + if os.path.exists(base): + executable = bot.paths.latest_executeble(Path(base).expanduser() / "Versions") + + # Finally, check the path for an actual executable. + # If we find one, great. Set up the SC2PATH. + if os.path.isfile(executable): + sc2_logger.info(f"Found an SC2 install at {base}!") + sc2_logger.debug(f"Latest executable at {executable}.") + os.environ["SC2PATH"] = base + sc2_logger.debug(f"SC2PATH set to {base}.") + return True + else: + sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.") + else: + sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.") + else: + sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. " + f"If that fails, please run /set_path with your SC2 install directory.") + return False + + +def is_mod_installed_correctly() -> bool: + """Searches for all required files.""" + if "SC2PATH" not in os.environ: + check_game_install_path() + + mapdir = os.environ['SC2PATH'] / Path('Maps/ArchipelagoCampaign') + mods = ["ArchipelagoCore", "ArchipelagoPlayer", "ArchipelagoPlayerWoL", "ArchipelagoTriggers"] + modfiles = [os.environ["SC2PATH"] / Path("Mods/" + mod + ".SC2Mod") for mod in mods] + wol_required_maps = ["WoL" + os.sep + map_name + ".SC2Map" for map_name in maps_table] + needs_files = False + + # Check for maps. + missing_maps = [] + for mapfile in wol_required_maps: + if not os.path.isfile(mapdir / mapfile): + missing_maps.append(mapfile) + if len(missing_maps) >= 19: + sc2_logger.warning(f"All map files missing from {mapdir}.") + needs_files = True + elif len(missing_maps) > 0: + for map in missing_maps: + sc2_logger.debug(f"Missing {map} from {mapdir}.") + sc2_logger.warning(f"Missing {len(missing_maps)} map files.") + needs_files = True + else: # Must be no maps missing + sc2_logger.info(f"All maps found in {mapdir}.") + + # Check for mods. + for modfile in modfiles: + if os.path.isfile(modfile) or os.path.isdir(modfile): + sc2_logger.info(f"Archipelago mod found at {modfile}.") + else: + sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.") + needs_files = True + + # Final verdict. + if needs_files: + sc2_logger.warning(f"Required files are missing. Run /download_data to acquire them.") + return False + else: + sc2_logger.debug(f"All map/mod files are properly installed.") + return True + + +class DllDirectory: + # Credit to Black Sliver for this code. + # More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw + _old: typing.Optional[str] = None + _new: typing.Optional[str] = None + + def __init__(self, new: typing.Optional[str]): + self._new = new + + def __enter__(self): + old = self.get() + if self.set(self._new): + self._old = old + + def __exit__(self, *args): + if self._old is not None: + self.set(self._old) + + @staticmethod + def get() -> typing.Optional[str]: + if sys.platform == "win32": + n = ctypes.windll.kernel32.GetDllDirectoryW(0, None) + buf = ctypes.create_unicode_buffer(n) + ctypes.windll.kernel32.GetDllDirectoryW(n, buf) + return buf.value + # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific + return None + + @staticmethod + def set(s: typing.Optional[str]) -> bool: + if sys.platform == "win32": + return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0 + # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific + return False + + +def download_latest_release_zip(owner: str, repo: str, api_version: str, metadata: str = None, force_download=False) -> (str, str): + """Downloads the latest release of a GitHub repo to the current directory as a .zip file.""" + import requests + + headers = {"Accept": 'application/vnd.github.v3+json'} + url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}" + + r1 = requests.get(url, headers=headers) + if r1.status_code == 200: + latest_metadata = str(r1.json()) + # sc2_logger.info(f"Latest version: {latest_metadata}.") + else: + sc2_logger.warning(f"Status code: {r1.status_code}") + sc2_logger.warning(f"Failed to reach GitHub. Could not find download link.") + sc2_logger.warning(f"text: {r1.text}") + return "", metadata + + if (force_download is False) and (metadata == latest_metadata): + sc2_logger.info("Latest version already installed.") + return "", metadata + + sc2_logger.info(f"Attempting to download latest version of API version {api_version} of {repo}.") + download_url = r1.json()["assets"][0]["browser_download_url"] + + 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: + fh.write(r2.content) + sc2_logger.info(f"Successfully downloaded {repo}.zip.") + return f"{repo}.zip", latest_metadata + else: + sc2_logger.warning(f"Status code: {r2.status_code}") + sc2_logger.warning("Download failed.") + sc2_logger.warning(f"text: {r2.text}") + return "", metadata + + +def is_mod_update_available(owner: str, repo: str, api_version: str, metadata: str) -> bool: + import requests + + headers = {"Accept": 'application/vnd.github.v3+json'} + url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}" + + r1 = requests.get(url, headers=headers) + if r1.status_code == 200: + latest_metadata = str(r1.json()) + if metadata != latest_metadata: + return True + else: + return False + + else: + sc2_logger.warning(f"Failed to reach GitHub while checking for updates.") + sc2_logger.warning(f"Status code: {r1.status_code}") + sc2_logger.warning(f"text: {r1.text}") + return False + + +def launch(): + colorama.init() + asyncio.run(main()) + colorama.deinit() diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index ea495adf79bc..971a75375fe4 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -12,6 +12,7 @@ class ItemData(typing.NamedTuple): classification: ItemClassification = ItemClassification.useful quantity: int = 1 parent_item: str = None + origin: typing.Set[str] = {"wol"} class StarcraftWoLItem(Item): @@ -43,23 +44,36 @@ def get_full_item_list(): "Ghost": ItemData(15 + SC2WOL_ITEM_ID_OFFSET, "Unit", 15, classification=ItemClassification.progression), "Spectre": ItemData(16 + SC2WOL_ITEM_ID_OFFSET, "Unit", 16, classification=ItemClassification.progression), "Thor": ItemData(17 + SC2WOL_ITEM_ID_OFFSET, "Unit", 17, classification=ItemClassification.progression), + # EE units + "Liberator": ItemData(18 + SC2WOL_ITEM_ID_OFFSET, "Unit", 18, classification=ItemClassification.progression, origin={"nco", "ext"}), + "Valkyrie": ItemData(19 + SC2WOL_ITEM_ID_OFFSET, "Unit", 19, classification=ItemClassification.progression, origin={"bw"}), + "Widow Mine": ItemData(20 + SC2WOL_ITEM_ID_OFFSET, "Unit", 20, classification=ItemClassification.progression, origin={"ext"}), + "Cyclone": ItemData(21 + SC2WOL_ITEM_ID_OFFSET, "Unit", 21, classification=ItemClassification.progression, origin={"ext"}), + # Some other items are moved to Upgrade group because of the way how the bot message is parsed "Progressive Infantry Weapon": ItemData(100 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, quantity=3), "Progressive Infantry Armor": ItemData(102 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, quantity=3), "Progressive Vehicle Weapon": ItemData(103 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, quantity=3), "Progressive Vehicle Armor": ItemData(104 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 6, quantity=3), "Progressive Ship Weapon": ItemData(105 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, quantity=3), "Progressive Ship Armor": ItemData(106 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 10, quantity=3), + # Upgrade bundle 'number' values are used as indices to get affected 'number's + "Progressive Weapon Upgrade": ItemData(107 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, quantity=3), + "Progressive Armor Upgrade": ItemData(108 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 1, quantity=3), + "Progressive Infantry Upgrade": ItemData(109 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, quantity=3), + "Progressive Vehicle Upgrade": ItemData(110 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 3, quantity=3), + "Progressive Ship Upgrade": ItemData(111 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, quantity=3), + "Progressive Weapon/Armor Upgrade": ItemData(112 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 5, quantity=3), "Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0, parent_item="Bunker"), "Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1, parent_item="Bunker"), "Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler, parent_item="Missile Turret"), "Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3, parent_item="Missile Turret"), - "Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4, parent_item="SCV"), - "Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5, parent_item="SCV"), + "Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4), + "Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5), "Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6), "Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7), - "Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, parent_item="Marine"), + "Progressive Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 0, parent_item="Marine", quantity=2), "Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression, parent_item="Marine"), "Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.filler, parent_item="Medic"), "Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression, parent_item="Medic"), @@ -69,10 +83,59 @@ def get_full_item_list(): "Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15, parent_item="Marauder"), "U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16, parent_item="Reaper"), "G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression, parent_item="Reaper"), + # Items from EE + "Mag-Field Accelerators (Cyclone)": ItemData(218 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 18, parent_item="Cyclone", origin={"ext"}), + "Mag-Field Launchers (Cyclone)": ItemData(219 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 19, parent_item="Cyclone", origin={"ext"}), + # Items from new mod + "Laser Targeting System (Marine)": ItemData(220 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, classification=ItemClassification.filler, parent_item="Marine", origin={"nco"}), # Freed slot from Stimpack + "Magrail Munitions (Marine)": ItemData(221 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 20, parent_item="Marine", origin={"nco"}), + "Optimized Logistics (Marine)": ItemData(222 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 21, classification=ItemClassification.filler, parent_item="Marine", origin={"nco"}), + "Restoration (Medic)": ItemData(223 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 22, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}), + "Optical Flare (Medic)": ItemData(224 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 23, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}), + "Optimized Logistics (Medic)": ItemData(225 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 24, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}), + "Progressive Stimpack (Firebat)": ItemData(226 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 6, parent_item="Firebat", quantity=2, origin={"bw"}), + "Optimized Logistics (Firebat)": ItemData(227 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 25, parent_item="Firebat", origin={"bw"}), + "Progressive Stimpack (Marauder)": ItemData(228 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 8, parent_item="Marauder", quantity=2, origin={"nco"}), + "Laser Targeting System (Marauder)": ItemData(229 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 26, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}), + "Magrail Munitions (Marauder)": ItemData(230 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 27, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}), + "Internal Tech Module (Marauder)": ItemData(231 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 28, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}), + + # Items from new mod + "Progressive Stimpack (Reaper)": ItemData(250 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 10, parent_item="Reaper", quantity=2, origin={"nco"}), + "Laser Targeting System (Reaper)": ItemData(251 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 0, classification=ItemClassification.filler, parent_item="Reaper", origin={"nco"}), + "Advanced Cloaking Field (Reaper)": ItemData(252 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 1, parent_item="Reaper", origin={"nco"}), + "Spider Mines (Reaper)": ItemData(253 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 2, classification=ItemClassification.filler, parent_item="Reaper", origin={"nco"}), + "Combat Drugs (Reaper)": ItemData(254 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 3, classification=ItemClassification.filler, parent_item="Reaper", origin={"ext"}), + "Hellbat Aspect (Hellion)": ItemData(255 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 4, parent_item="Hellion", origin={"nco"}), + "Smart Servos (Hellion)": ItemData(256 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 5, parent_item="Hellion", origin={"nco"}), + "Optimized Logistics (Hellion)": ItemData(257 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 6, classification=ItemClassification.filler, parent_item="Hellion", origin={"nco"}), + "Jump Jets (Hellion)": ItemData(258 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 7, classification=ItemClassification.filler, parent_item="Hellion", origin={"nco"}), + "Progressive Stimpack (Hellion)": ItemData(259 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 12, parent_item="Hellion", quantity=2, origin={"nco"}), + "Ion Thrusters (Vulture)": ItemData(260 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 8, classification=ItemClassification.filler, parent_item="Vulture", origin={"bw"}), + "Auto Launchers (Vulture)": ItemData(261 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 9, parent_item="Vulture", origin={"bw"}), + "High Explosive Munition (Spider Mine)": ItemData(262 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 10, origin={"bw"}), + "Jump Jets (Goliath)": ItemData(263 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 11, classification=ItemClassification.filler, parent_item="Goliath", origin={"nco"}), + "Optimized Logistics (Goliath)": ItemData(264 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 12, classification=ItemClassification.filler, parent_item="Goliath", origin={"nco"}), + "Hyperfluxor (Diamondback)": ItemData(265 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 13, parent_item="Diamondback", origin={"ext"}), + "Burst Capacitors (Diamondback)": ItemData(266 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 14, classification=ItemClassification.filler, parent_item="Diamondback", origin={"ext"}), + "Optimized Logistics (Diamondback)": ItemData(267 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 15, parent_item="Diamondback", origin={"ext"}), + "Jump Jets (Siege Tank)": ItemData(268 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 16, parent_item="Siege Tank", origin={"nco"}), + "Spider Mines (Siege Tank)": ItemData(269 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 17, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}), + "Smart Servos (Siege Tank)": ItemData(270 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 18, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}), + "Graduating Range (Siege Tank)": ItemData(271 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 19, classification=ItemClassification.progression, parent_item="Siege Tank", origin={"ext"}), + "Laser Targeting System (Siege Tank)": ItemData(272 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 20, parent_item="Siege Tank", origin={"nco"}), + "Advanced Siege Tech (Siege Tank)": ItemData(273 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 21, parent_item="Siege Tank", origin={"ext"}), + "Internal Tech Module (Siege Tank)": ItemData(274 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 22, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}), + "Optimized Logistics (Predator)": ItemData(275 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 23, classification=ItemClassification.filler, parent_item="Predator", origin={"ext"}), + "Expanded Hull (Medivac)": ItemData(276 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 24, classification=ItemClassification.filler, parent_item="Medivac", origin={"ext"}), + "Afterburners (Medivac)": ItemData(277 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 25, classification=ItemClassification.filler, parent_item="Medivac", origin={"ext"}), + "Advanced Laser Technology (Wraith)": ItemData(278 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 26, classification=ItemClassification.progression, parent_item="Wraith", origin={"ext"}), + "Smart Servos (Viking)": ItemData(279 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 27, parent_item="Viking", origin={"ext"}), + "Magrail Munitions (Viking)": ItemData(280 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 28, parent_item="Viking", origin={"ext"}), "Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler, parent_item="Hellion"), "Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1, parent_item="Hellion"), - "Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler, parent_item="Vulture"), + "Cerberus Mine (Spider Mine)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler), "Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler, parent_item="Vulture"), "Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4, parent_item="Goliath"), "Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5, parent_item="Goliath"), @@ -86,7 +149,7 @@ def get_full_item_list(): "Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler, parent_item="Wraith"), "Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14, parent_item="Viking"), "Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15, parent_item="Viking"), - "Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler, parent_item="Banshee"), + "Progressive Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 2, classification=ItemClassification.filler, parent_item="Banshee", quantity=2), "Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17, parent_item="Banshee"), "Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler, parent_item="Battlecruiser"), "Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler, parent_item="Battlecruiser"), @@ -96,6 +159,47 @@ def get_full_item_list(): "Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23, parent_item="Spectre"), "330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler, parent_item="Thor"), "Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler, parent_item="Thor"), + # Items from EE + "Advanced Ballistics (Liberator)": ItemData(326 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 26, parent_item="Liberator", origin={"ext"}), + "Raid Artillery (Liberator)": ItemData(327 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 27, classification=ItemClassification.progression, parent_item="Liberator", origin={"nco"}), + "Drilling Claws (Widow Mine)": ItemData(328 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 28, classification=ItemClassification.filler, parent_item="Widow Mine", origin={"ext"}), + "Concealment (Widow Mine)": ItemData(329 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 29, classification=ItemClassification.progression, parent_item="Widow Mine", origin={"ext"}), + + #Items from new mod + "Hyperflight Rotors (Banshee)": ItemData(350 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 0, classification=ItemClassification.filler, parent_item="Banshee", origin={"ext"}), + "Laser Targeting System (Banshee)": ItemData(351 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 1, classification=ItemClassification.filler, parent_item="Banshee", origin={"nco"}), + "Internal Tech Module (Banshee)": ItemData(352 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 2, classification=ItemClassification.filler, parent_item="Banshee", origin={"nco"}), + "Tactical Jump (Battlecruiser)": ItemData(353 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 3, parent_item="Battlecruiser", origin={"nco", "ext"}), + "Cloak (Battlecruiser)": ItemData(354 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 4, parent_item="Battlecruiser", origin={"nco"}), + "ATX Laser Battery (Battlecruiser)": ItemData(355 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 5, classification=ItemClassification.progression, parent_item="Battlecruiser", origin={"nco"}), + "Optimized Logistics (Battlecruiser)": ItemData(356 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 6, classification=ItemClassification.filler, parent_item="Battlecruiser", origin={"ext"}), + "Internal Tech Module (Battlecruiser)": ItemData(357 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 7, classification=ItemClassification.filler, parent_item="Battlecruiser", origin={"nco"}), + "EMP Rounds (Ghost)": ItemData(358 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 8, parent_item="Ghost", origin={"ext"}), + "Lockdown (Ghost)": ItemData(359 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 9, parent_item="Ghost", origin={"bw"}), + "Impaler Rounds (Spectre)": ItemData(360 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 10, parent_item="Spectre", origin={"ext"}), + "Progressive High Impact Payload (Thor)": ItemData(361 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 14, parent_item="Thor", quantity=2, origin={"ext"}), # L2 is Smart Servos + "Bio Mechanical Repair Drone (Raven)": ItemData(363 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 13, parent_item="Raven", origin={"nco"}), + "Spider Mines (Raven)": ItemData(364 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 14, parent_item="Raven", origin={"nco"}), + "Railgun Turret (Raven)": ItemData(365 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 15, parent_item="Raven", origin={"nco"}), + "Hunter-Seeker Weapon (Raven)": ItemData(366 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 16, parent_item="Raven", origin={"nco"}), + "Interference Matrix (Raven)": ItemData(367 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 17, parent_item="Raven", origin={"ext"}), + "Anti-Armor Missile (Raven)": ItemData(368 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 18, classification=ItemClassification.filler, parent_item="Raven", origin={"ext"}), + "Internal Tech Module (Raven)": ItemData(369 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 19, classification=ItemClassification.filler, parent_item="Raven", origin={"nco"}), + "EMP Shockwave (Science Vessel)": ItemData(370 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 20, parent_item="Science Vessel", origin={"bw"}), + "Defensive Matrix (Science Vessel)": ItemData(371 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 21, parent_item="Science Vessel", origin={"bw"}), + "Targeting Optics (Cyclone)": ItemData(372 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 22, parent_item="Cyclone", origin={"ext"}), + "Rapid Fire Launchers (Cyclone)": ItemData(373 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 23, parent_item="Cyclone", origin={"ext"}), + "Cloak (Liberator)": ItemData(374 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 24, classification=ItemClassification.filler, parent_item="Liberator", origin={"nco"}), + "Laser Targeting System (Liberator)": ItemData(375 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 25, classification=ItemClassification.filler, parent_item="Liberator", origin={"ext"}), + "Optimized Logistics (Liberator)": ItemData(376 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 26, classification=ItemClassification.filler, parent_item="Liberator", origin={"nco"}), + "Black Market Launchers (Widow Mine)": ItemData(377 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 27, classification=ItemClassification.filler, parent_item="Widow Mine", origin={"ext"}), + "Executioner Missiles (Widow Mine)": ItemData(378 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 28, parent_item="Widow Mine", origin={"ext"}), + + # Just lazy to create a new group for one unit + "Enhanced Cluster Launchers (Valkyrie)": ItemData(379 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17, parent_item="Valkyrie", origin={"ext"}), + "Shaped Hull (Valkyrie)": ItemData(380 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 20, classification=ItemClassification.filler, parent_item="Valkyrie", origin={"ext"}), + "Burst Lasers (Valkyrie)": ItemData(381 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 21, parent_item="Valkyrie", origin={"ext"}), + "Afterburners (Valkyrie)": ItemData(382 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 22, classification=ItemClassification.filler, parent_item="Valkyrie", origin={"ext"}), "Bunker": ItemData(400 + SC2WOL_ITEM_ID_OFFSET, "Building", 0, classification=ItemClassification.progression), "Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression), @@ -120,14 +224,14 @@ def get_full_item_list(): "Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, classification=ItemClassification.progression), "Tech Reactor": ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8), "Orbital Strike": ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9), - "Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, parent_item="Bunker"), - "Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"), + "Shrike Turret (Bunker)": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, parent_item="Bunker"), + "Fortified Bunker (Bunker)": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"), "Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, classification=ItemClassification.progression), "Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13, classification=ItemClassification.progression), "Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler), "Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression), "Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16), - "Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17), + "Progressive Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 4, quantity=2), "Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18, ItemClassification.progression), "Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.progression), @@ -136,18 +240,24 @@ def get_full_item_list(): "High Templar": ItemData(702 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 2, classification=ItemClassification.progression), "Dark Templar": ItemData(703 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 3, classification=ItemClassification.progression), "Immortal": ItemData(704 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 4, classification=ItemClassification.progression), - "Colossus": ItemData(705 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 5, classification=ItemClassification.progression), - "Phoenix": ItemData(706 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 6, classification=ItemClassification.progression), + "Colossus": ItemData(705 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 5), + "Phoenix": ItemData(706 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 6, classification=ItemClassification.filler), "Void Ray": ItemData(707 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 7, classification=ItemClassification.progression), "Carrier": ItemData(708 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 8, classification=ItemClassification.progression), + # Filler items to fill remaining spots "+15 Starting Minerals": ItemData(800 + SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, quantity=0, classification=ItemClassification.filler), "+15 Starting Vespene": ItemData(801 + SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, quantity=0, classification=ItemClassification.filler), + # This Filler item isn't placed by the generator yet unless plando'd "+2 Starting Supply": ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, quantity=0, classification=ItemClassification.filler), + # This item is used to "remove" location from the game. Never placed unless plando'd + "Nothing": ItemData(803 + SC2WOL_ITEM_ID_OFFSET, "Nothing Group", 2, quantity=0, classification=ItemClassification.trap), # "Keystone Piece": ItemData(850 + SC2WOL_ITEM_ID_OFFSET, "Goal", 0, quantity=0, classification=ItemClassification.progression_skip_balancing) } +def get_item_table(multiworld: MultiWorld, player: int): + return item_table basic_units = { 'Marine', @@ -172,10 +282,49 @@ def get_basic_units(multiworld: MultiWorld, player: int) -> typing.Set[str]: item_name_groups = {} -for item, data in item_table.items(): +for item, data in get_full_item_list().items(): item_name_groups.setdefault(data.type, []).append(item) + if data.type in ("Armory 1", "Armory 2") and '(' in item: + short_name = item[:item.find(' (')] + item_name_groups[short_name] = [item] item_name_groups["Missions"] = ["Beat " + mission_name for mission_name in vanilla_mission_req_table] + +# Items that can be placed before resources if not already in +# General upgrades and Mercs +second_pass_placeable_items: typing.Tuple[str, ...] = ( + # Buildings without upgrades + "Sensor Tower", + "Hive Mind Emulator", + "Psi Disrupter", + "Perdition Turret", + # General upgrades without any dependencies + "Advanced Construction (SCV)", + "Dual-Fusion Welders (SCV)", + "Fire-Suppression System (Building)", + "Orbital Command (Building)", + "Ultra-Capacitors", + "Vanadium Plating", + "Orbital Depots", + "Micro-Filtering", + "Automated Refinery", + "Command Center Reactor", + "Tech Reactor", + "Planetary Fortress", + "Cellular Reactor", + "Progressive Regenerative Bio-Steel", # Place only L1 + # Mercenaries + "War Pigs", + "Devil Dogs", + "Hammer Securities", + "Spartan Company", + "Siege Breakers", + "Hel's Angel", + "Dusk Wings", + "Jackson's Revenge" +) + + filler_items: typing.Tuple[str, ...] = ( '+15 Starting Minerals', '+15 Starting Vespene' @@ -190,7 +339,10 @@ def get_basic_units(multiworld: MultiWorld, player: int) -> typing.Set[str]: # Bunker w/ Marine/Marauder: 3, "Perdition Turret": 2, "Missile Turret": 2, - "Vulture": 2 + "Vulture": 2, + "Liberator": 2, + "Widow Mine": 2 + # "Concealment (Widow Mine)": 1 } zerg_defense_ratings = { "Perdition Turret": 2, @@ -199,14 +351,61 @@ def get_basic_units(multiworld: MultiWorld, player: int) -> typing.Set[str]: "Psi Disruptor": 3 } +spider_mine_sources = { + "Vulture", + "Spider Mines (Reaper)", + "Spider Mines (Siege Tank)", + "Spider Mines (Raven)" +} + +progressive_if_nco = { + "Progressive Stimpack (Marine)", + "Progressive Stimpack (Firebat)", + "Progressive Cross-Spectrum Dampeners (Banshee)", + "Progressive Regenerative Bio-Steel" +} + +# 'number' values of upgrades for upgrade bundle items +upgrade_numbers = [ + {0, 4, 8}, # Weapon + {2, 6, 10}, # Armor + {0, 2}, # Infantry + {4, 6}, # Vehicle + {8, 10}, # Starship + {0, 2, 4, 6, 8, 10} # All +] +# Names of upgrades to be included for different options +upgrade_included_names = [ + { # Individual Items + "Progressive Infantry Weapon", + "Progressive Infantry Armor", + "Progressive Vehicle Weapon", + "Progressive Vehicle Armor", + "Progressive Ship Weapon", + "Progressive Ship Armor" + }, + { # Bundle Weapon And Armor + "Progressive Weapon Upgrade", + "Progressive Armor Upgrade" + }, + { # Bundle Unit Class + "Progressive Infantry Upgrade", + "Progressive Vehicle Upgrade", + "Progressive Starship Upgrade" + }, + { # Bundle All + "Progressive Weapon/Armor Upgrade" + } +] + lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if data.code} # Map type to expected int type_flaggroups: typing.Dict[str, int] = { "Unit": 0, - "Upgrade": 1, - "Armory 1": 2, - "Armory 2": 3, + "Upgrade": 1, # Weapon / Armor upgrades + "Armory 1": 2, # Unit upgrades + "Armory 2": 3, # Unit upgrades "Building": 4, "Mercenary": 5, "Laboratory": 6, @@ -214,5 +413,9 @@ def get_basic_units(multiworld: MultiWorld, player: int) -> typing.Set[str]: "Minerals": 8, "Vespene": 9, "Supply": 10, - "Goal": 11 + "Goal": 11, + "Armory 3": 12, # Unit upgrades + "Armory 4": 13, # Unit upgrades + "Progressive Upgrade": 14, # Unit upgrades that exist multiple times (Stimpack / Super Stimpack) + "Nothing Group": 15 } diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index e91068c4f220..ae31fa8eaadd 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -1,3 +1,4 @@ +from enum import IntEnum from typing import List, Tuple, Optional, Callable, NamedTuple from BaseClasses import MultiWorld from .Options import get_option_value @@ -11,10 +12,18 @@ class SC2WoLLocation(Location): game: str = "Starcraft2WoL" +class LocationType(IntEnum): + VICTORY = 0 # Winning a mission + MISSION_PROGRESS = 1 # All tasks done for progressing the mission normally towards victory. All cleaning of expansion bases falls here + BONUS = 2 # Bonus objective, getting a campaign or mission bonus in vanilla (credits, research, bonus units or resources) + CHALLENGE = 3 # Challenging objectives, often harder than just completing a mission + OPTIONAL_BOSS = 4 # Any boss that's not required to win the mission. All Brutalisks, Loki, etc. + class LocationData(NamedTuple): region: str name: str code: Optional[int] + type: LocationType rule: Callable = lambda state: True @@ -22,256 +31,473 @@ def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tu # Note: rules which are ended with or True are rules identified as needed later when restricted units is an option logic_level = get_option_value(multiworld, player, 'required_tactics') location_table: List[LocationData] = [ - LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100), - LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101), - LocationData("Liberation Day", "Liberation Day: Second Statue", SC2WOL_LOC_ID_OFFSET + 102), - LocationData("Liberation Day", "Liberation Day: Third Statue", SC2WOL_LOC_ID_OFFSET + 103), - LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104), - LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105), - LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106), - LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200, + LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100, LocationType.VICTORY), + LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101, LocationType.BONUS), + LocationData("Liberation Day", "Liberation Day: Second Statue", SC2WOL_LOC_ID_OFFSET + 102, LocationType.BONUS), + LocationData("Liberation Day", "Liberation Day: Third Statue", SC2WOL_LOC_ID_OFFSET + 103, LocationType.BONUS), + LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104, LocationType.BONUS), + LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105, LocationType.BONUS), + LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106, LocationType.BONUS), + LocationData("Liberation Day", "Liberation Day: Special Delivery", SC2WOL_LOC_ID_OFFSET + 107, LocationType.MISSION_PROGRESS), + LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200, LocationType.VICTORY, + lambda state: state._sc2wol_has_common_unit(multiworld, player)), + LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201, LocationType.BONUS, lambda state: state._sc2wol_has_common_unit(multiworld, player)), - LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201, + LocationData("The Outlaws", "The Outlaws: North Resource Pickups", SC2WOL_LOC_ID_OFFSET + 202, LocationType.BONUS), + LocationData("The Outlaws", "The Outlaws: Bunker", SC2WOL_LOC_ID_OFFSET + 203, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_common_unit(multiworld, player)), - LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300, + LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300, LocationType.VICTORY, lambda state: state._sc2wol_has_common_unit(multiworld, player) and state._sc2wol_defense_rating(multiworld, player, True) >= 2 and (logic_level > 0 or state._sc2wol_has_anti_air(multiworld, player))), - LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301), - LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302, + LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301, LocationType.BONUS), + LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302, LocationType.BONUS, lambda state: state._sc2wol_has_common_unit(multiworld, player)), - LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303, + LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303, LocationType.BONUS, lambda state: state._sc2wol_has_common_unit(multiworld, player) and state._sc2wol_defense_rating(multiworld, player, True) >= 2), - LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, + LocationData("Zero Hour", "Zero Hour: First Hatchery", SC2WOL_LOC_ID_OFFSET + 304, LocationType.CHALLENGE, + lambda state: state._sc2wol_has_competent_comp(multiworld, player)), + LocationData("Zero Hour", "Zero Hour: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 305, LocationType.CHALLENGE, + lambda state: state._sc2wol_has_competent_comp(multiworld, player)), + LocationData("Zero Hour", "Zero Hour: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 306, LocationType.CHALLENGE, + lambda state: state._sc2wol_has_competent_comp(multiworld, player)), + LocationData("Zero Hour", "Zero Hour: Fourth Hatchery", SC2WOL_LOC_ID_OFFSET + 307, LocationType.CHALLENGE, + lambda state: state._sc2wol_has_competent_comp(multiworld, player)), + LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, LocationType.VICTORY, 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 Chysalis", SC2WOL_LOC_ID_OFFSET + 401), - LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402, + LocationData("Evacuation", "Evacuation: First Chrysalis", SC2WOL_LOC_ID_OFFSET + 401, LocationType.BONUS), + LocationData("Evacuation", "Evacuation: Second Chrysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationType.BONUS, lambda state: state._sc2wol_has_common_unit(multiworld, player)), - LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403, + LocationData("Evacuation", "Evacuation: Third Chrysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationType.BONUS, lambda state: state._sc2wol_has_common_unit(multiworld, player)), - LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, + 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), + LocationData("Evacuation", "Evacuation: Flawless", SC2WOL_LOC_ID_OFFSET + 406, LocationType.CHALLENGE, + lambda state: state._sc2wol_has_common_unit(multiworld, player) and + state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and + (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player) + or state._sc2wol_has_competent_anti_air(multiworld, player))), + LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, LocationType.VICTORY, lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 4 and (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))), - LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, + LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, LocationType.BONUS, + lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and + (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))), + LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502, LocationType.BONUS, + lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and + (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))), + LocationData("Outbreak", "Outbreak: North Infested Command Center", SC2WOL_LOC_ID_OFFSET + 503, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))), - LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502, + LocationData("Outbreak", "Outbreak: South Infested Command Center", SC2WOL_LOC_ID_OFFSET + 504, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))), - LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600, + LocationData("Outbreak", "Outbreak: Northwest Bar", SC2WOL_LOC_ID_OFFSET + 505, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and + (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))), + LocationData("Outbreak", "Outbreak: North Bar", SC2WOL_LOC_ID_OFFSET + 506, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and + (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))), + LocationData("Outbreak", "Outbreak: South Bar", SC2WOL_LOC_ID_OFFSET + 507, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and + (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))), + LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600, LocationType.VICTORY, lambda state: state._sc2wol_has_common_unit(multiworld, player) and state._sc2wol_has_competent_anti_air(multiworld, player)), - LocationData("Safe Haven", "Safe Haven: North Nexus", SC2WOL_LOC_ID_OFFSET + 601, + LocationData("Safe Haven", "Safe Haven: North Nexus", SC2WOL_LOC_ID_OFFSET + 601, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_common_unit(multiworld, player) and state._sc2wol_has_competent_anti_air(multiworld, player)), - LocationData("Safe Haven", "Safe Haven: East Nexus", SC2WOL_LOC_ID_OFFSET + 602, + LocationData("Safe Haven", "Safe Haven: East Nexus", SC2WOL_LOC_ID_OFFSET + 602, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_common_unit(multiworld, player) and state._sc2wol_has_competent_anti_air(multiworld, player)), - LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603, + LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_common_unit(multiworld, player) and state._sc2wol_has_competent_anti_air(multiworld, player)), - LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700, + LocationData("Safe Haven", "Safe Haven: First Terror Fleet", SC2WOL_LOC_ID_OFFSET + 604, LocationType.BONUS, + lambda state: state._sc2wol_has_common_unit(multiworld, player) and + state._sc2wol_has_competent_anti_air(multiworld, player)), + LocationData("Safe Haven", "Safe Haven: Second Terror Fleet", SC2WOL_LOC_ID_OFFSET + 605, LocationType.BONUS, + lambda state: state._sc2wol_has_common_unit(multiworld, player) and + state._sc2wol_has_competent_anti_air(multiworld, player)), + LocationData("Safe Haven", "Safe Haven: Third Terror Fleet", SC2WOL_LOC_ID_OFFSET + 606, LocationType.BONUS, + lambda state: state._sc2wol_has_common_unit(multiworld, player) and + state._sc2wol_has_competent_anti_air(multiworld, player)), + LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700, LocationType.VICTORY, lambda state: state._sc2wol_has_common_unit(multiworld, player) and state._sc2wol_has_competent_anti_air(multiworld, player) and state._sc2wol_defense_rating(multiworld, player, True) >= 3), - LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701, + LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_common_unit(multiworld, player) and state._sc2wol_has_competent_anti_air(multiworld, player) and state._sc2wol_defense_rating(multiworld, player, True) >= 3), - LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702, + LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_common_unit(multiworld, player) and state._sc2wol_has_competent_anti_air(multiworld, player) and state._sc2wol_defense_rating(multiworld, player, True) >= 3), - LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703, + LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_common_unit(multiworld, player) and state._sc2wol_has_competent_anti_air(multiworld, player) and state._sc2wol_defense_rating(multiworld, player, True) >= 3), - LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800, + LocationData("Haven's Fall", "Haven's Fall: Northeast Colony Base", SC2WOL_LOC_ID_OFFSET + 704, LocationType.CHALLENGE, + lambda state: state._sc2wol_can_respond_to_colony_infestations), + LocationData("Haven's Fall", "Haven's Fall: East Colony Base", SC2WOL_LOC_ID_OFFSET + 705, LocationType.CHALLENGE, + lambda state: state._sc2wol_can_respond_to_colony_infestations), + LocationData("Haven's Fall", "Haven's Fall: Middle Colony Base", SC2WOL_LOC_ID_OFFSET + 706, LocationType.CHALLENGE, + lambda state: state._sc2wol_can_respond_to_colony_infestations), + LocationData("Haven's Fall", "Haven's Fall: Southeast Colony Base", SC2WOL_LOC_ID_OFFSET + 707, LocationType.CHALLENGE, + lambda state: state._sc2wol_can_respond_to_colony_infestations), + LocationData("Haven's Fall", "Haven's Fall: Southwest Colony Base", SC2WOL_LOC_ID_OFFSET + 708, LocationType.CHALLENGE, + lambda state: state._sc2wol_can_respond_to_colony_infestations), + LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800, LocationType.VICTORY, 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("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801), - LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802), - LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803, + LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801, LocationType.BONUS), + LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802, LocationType.BONUS), + LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803, LocationType.BONUS, 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("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804, + LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804, LocationType.BONUS, 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("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900, + LocationData("Smash and Grab", "Smash and Grab: First Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 805, LocationType.MISSION_PROGRESS, + 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("Smash and Grab", "Smash and Grab: Second Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 806, LocationType.MISSION_PROGRESS, + 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("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900, LocationType.VICTORY, lambda state: state._sc2wol_has_anti_air(multiworld, player) and state._sc2wol_defense_rating(multiworld, player, False) >= 7), - LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901, + LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901, LocationType.BONUS, lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5), - LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902, + LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902, LocationType.BONUS, lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5), - LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, + LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, LocationType.BONUS, lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5), - LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, + LocationData("The Dig", "The Dig: Moebius Base", SC2WOL_LOC_ID_OFFSET + 904, LocationType.MISSION_PROGRESS), + LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, LocationType.VICTORY, lambda state: state._sc2wol_has_anti_air(multiworld, player) and (state._sc2wol_has_air(multiworld, player) or state.has_any({'Medivac', 'Hercules'}, player) and state._sc2wol_has_common_unit(multiworld, player))), - LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, + LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core", SC2WOL_LOC_ID_OFFSET + 1001, LocationType.MISSION_PROGRESS), + LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, LocationType.MISSION_PROGRESS, + lambda state: (state._sc2wol_has_air(multiworld, player) + or state.has_any({'Medivac', 'Hercules'}, player) + and state._sc2wol_has_common_unit(multiworld, player))), + LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, LocationType.BONUS, lambda state: state._sc2wol_able_to_rescue(multiworld, player)), - LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, + LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, LocationType.BONUS, lambda state: state._sc2wol_able_to_rescue(multiworld, player)), - LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005, + LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005, LocationType.BONUS, lambda state: state._sc2wol_able_to_rescue(multiworld, player)), - LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006, + LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006, LocationType.BONUS, lambda state: state._sc2wol_able_to_rescue(multiworld, player)), - LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, + LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, LocationType.BONUS, lambda state: state._sc2wol_able_to_rescue(multiworld, player)), - LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, + LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, LocationType.OPTIONAL_BOSS, lambda state: state._sc2wol_has_anti_air(multiworld, player) and (state._sc2wol_has_air(multiworld, player) or state.has_any({'Medivac', 'Hercules'}, player) and state._sc2wol_has_common_unit(multiworld, player))), - LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, + LocationData("The Moebius Factor", "The Moebius Factor: 3rd Data Core", SC2WOL_LOC_ID_OFFSET + 1009, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_anti_air(multiworld, player) and + (state._sc2wol_has_air(multiworld, player) + or state.has_any({'Medivac', 'Hercules'}, player) + and state._sc2wol_has_common_unit(multiworld, player))), + LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, LocationType.VICTORY, + lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)), + LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101, LocationType.BONUS), + LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102, LocationType.BONUS), + LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103, LocationType.BONUS, + lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)), + LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, LocationType.BONUS, lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)), - LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101), - LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102), - LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103, + LocationData("Supernova", "Supernova: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1105, LocationType.MISSION_PROGRESS), + LocationData("Supernova", "Supernova: Middle Base", SC2WOL_LOC_ID_OFFSET + 1106, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)), - LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, + LocationData("Supernova", "Supernova: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1107, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)), - LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200, + LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200, LocationType.VICTORY, lambda state: state._sc2wol_survives_rip_field(multiworld, player)), - LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201), - LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202, + LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201, LocationType.MISSION_PROGRESS), + LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202, LocationType.BONUS, lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)), - LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, + LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, LocationType.BONUS, lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)), - LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, + LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, LocationType.BONUS, lambda state: state._sc2wol_survives_rip_field(multiworld, player)), - LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, + LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, LocationType.BONUS, lambda state: state._sc2wol_survives_rip_field(multiworld, player)), - LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, + LocationData("Maw of the Void", "Maw of the Void: Mothership", SC2WOL_LOC_ID_OFFSET + 1206, LocationType.OPTIONAL_BOSS, + lambda state: state._sc2wol_survives_rip_field(multiworld, player)), + LocationData("Maw of the Void", "Maw of the Void: Expansion Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1207, LocationType.MISSION_PROGRESS, + lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)), + LocationData("Maw of the Void", "Maw of the Void: Middle Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1208, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_survives_rip_field(multiworld, player)), + LocationData("Maw of the Void", "Maw of the Void: Southeast Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1209, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_survives_rip_field(multiworld, player)), + LocationData("Maw of the Void", "Maw of the Void: Stargate Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1210, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_survives_rip_field(multiworld, player)), + LocationData("Maw of the Void", "Maw of the Void: Northwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1211, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_survives_rip_field(multiworld, player)), + LocationData("Maw of the Void", "Maw of the Void: West Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1212, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_survives_rip_field(multiworld, player)), + LocationData("Maw of the Void", "Maw of the Void: Southwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1213, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_survives_rip_field(multiworld, player)), + LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, LocationType.VICTORY, lambda state: logic_level > 0 or state._sc2wol_has_anti_air(multiworld, player) and ( state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))), - LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301), - LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302, + LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301, LocationType.BONUS), + LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302, LocationType.OPTIONAL_BOSS, lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400, - lambda state: state._sc2wol_has_common_unit(multiworld, player) and - state._sc2wol_has_competent_anti_air(multiworld, player)), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402, - lambda state: state._sc2wol_has_common_unit(multiworld, player) and - state._sc2wol_has_competent_anti_air(multiworld, player)), - LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403, - lambda state: state._sc2wol_has_common_unit(multiworld, player) and - state._sc2wol_has_competent_anti_air(multiworld, player)), - LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500), - LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501), - LocationData("Breakout", "Breakout: Siegetank Prison", SC2WOL_LOC_ID_OFFSET + 1502), - LocationData("Ghost of a Chance", "Ghost of a Chance: Victory", SC2WOL_LOC_ID_OFFSET + 1600), - LocationData("Ghost of a Chance", "Ghost of a Chance: Terrazine Tank", SC2WOL_LOC_ID_OFFSET + 1601), - LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602), - LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603), - LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604), - LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605), - LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700, + LocationData("Devil's Playground", "Devil's Playground: North Reapers", SC2WOL_LOC_ID_OFFSET + 1303, LocationType.BONUS), + LocationData("Devil's Playground", "Devil's Playground: Middle Reapers", SC2WOL_LOC_ID_OFFSET + 1304, LocationType.BONUS, + lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)), + LocationData("Devil's Playground", "Devil's Playground: Southwest Reapers", SC2WOL_LOC_ID_OFFSET + 1305, LocationType.BONUS, + lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)), + LocationData("Devil's Playground", "Devil's Playground: Southeast Reapers", SC2WOL_LOC_ID_OFFSET + 1306, LocationType.BONUS, + lambda state: logic_level > 0 or + state._sc2wol_has_anti_air(multiworld, player) and ( + state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))), + LocationData("Devil's Playground", "Devil's Playground: East Reapers", SC2WOL_LOC_ID_OFFSET + 1307, LocationType.BONUS, + lambda state: state._sc2wol_has_anti_air(multiworld, player) and + (logic_level > 0 or + state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400, LocationType.VICTORY, + lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401, LocationType.BONUS), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402, LocationType.BONUS, + lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403, LocationType.BONUS, + lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: Middle Base", SC2WOL_LOC_ID_OFFSET + 1404, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: Main Base", SC2WOL_LOC_ID_OFFSET + 1405, LocationType.CHALLENGE, + lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player) + and state._sc2wol_beats_protoss_deathball(multiworld, player)), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: No Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1406, LocationType.CHALLENGE, + lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player) + and state._sc2wol_has_competent_ground_to_air(multiworld, player) + and state._sc2wol_beats_protoss_deathball(multiworld, player)), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 1 Terrazine Node Sealed", SC2WOL_LOC_ID_OFFSET + 1407, LocationType.CHALLENGE, + lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player) + and state._sc2wol_has_competent_ground_to_air(multiworld, player) + and state._sc2wol_beats_protoss_deathball(multiworld, player)), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 2 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1408, LocationType.CHALLENGE, + lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player) + and state._sc2wol_beats_protoss_deathball(multiworld, player)), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 3 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1409, LocationType.CHALLENGE, + lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player) + and state._sc2wol_has_competent_comp(multiworld, player)), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 4 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1410, LocationType.CHALLENGE, + lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)), + LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 5 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1411, LocationType.CHALLENGE, + lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)), + LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500, LocationType.VICTORY), + LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501, LocationType.BONUS), + LocationData("Breakout", "Breakout: Siege Tank Prison", SC2WOL_LOC_ID_OFFSET + 1502, LocationType.BONUS), + LocationData("Breakout", "Breakout: First Checkpoint", SC2WOL_LOC_ID_OFFSET + 1503, LocationType.MISSION_PROGRESS), + LocationData("Breakout", "Breakout: Second Checkpoint", SC2WOL_LOC_ID_OFFSET + 1504, LocationType.MISSION_PROGRESS), + LocationData("Ghost of a Chance", "Ghost of a Chance: Victory", SC2WOL_LOC_ID_OFFSET + 1600, LocationType.VICTORY), + LocationData("Ghost of a Chance", "Ghost of a Chance: Terrazine Tank", SC2WOL_LOC_ID_OFFSET + 1601, LocationType.MISSION_PROGRESS), + LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602, LocationType.MISSION_PROGRESS), + LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603, LocationType.BONUS), + LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604, LocationType.BONUS), + LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605, LocationType.BONUS), + LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700, LocationType.VICTORY, lambda state: state._sc2wol_has_train_killers(multiworld, player) and state._sc2wol_has_anti_air(multiworld, player)), - LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701), - LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702), - LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703), - LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800, + LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701, LocationType.BONUS), + LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702, LocationType.BONUS), + LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703, LocationType.BONUS), + LocationData("The Great Train Robbery", "The Great Train Robbery: Close Diamondback", SC2WOL_LOC_ID_OFFSET + 1704, LocationType.BONUS), + LocationData("The Great Train Robbery", "The Great Train Robbery: Northwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1705, LocationType.BONUS), + LocationData("The Great Train Robbery", "The Great Train Robbery: North Diamondback", SC2WOL_LOC_ID_OFFSET + 1706, LocationType.BONUS), + LocationData("The Great Train Robbery", "The Great Train Robbery: Northeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1707, LocationType.BONUS), + LocationData("The Great Train Robbery", "The Great Train Robbery: Southwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1708, LocationType.BONUS), + LocationData("The Great Train Robbery", "The Great Train Robbery: Southeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1709, LocationType.BONUS), + LocationData("The Great Train Robbery", "The Great Train Robbery: Kill Team", SC2WOL_LOC_ID_OFFSET + 1710, LocationType.CHALLENGE, + lambda state: (logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player)) and + state._sc2wol_has_train_killers(multiworld, player) and + state._sc2wol_has_anti_air(multiworld, player)), + LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800, LocationType.VICTORY, lambda state: state._sc2wol_has_common_unit(multiworld, player) and (logic_level > 0 or state._sc2wol_has_anti_air)), - LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801, + LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_common_unit(multiworld, player)), + LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802, LocationType.BONUS, + lambda state: state._sc2wol_has_common_unit(multiworld, player)), + LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803, LocationType.BONUS), + LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804, LocationType.BONUS, + lambda state: state._sc2wol_has_common_unit(multiworld, player)), + LocationData("Cutthroat", "Cutthroat: North Command Center", SC2WOL_LOC_ID_OFFSET + 1805, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_common_unit(multiworld, player)), - LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802, + LocationData("Cutthroat", "Cutthroat: South Command Center", SC2WOL_LOC_ID_OFFSET + 1806, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_common_unit(multiworld, player)), - LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803), - LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804, + LocationData("Cutthroat", "Cutthroat: West Command Center", SC2WOL_LOC_ID_OFFSET + 1807, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_common_unit(multiworld, player)), - LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900, + LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900, LocationType.VICTORY, lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)), - LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901), - LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, + LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901, LocationType.MISSION_PROGRESS), + LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, LocationType.OPTIONAL_BOSS, lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)), - LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903), - LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904, + LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903, LocationType.BONUS), + LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904, LocationType.BONUS, lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)), - LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, + LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, LocationType.BONUS, lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)), - LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000, + LocationData("Engine of Destruction", "Engine of Destruction: West Base", SC2WOL_LOC_ID_OFFSET + 1906, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and + state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)), + LocationData("Engine of Destruction", "Engine of Destruction: Northwest Base", SC2WOL_LOC_ID_OFFSET + 1907, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and + state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)), + LocationData("Engine of Destruction", "Engine of Destruction: Northeast Base", SC2WOL_LOC_ID_OFFSET + 1908, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and + state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)), + LocationData("Engine of Destruction", "Engine of Destruction: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1909, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and + state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)), + LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000, LocationType.VICTORY, + lambda state: state._sc2wol_has_competent_comp(multiworld, player)), + LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_competent_comp(multiworld, player)), - LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001, + LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_competent_comp(multiworld, player)), - LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002, + LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_competent_comp(multiworld, player)), - LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003, + LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004, LocationType.BONUS), + LocationData("Media Blitz", "Media Blitz: All Barracks", SC2WOL_LOC_ID_OFFSET + 2005, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_competent_comp(multiworld, player)), - LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004), - LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100, + LocationData("Media Blitz", "Media Blitz: All Factories", SC2WOL_LOC_ID_OFFSET + 2006, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_competent_comp(multiworld, player)), + LocationData("Media Blitz", "Media Blitz: All Starports", SC2WOL_LOC_ID_OFFSET + 2007, LocationType.MISSION_PROGRESS, + lambda state: logic_level > 0 or state._sc2wol_has_competent_comp(multiworld, player)), + LocationData("Media Blitz", "Media Blitz: Odin Not Trashed", SC2WOL_LOC_ID_OFFSET + 2008, LocationType.CHALLENGE, + lambda state: state._sc2wol_has_competent_comp(multiworld, player)), + LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100, LocationType.VICTORY, + lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)), + LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101, LocationType.BONUS), + LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102, LocationType.BONUS, lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)), - LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101), - LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102, + LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103,LocationType.BONUS, lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)), - LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103, + LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104, LocationType.BONUS, lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)), - LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104, + LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk", SC2WOL_LOC_ID_OFFSET + 2105, LocationType.OPTIONAL_BOSS, lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)), - LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk", SC2WOL_LOC_ID_OFFSET + 2105, + LocationData("Piercing the Shroud", "Piercing the Shroud: Fusion Reactor", SC2WOL_LOC_ID_OFFSET + 2106, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)), - LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200), - LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201), - LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202), - LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203), - LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300, + LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200, LocationType.VICTORY), + LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201, LocationType.BONUS), + LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202, LocationType.BONUS), + LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203, LocationType.BONUS), + LocationData("Whispers of Doom", "Whispers of Doom: First Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2204, LocationType.MISSION_PROGRESS), + LocationData("Whispers of Doom", "Whispers of Doom: Second Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2205, LocationType.MISSION_PROGRESS), + LocationData("Whispers of Doom", "Whispers of Doom: Third Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2206, LocationType.MISSION_PROGRESS), + LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300, LocationType.VICTORY, lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)), - LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301, + LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301, LocationType.BONUS, lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)), - LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302, + LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302, LocationType.BONUS, lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)), - LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303, - lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)), - LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400, + LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303, LocationType.BONUS, + 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, + 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)), - LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401), - LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402, + LocationData("A Sinister Turn", "A Sinister Turn: Northwest Preserver", SC2WOL_LOC_ID_OFFSET + 2307, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)), + LocationData("A Sinister Turn", "A Sinister Turn: Southwest Preserver", SC2WOL_LOC_ID_OFFSET + 2308, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)), + LocationData("A Sinister Turn", "A Sinister Turn: East Preserver", SC2WOL_LOC_ID_OFFSET + 2309, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)), + LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400, LocationType.VICTORY, + lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(multiworld, player)), + LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401, LocationType.BONUS), + LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402, LocationType.BONUS, + lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)), + LocationData("Echoes of the Future", "Echoes of the Future: Base", SC2WOL_LOC_ID_OFFSET + 2403, LocationType.MISSION_PROGRESS), + LocationData("Echoes of the Future", "Echoes of the Future: Southwest Tendril", SC2WOL_LOC_ID_OFFSET + 2404, LocationType.MISSION_PROGRESS), + LocationData("Echoes of the Future", "Echoes of the Future: Southeast Tendril", SC2WOL_LOC_ID_OFFSET + 2405, LocationType.MISSION_PROGRESS, + lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)), + LocationData("Echoes of the Future", "Echoes of the Future: Northeast Tendril", SC2WOL_LOC_ID_OFFSET + 2406, LocationType.MISSION_PROGRESS, lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)), - LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500), - LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501, + LocationData("Echoes of the Future", "Echoes of the Future: Northwest Tendril", SC2WOL_LOC_ID_OFFSET + 2407, LocationType.MISSION_PROGRESS, + lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)), + LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500, LocationType.VICTORY), + LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501, LocationType.BONUS, lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)), - LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502, + LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502, LocationType.CHALLENGE, + lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)), + LocationData("In Utter Darkness", "In Utter Darkness: Urun", SC2WOL_LOC_ID_OFFSET + 2503, LocationType.MISSION_PROGRESS), + LocationData("In Utter Darkness", "In Utter Darkness: Mohandar", SC2WOL_LOC_ID_OFFSET + 2504, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)), + LocationData("In Utter Darkness", "In Utter Darkness: Selendis", SC2WOL_LOC_ID_OFFSET + 2505, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)), - LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, + LocationData("In Utter Darkness", "In Utter Darkness: Artanis", SC2WOL_LOC_ID_OFFSET + 2506, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)), + LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, LocationType.VICTORY, + lambda state: state._sc2wol_has_competent_comp(multiworld, player) and + state._sc2wol_defense_rating(multiworld, player, True) > 6), + LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_competent_comp(multiworld, player) and + state._sc2wol_defense_rating(multiworld, player, True) > 6), + LocationData("Gates of Hell", "Gates of Hell: 2 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2602, LocationType.BONUS, + lambda state: state._sc2wol_has_competent_comp(multiworld, player) and + state._sc2wol_defense_rating(multiworld, player, True) > 6), + LocationData("Gates of Hell", "Gates of Hell: 4 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2603, LocationType.BONUS, + lambda state: state._sc2wol_has_competent_comp(multiworld, player) and + state._sc2wol_defense_rating(multiworld, player, True) > 6), + LocationData("Gates of Hell", "Gates of Hell: 6 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2604, LocationType.BONUS, lambda state: state._sc2wol_has_competent_comp(multiworld, player) and state._sc2wol_defense_rating(multiworld, player, True) > 6), - LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, + LocationData("Gates of Hell", "Gates of Hell: 8 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2605, LocationType.BONUS, lambda state: state._sc2wol_has_competent_comp(multiworld, player) and state._sc2wol_defense_rating(multiworld, player, True) > 6), - LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700), - LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701), - LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702), - LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703), - LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800, + LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700, LocationType.VICTORY), + LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701, LocationType.MISSION_PROGRESS), + LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702, LocationType.MISSION_PROGRESS), + LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703, LocationType.MISSION_PROGRESS), + LocationData("Belly of the Beast", "Belly of the Beast: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 2704, LocationType.BONUS), + LocationData("Belly of the Beast", "Belly of the Beast: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 2705, LocationType.BONUS), + LocationData("Belly of the Beast", "Belly of the Beast: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 2706, LocationType.BONUS), + LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800, LocationType.VICTORY, + lambda state: state._sc2wol_has_competent_comp(multiworld, player)), + LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_competent_comp(multiworld, player)), + LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802, LocationType.MISSION_PROGRESS, + lambda state: state._sc2wol_has_competent_comp(multiworld, player)), + LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_competent_comp(multiworld, player)), - LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801, + LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_competent_comp(multiworld, player)), - LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802, + LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, LocationType.OPTIONAL_BOSS, lambda state: state._sc2wol_has_competent_comp(multiworld, player)), - LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803, + LocationData("Shatter the Sky", "Shatter the Sky: East Hatchery", SC2WOL_LOC_ID_OFFSET + 2806, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_competent_comp(multiworld, player)), - LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804, + LocationData("Shatter the Sky", "Shatter the Sky: North Hatchery", SC2WOL_LOC_ID_OFFSET + 2807, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_competent_comp(multiworld, player)), - LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, + LocationData("Shatter the Sky", "Shatter the Sky: Mid Hatchery", SC2WOL_LOC_ID_OFFSET + 2808, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_competent_comp(multiworld, player)), - LocationData("All-In", "All-In: Victory", None, + LocationData("All-In", "All-In: Victory", None, LocationType.VICTORY, lambda state: state._sc2wol_final_mission_requirements(multiworld, player)) ] diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index 8c7182e0e877..112302beb207 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -9,22 +9,38 @@ def _sc2wol_has_common_unit(self, multiworld: MultiWorld, player: int) -> bool: return self.has_any(get_basic_units(multiworld, player), player) def _sc2wol_has_air(self, multiworld: MultiWorld, player: int) -> bool: - return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0 \ + return self.has_any({'Viking', 'Wraith', 'Banshee', 'Battlecruiser'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0 \ and self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(multiworld, player) def _sc2wol_has_air_anti_air(self, multiworld: MultiWorld, player: int) -> bool: return self.has('Viking', player) \ - or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Wraith', player) + or self.has_all({'Wraith', 'Advanced Laser Technology (Wraith)'}, player) \ + or self.has_all({'Battlecruiser', 'ATX Laser Battery (Battlecruiser)'}, player) \ + or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Wraith', 'Valkyrie', 'Battlecruiser'}, player) - def _sc2wol_has_competent_anti_air(self, multiworld: MultiWorld, player: int) -> bool: + def _sc2wol_has_competent_ground_to_air(self, multiworld: MultiWorld, player: int) -> bool: return self.has('Goliath', player) \ or self.has('Marine', player) and self.has_any({'Medic', 'Medivac'}, player) \ + or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Cyclone', player) + + def _sc2wol_has_competent_anti_air(self, multiworld: MultiWorld, player: int) -> bool: + return self._sc2wol_has_competent_ground_to_air(multiworld, player) \ or self._sc2wol_has_air_anti_air(multiworld, player) + def _sc2wol_welcome_to_the_jungle_requirement(self, multiworld: MultiWorld, player: int) -> bool: + return ( + self._sc2wol_has_common_unit(multiworld, player) + and self._sc2wol_has_competent_ground_to_air(multiworld, player) + ) or ( + get_option_value(multiworld, player, 'required_tactics') > 0 + and self.has_any({'Marine', 'Vulture'}, player) + and self._sc2wol_has_air_anti_air(multiworld, player) + ) + def _sc2wol_has_anti_air(self, multiworld: MultiWorld, player: int) -> bool: - return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Marine', 'Wraith'}, player) \ + return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Marine', 'Wraith', 'Valkyrie', 'Cyclone'}, player) \ or self._sc2wol_has_competent_anti_air(multiworld, player) \ - or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player) + or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre', 'Widow Mine', 'Liberator'}, player) def _sc2wol_defense_rating(self, multiworld: MultiWorld, player: int, zerg_enemy: bool, air_enemy: bool = True) -> bool: defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player))) @@ -32,6 +48,10 @@ def _sc2wol_defense_rating(self, multiworld: MultiWorld, player: int, zerg_enemy defense_score += 3 if self.has_all({'Siege Tank', 'Maelstrom Rounds (Siege Tank)'}, player): defense_score += 2 + if self.has_all({'Siege Tank', 'Graduating Range (Siege Tank)'}, player): + defense_score += 1 + if self.has_all({'Widow Mine', 'Concealment (Widow Mine)'}, player): + defense_score += 1 if zerg_enemy: defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player))) if self.has('Firebat', player) and self.has('Bunker', player): @@ -44,20 +64,27 @@ def _sc2wol_defense_rating(self, multiworld: MultiWorld, player: int, zerg_enemy return defense_score def _sc2wol_has_competent_comp(self, multiworld: MultiWorld, player: int) -> bool: - return (self.has('Marine', player) or self.has('Marauder', player) and - self._sc2wol_has_competent_anti_air(multiworld, player)) and self.has_any({'Medivac', 'Medic'}, player) or \ - self.has('Thor', player) or self.has("Banshee", player) and self._sc2wol_has_competent_anti_air(multiworld, player) or \ - self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(multiworld, player) or \ - self.has('Siege Tank', player) and self._sc2wol_has_competent_anti_air(multiworld, player) + return \ + ( + ( + self.has_any({'Marine', 'Marauder'}, player) and self.has_any({'Medivac', 'Medic'}, player) + or self.has_any({'Thor', 'Banshee', 'Siege Tank'}, player) + or self.has_all({'Liberator', 'Raid Artillery (Liberator)'}, player) + ) and self._sc2wol_has_competent_anti_air(multiworld, player) + ) \ + or \ + ( + self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(multiworld, player) + ) def _sc2wol_has_train_killers(self, multiworld: MultiWorld, player: int) -> bool: return ( - self.has_any({'Siege Tank', 'Diamondback', 'Marauder'}, player) + self.has_any({'Siege Tank', 'Diamondback', 'Marauder', 'Cyclone'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0 and ( self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player) - or self.has('Vulture', player) + or self.has_any({'Vulture', 'Liberator'}, player) ) ) @@ -66,16 +93,18 @@ def _sc2wol_able_to_rescue(self, multiworld: MultiWorld, player: int) -> bool: def _sc2wol_has_protoss_common_units(self, multiworld: MultiWorld, player: int) -> bool: return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) \ - or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player) + or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('High Templar', player) def _sc2wol_has_protoss_medium_units(self, multiworld: MultiWorld, player: int) -> bool: return self._sc2wol_has_protoss_common_units(multiworld, player) and \ - self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player) \ - or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player) + self.has_any({'Stalker', 'Void Ray', 'Carrier'}, player) \ + or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Dark Templar', player) def _sc2wol_beats_protoss_deathball(self, multiworld: MultiWorld, player: int) -> bool: - return self.has_any({'Banshee', 'Battlecruiser'}, player) and self._sc2wol_has_competent_anti_air(multiworld, player) or \ - self._sc2wol_has_competent_comp(multiworld, player) and self._sc2wol_has_air_anti_air(multiworld, player) + return (self.has_any({'Banshee', 'Battlecruiser'}, player) or + self.has_all({'Liberator', 'Raid Artillery (Liberator)'}, player)) \ + and self._sc2wol_has_competent_anti_air(multiworld, player) or \ + self._sc2wol_has_competent_comp(multiworld, player) and self._sc2wol_has_air_anti_air(multiworld, player) def _sc2wol_has_mm_upgrade(self, multiworld: MultiWorld, player: int) -> bool: return self.has_any({"Combat Shield (Marine)", "Stabilizer Medpacks (Medic)"}, player) @@ -89,6 +118,17 @@ def _sc2wol_survives_rip_field(self, multiworld: MultiWorld, player: int) -> boo def _sc2wol_has_nukes(self, multiworld: MultiWorld, player: int) -> bool: return get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player) + def _sc2wol_can_respond_to_colony_infestations(self, multiworld: MultiWorld, player: int) -> bool: + return self._sc2wol_has_common_unit(multiworld, player) \ + and self._sc2wol_has_competent_anti_air(multiworld, player) \ + and \ + ( + self._sc2wol_has_air_anti_air(multiworld, player) or + self.has_any({'Battlecruiser', 'Valkyrie'}), player + ) \ + and \ + self._sc2wol_defense_rating(multiworld, player, True) >= 3 + def _sc2wol_final_mission_requirements(self, multiworld: MultiWorld, player: int): beats_kerrigan = self.has_any({'Marine', 'Banshee', 'Ghost'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0 if get_option_value(multiworld, player, 'all_in_map') == 0: @@ -101,7 +141,7 @@ def _sc2wol_final_mission_requirements(self, multiworld: MultiWorld, player: int # Air defense_rating = self._sc2wol_defense_rating(multiworld, player, True, True) return defense_rating >= 8 and beats_kerrigan \ - and self.has_any({'Viking', 'Battlecruiser'}, player) \ + and self.has_any({'Viking', 'Battlecruiser', 'Valkyrie'}, player) \ and self.has_any({'Hive Mind Emulator', 'Psi Disruptor', 'Missile Turret'}, player) def _sc2wol_cleared_missions(self, multiworld: MultiWorld, player: int, mission_count: int) -> bool: diff --git a/worlds/sc2wol/MissionTables.py b/worlds/sc2wol/MissionTables.py index 6db93547689b..298cd7a978a6 100644 --- a/worlds/sc2wol/MissionTables.py +++ b/worlds/sc2wol/MissionTables.py @@ -49,8 +49,8 @@ class FillMission(NamedTuple): FillMission(MissionPools.EASY, [2], "Artifact", completion_critical=True), FillMission(MissionPools.MEDIUM, [7], "Artifact", number=8, completion_critical=True), FillMission(MissionPools.HARD, [8], "Artifact", number=11, completion_critical=True), - FillMission(MissionPools.HARD, [9], "Artifact", number=14, completion_critical=True), - FillMission(MissionPools.HARD, [10], "Artifact", completion_critical=True), + FillMission(MissionPools.HARD, [9], "Artifact", number=14, completion_critical=True, removal_priority=11), + FillMission(MissionPools.HARD, [10], "Artifact", completion_critical=True, removal_priority=10), FillMission(MissionPools.MEDIUM, [2], "Covert", number=4), FillMission(MissionPools.MEDIUM, [12], "Covert"), FillMission(MissionPools.HARD, [13], "Covert", number=8, removal_priority=3), @@ -58,7 +58,7 @@ class FillMission(NamedTuple): FillMission(MissionPools.MEDIUM, [2], "Rebellion", number=6), FillMission(MissionPools.HARD, [16], "Rebellion"), FillMission(MissionPools.HARD, [17], "Rebellion"), - FillMission(MissionPools.HARD, [18], "Rebellion"), + FillMission(MissionPools.HARD, [18], "Rebellion", removal_priority=12), FillMission(MissionPools.HARD, [19], "Rebellion", removal_priority=5), FillMission(MissionPools.MEDIUM, [8], "Prophecy", removal_priority=9), FillMission(MissionPools.HARD, [21], "Prophecy", removal_priority=8), @@ -98,6 +98,13 @@ class FillMission(NamedTuple): FillMission(MissionPools.FINAL, [5], "Final", completion_critical=True) ] +mini_gauntlet_order = [ + FillMission(MissionPools.STARTER, [-1], "I", completion_critical=True), + FillMission(MissionPools.EASY, [0], "II", completion_critical=True), + FillMission(MissionPools.MEDIUM, [1], "III", completion_critical=True), + FillMission(MissionPools.FINAL, [2], "Final", completion_critical=True) +] + grid_order = [ FillMission(MissionPools.STARTER, [-1], "_1"), FillMission(MissionPools.EASY, [0], "_1"), @@ -129,6 +136,13 @@ class FillMission(NamedTuple): FillMission(MissionPools.FINAL, [5, 7], "_3", or_requirements=True) ] +tiny_grid_order = [ + FillMission(MissionPools.STARTER, [-1], "_1"), + FillMission(MissionPools.MEDIUM, [0], "_1"), + FillMission(MissionPools.EASY, [0], "_2"), + FillMission(MissionPools.FINAL, [1, 2], "_2", or_requirements=True), +] + blitz_order = [ FillMission(MissionPools.STARTER, [-1], "I"), FillMission(MissionPools.EASY, [-1], "I"), @@ -144,7 +158,17 @@ class FillMission(NamedTuple): FillMission(MissionPools.FINAL, [0, 1], "Final", number=5, or_requirements=True) ] -mission_orders = [vanilla_shuffle_order, vanilla_shuffle_order, mini_campaign_order, grid_order, mini_grid_order, blitz_order, gauntlet_order] +mission_orders = [ + vanilla_shuffle_order, + vanilla_shuffle_order, + mini_campaign_order, + grid_order, + mini_grid_order, + blitz_order, + gauntlet_order, + mini_gauntlet_order, + tiny_grid_order +] vanilla_mission_req_table = { @@ -190,7 +214,7 @@ class FillMission(NamedTuple): "Whispers of Doom": "Whispers of Doom: Victory", "Belly of the Beast": "Belly of the Beast: Victory", "Zero Hour": "Zero Hour: First Group Rescued", - "Evacuation": "Evacuation: First Chysalis", + "Evacuation": "Evacuation: Reach Hanson", "Devil's Playground": "Devil's Playground: Tosh's Miners", "Smash and Grab": "Smash and Grab: First Relic", "The Great Train Robbery": "The Great Train Robbery: North Defiler" diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py index 4f2032d662e8..0702e431a4de 100644 --- a/worlds/sc2wol/Options.py +++ b/worlds/sc2wol/Options.py @@ -3,31 +3,49 @@ from Options import Choice, Option, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range from .MissionTables import vanilla_mission_req_table +ORDER_VANILLA = 0 +ORDER_VANILLA_SHUFFLED = 1 class GameDifficulty(Choice): - """The difficulty of the campaign, affects enemy AI, starting units, and game speed.""" + """ + The difficulty of the campaign, affects enemy AI, starting units, and game speed. + + For those unfamiliar with the Archipelago randomizer, the recommended settings are one difficulty level + lower than the vanilla game + """ display_name = "Game Difficulty" option_casual = 0 option_normal = 1 option_hard = 2 option_brutal = 3 - - -class UpgradeBonus(Choice): - """Determines what lab upgrade to use, whether it is Ultra-Capacitors which boost attack speed with every weapon - upgrade or Vanadium Plating which boosts life with every armor upgrade.""" - display_name = "Upgrade Bonus" - option_ultra_capacitors = 0 - option_vanadium_plating = 1 - - -class BunkerUpgrade(Choice): - """Determines what bunker lab upgrade to use, whether it is Shrike Turret which outfits bunkers with an automated - turret or Fortified Bunker which boosts the life of bunkers.""" - display_name = "Bunker Upgrade" - option_shrike_turret = 0 - option_fortified_bunker = 1 - + default = 1 + +class GameSpeed(Choice): + """Optional setting to override difficulty-based game speed.""" + display_name = "Game Speed" + option_default = 0 + option_slower = 1 + option_slow = 2 + option_normal = 3 + option_fast = 4 + option_faster = 5 + default = option_default + +class FinalMap(Choice): + """ + Determines if the final map and goal of the campaign. + All in: You need to beat All-in map + Random Hard: A random hard mission is selected as a goal. + Beat this mission in order to complete the game. + All-in map won't be in the campaign + + Vanilla mission order always ends with All in mission! + + This option is short-lived. It may be changed in the future + """ + display_name = "Final Map" + option_all_in = 0 + option_random_hard = 1 class AllInMap(Choice): """Determines what version of All-In (final map) that will be generated for the campaign.""" @@ -37,14 +55,18 @@ class AllInMap(Choice): class MissionOrder(Choice): - """Determines the order the missions are played in. The last three mission orders end in a random mission. + """ + Determines the order the missions are played in. The last three mission orders end in a random mission. Vanilla (29): Keeps the standard mission order and branching from the WoL Campaign. Vanilla Shuffled (29): Keeps same branching paths from the WoL Campaign but randomizes the order of missions within. Mini Campaign (15): Shorter version of the campaign with randomized missions and optional branches. - Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards All-In. + Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards bottom-right mission to win. Mini Grid (9): A 3x3 version of Grid. Complete the bottom-right mission to win. Blitz (12): 12 random missions that open up very quickly. Complete the bottom-right mission to win. - Gauntlet (7): Linear series of 7 random missions to complete the campaign.""" + Gauntlet (7): Linear series of 7 random missions to complete the campaign. + Mini Gauntlet (4): Linear series of 4 random missions to complete the campaign. + Tiny Grid (4): A 2x2 version of Grid. Complete the bottom-right mission to win. + """ display_name = "Mission Order" option_vanilla = 0 option_vanilla_shuffled = 1 @@ -53,27 +75,53 @@ class MissionOrder(Choice): option_mini_grid = 4 option_blitz = 5 option_gauntlet = 6 + option_mini_gauntlet = 7 + option_tiny_grid = 8 + + +class PlayerColor(Choice): + """Determines in-game team color.""" + display_name = "Player Color" + option_white = 0 + option_red = 1 + option_blue = 2 + option_teal = 3 + option_purple = 4 + option_yellow = 5 + option_orange = 6 + option_green = 7 + option_light_pink = 8 + option_violet = 9 + option_light_grey = 10 + option_dark_green = 11 + option_brown = 12 + option_light_green = 13 + option_dark_grey = 14 + option_pink = 15 + option_rainbow = 16 + option_default = 17 + default = option_default class ShuffleProtoss(DefaultOnToggle): """Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled. - If turned off with Vanilla Shuffled, the 3 protoss missions will be in their normal position on the Prophecy chain - if not shuffled. - If turned off with reduced mission settings, the 3 protoss missions will not appear and Protoss units are removed - from the pool.""" + If turned off, the 3 protoss missions will not appear and Protoss units are removed from the pool.""" display_name = "Shuffle Protoss Missions" class ShuffleNoBuild(DefaultOnToggle): """Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled. - If turned off with Vanilla Shuffled, one no-build mission will be placed as the first mission and the rest will be - placed at the end of optional routes. - If turned off with reduced mission settings, the 5 no-build missions will not appear.""" + If turned off, the 5 no-build missions will not appear.""" display_name = "Shuffle No-Build Missions" class EarlyUnit(DefaultOnToggle): - """Guarantees that the first mission will contain a unit.""" + """ + Guarantees that the first mission will contain a unit. + + Each mission available to be the first mission has a pre-defined location where the unit should spawn. + This location gets overriden over any exclusion. It's guaranteed to be reachable with an empty inventory. + """ display_name = "Early Unit" @@ -91,11 +139,97 @@ class RequiredTactics(Choice): class UnitsAlwaysHaveUpgrades(DefaultOnToggle): - """If turned on, both upgrades will be present for each unit and structure in the seed. - This usually results in fewer units.""" + """ + If turned on, all upgrades will be present for each unit and structure in the seed. + This usually results in fewer units. + + See also: Max Number of Upgrades + """ display_name = "Units Always Have Upgrades" +class GenericUpgradeMissions(Range): + """Determines the percentage of missions in the mission order that must be completed before + level 1 of all weapon and armor upgrades is unlocked. Level 2 upgrades require double the amount of missions, + and level 3 requires triple the amount. The required amounts are always rounded down. + If set to 0, upgrades are instead added to the item pool and must be found to be used.""" + display_name = "Generic Upgrade Missions" + range_start = 0 + range_end = 100 + default = 0 + + +class GenericUpgradeResearch(Choice): + """Determines how weapon and armor upgrades affect missions once unlocked. + + Vanilla: Upgrades must be researched as normal. + Auto In No-Build: In No-Build missions, upgrades are automatically researched. + In all other missions, upgrades must be researched as normal. + Auto In Build: In No-Build missions, upgrades are unavailable as normal. + In all other missions, upgrades are automatically researched. + Always Auto: Upgrades are automatically researched in all missions.""" + display_name = "Generic Upgrade Research" + option_vanilla = 0 + option_auto_in_no_build = 1 + option_auto_in_build = 2 + option_always_auto = 3 + + +class GenericUpgradeItems(Choice): + """Determines how weapon and armor upgrades are split into items. All options produce 3 levels of each item. + Does nothing if upgrades are unlocked by completed mission counts. + + Individual Items: All weapon and armor upgrades are each an item, + resulting in 18 total upgrade items. + Bundle Weapon And Armor: All types of weapon upgrades are one item, + and all types of armor upgrades are one item, + resulting in 6 total items. + Bundle Unit Class: Weapon and armor upgrades are merged, + but Infantry, Vehicle, and Starship upgrades are bundled separately, + resulting in 9 total items. + Bundle All: All weapon and armor upgrades are one item, + resulting in 3 total items.""" + display_name = "Generic Upgrade Items" + option_individual_items = 0 + option_bundle_weapon_and_armor = 1 + option_bundle_unit_class = 2 + option_bundle_all = 3 + + +class NovaCovertOpsItems(Toggle): + """If turned on, the equipment upgrades from Nova Covert Ops may be present in the world.""" + display_name = "Nova Covert Ops Items" + default = Toggle.option_true + + +class BroodWarItems(Toggle): + """If turned on, returning items from StarCraft: Brood War may appear in the world.""" + display_name = "Brood War Items" + default = Toggle.option_true + + +class ExtendedItems(Toggle): + """If turned on, original items that did not appear in Campaign mode may appear in the world.""" + display_name = "Extended Items" + default = Toggle.option_true + + +class MaxNumberOfUpgrades(Range): + """ + Set a maximum to the number of upgrades a unit/structure can have. -1 is used to define unlimited. + Note that most unit have 4 or 6 upgrades. + + If used with Units Always Have Upgrades, each unit has this given amount of upgrades (if there enough upgrades exist) + + See also: Units Always Have Upgrades + """ + display_name = "Maximum number of upgrades per unit/structure" + range_start = -1 + # Do not know the maximum, but it is less than 123! + range_end = 123 + default = -1 + + class LockedItems(ItemSet): """Guarantees that these items will be unlockable""" display_name = "Locked Items" @@ -108,27 +242,114 @@ class ExcludedItems(ItemSet): class ExcludedMissions(OptionSet): """Guarantees that these missions will not appear in the campaign - Only applies on shortened mission orders. + Doesn't apply to vanilla mission order. It may be impossible to build a valid campaign if too many missions are excluded.""" display_name = "Excluded Missions" valid_keys = {mission_name for mission_name in vanilla_mission_req_table.keys() if mission_name != 'All-In'} +class LocationInclusion(Choice): + option_enabled = 0 + option_trash = 1 + option_nothing = 2 + + +class MissionProgressLocations(LocationInclusion): + """ + Enables or disables item rewards for progressing (not finishing) a mission. + Progressing a mission is usually a task of completing or progressing into a main objective. + Clearing an expansion base also counts here. + + Enabled: All locations fitting into this do their normal rewards + Trash: Forces a trash item in + 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" + + +class BonusLocations(LocationInclusion): + """ + Enables or disables item rewards for completing bonus tasks. + Bonus tasks are those giving you a campaign-wide or mission-wide bonus in vanilla game: + Research, credits, bonus units or resources, etc. + + Enabled: All locations fitting into this do their normal rewards + Trash: Forces a trash item in + 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" + + +class ChallengeLocations(LocationInclusion): + """ + Enables or disables item rewards for completing challenge tasks. + Challenges are tasks that have usually higher requirements to be completed + than to complete the mission they're in successfully. + You might be required to visit the same mission later when getting stronger in order to finish these tasks. + + Enabled: All locations fitting into this do their normal rewards + Trash: Forces a trash item in + 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" + + +class OptionalBossLocations(LocationInclusion): + """ + Enables or disables item rewards for defeating optional bosses. + An optional boss is any boss that's not required to kill in order to finish the mission successfully. + All Brutalisks, Loki, etc. belongs here. + + Enabled: All locations fitting into this do their normal rewards + Trash: Forces a trash item in + 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" + + # noinspection PyTypeChecker sc2wol_options: Dict[str, Option] = { "game_difficulty": GameDifficulty, - "upgrade_bonus": UpgradeBonus, - "bunker_upgrade": BunkerUpgrade, + "game_speed": GameSpeed, "all_in_map": AllInMap, + "final_map": FinalMap, "mission_order": MissionOrder, + "player_color": PlayerColor, "shuffle_protoss": ShuffleProtoss, "shuffle_no_build": ShuffleNoBuild, "early_unit": EarlyUnit, "required_tactics": RequiredTactics, "units_always_have_upgrades": UnitsAlwaysHaveUpgrades, + "max_number_of_upgrades": MaxNumberOfUpgrades, + "generic_upgrade_missions": GenericUpgradeMissions, + "generic_upgrade_research": GenericUpgradeResearch, + "generic_upgrade_items": GenericUpgradeItems, "locked_items": LockedItems, "excluded_items": ExcludedItems, - "excluded_missions": ExcludedMissions + "excluded_missions": ExcludedMissions, + "nco_items": NovaCovertOpsItems, + "bw_items": BroodWarItems, + "ext_items": ExtendedItems, + "mission_progress_locations": MissionProgressLocations, + "bonus_locations": BonusLocations, + "challenge_locations": ChallengeLocations, + "optional_boss_locations": OptionalBossLocations } diff --git a/worlds/sc2wol/PoolFilter.py b/worlds/sc2wol/PoolFilter.py index 16cc51f24372..4a19e2dbb305 100644 --- a/worlds/sc2wol/PoolFilter.py +++ b/worlds/sc2wol/PoolFilter.py @@ -1,22 +1,22 @@ from typing import Callable, Dict, List, Set from BaseClasses import MultiWorld, ItemClassification, Item, Location -from .Items import item_table +from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, filler_items 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 +from .Options import get_option_value, MissionOrder, FinalMap, MissionProgressLocations, LocationInclusion from .LogicMixin import SC2WoLLogic # Items with associated upgrades UPGRADABLE_ITEMS = [ "Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre", - "Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", - "Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", + "Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine", "Cyclone", + "Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Raven", "Science Vessel", "Liberator", "Valkyrie", "Bunker", "Missile Turret" ] BARRACKS_UNITS = {"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre"} -FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator"} -STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven"} +FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine"} +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"} @@ -30,7 +30,7 @@ def filter_missions(multiworld: MultiWorld, player: int) -> Dict[int, List[str]] shuffle_no_build = get_option_value(multiworld, player, "shuffle_no_build") shuffle_protoss = get_option_value(multiworld, player, "shuffle_protoss") excluded_missions = get_option_value(multiworld, player, "excluded_missions") - mission_count = len(mission_orders[mission_order_type]) - 1 + final_map = get_option_value(multiworld, player, "final_map") mission_pools = { MissionPools.STARTER: no_build_regions_list[:], MissionPools.EASY: easy_regions_list[:], @@ -38,21 +38,18 @@ def filter_missions(multiworld: MultiWorld, player: int) -> Dict[int, List[str]] MissionPools.HARD: hard_regions_list[:], MissionPools.FINAL: [] } - if mission_order_type == 0: + if mission_order_type == MissionOrder.option_vanilla: # Vanilla uses the entire mission pool mission_pools[MissionPools.FINAL] = ['All-In'] return mission_pools - elif mission_order_type == 1: - # Vanilla Shuffled ignores the player-provided excluded missions - excluded_missions = set() # Omitting No-Build missions if not shuffling no-build if not shuffle_no_build: excluded_missions = excluded_missions.union(no_build_regions_list) # Omitting Protoss missions if not shuffling protoss if not shuffle_protoss: excluded_missions = excluded_missions.union(PROTOSS_REGIONS) - # Replacing All-In on low mission counts - if mission_count < 14: + # Replacing All-In with alternate ending depending on option + if final_map == FinalMap.option_random_hard: final_mission = multiworld.random.choice([mission for mission in alt_final_mission_locations.keys() if mission not in excluded_missions]) excluded_missions.add(final_mission) else: @@ -92,10 +89,18 @@ def get_item_upgrades(inventory: List[Item], parent_item: Item or str): item_name = parent_item.name if isinstance(parent_item, Item) else parent_item return [ inv_item for inv_item in inventory - if item_table[inv_item.name].parent_item == item_name + if get_full_item_list()[inv_item.name].parent_item == item_name ] +def get_item_quantity(item): + return get_full_item_list()[item.name].quantity + + +def copy_item(item: Item): + return Item(item.name, item.classification, item.code, item.player) + + class ValidInventory: def has(self, item: str, player: int): @@ -124,22 +129,6 @@ def generate_reduced_inventory(self, inventory_size: int, mission_requirements: cascade_keys = self.cascade_removal_map.keys() units_always_have_upgrades = get_option_value(self.multiworld, self.player, "units_always_have_upgrades") - # Locking associated items for items that have already been placed when units_always_have_upgrades is on - if units_always_have_upgrades: - existing_items = self.existing_items[:] - while existing_items: - existing_item = existing_items.pop() - items_to_lock = self.cascade_removal_map.get(existing_item, [existing_item]) - for item in items_to_lock: - if item in inventory: - inventory.remove(item) - locked_items.append(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()) - def attempt_removal(item: Item) -> bool: # If item can be removed and has associated items, remove them as well inventory.remove(item) @@ -149,9 +138,77 @@ 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) - locked_items.append(item) + for _ in range(get_item_quantity(item)): + locked_items.append(copy_item(item)) return False return True + + # Limit the maximum number of upgrades + maxUpgrad = get_option_value(self.multiworld, self.player, + "max_number_of_upgrades") + if maxUpgrad != -1: + unit_avail_upgrades = {} + # Needed to take into account locked/existing items + unit_nb_upgrades = {} + for item in inventory: + cItem = get_full_item_list()[item.name] + if cItem.type in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades: + unit_avail_upgrades[item.name] = [] + unit_nb_upgrades[item.name] = 0 + elif cItem.parent_item is not None: + if cItem.parent_item not in unit_avail_upgrades: + unit_avail_upgrades[cItem.parent_item] = [item] + unit_nb_upgrades[cItem.parent_item] = 1 + else: + unit_avail_upgrades[cItem.parent_item].append(item) + unit_nb_upgrades[cItem.parent_item] += 1 + # For those two categories, we count them but dont include them in removal + for item in locked_items + self.existing_items: + cItem = get_full_item_list()[item.name] + if cItem.type in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades: + unit_avail_upgrades[item.name] = [] + unit_nb_upgrades[item.name] = 0 + elif cItem.parent_item is not None: + if cItem.parent_item not in unit_avail_upgrades: + unit_nb_upgrades[cItem.parent_item] = 1 + else: + unit_nb_upgrades[cItem.parent_item] += 1 + # Making sure that the upgrades being removed is random + # Currently, only for combat shield vs Stabilizer Medpacks... + shuffled_unit_upgrade_list = list(unit_avail_upgrades.keys()) + self.multiworld.random.shuffle(shuffled_unit_upgrade_list) + for unit in shuffled_unit_upgrade_list: + while (unit_nb_upgrades[unit] > maxUpgrad) \ + and (len(unit_avail_upgrades[unit]) > 0): + itemCandidate = self.multiworld.random.choice(unit_avail_upgrades[unit]) + _ = attempt_removal(itemCandidate) + # Whatever it succeed to remove the iventory or it fails and thus + # lock it, the upgrade is no longer available for removal + unit_avail_upgrades[unit].remove(itemCandidate) + unit_nb_upgrades[unit] -= 1 + + # Locking associated items for items that have already been placed when units_always_have_upgrades is on + if units_always_have_upgrades: + existing_items = set(self.existing_items[:] + locked_items) + while existing_items: + existing_item = existing_items.pop() + items_to_lock = self.cascade_removal_map.get(existing_item, [existing_item]) + if get_full_item_list()[existing_item.name].type != "Upgrade": + # Don't process general upgrades, they may have been pre-locked per-level + for item in items_to_lock: + if item in inventory: + # Unit upgrades, lock all levels + for _ in range(inventory.count(item)): + 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)): + locked_items.append(copy_item(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()) # Determining if the full-size inventory can complete campaign if not all(requirement(self) for requirement in requirements): @@ -185,21 +242,47 @@ def attempt_removal(item: Item) -> bool: if cascade_failure: for transient_item in transient_items: if transient_item in inventory: - inventory.remove(transient_item) + for _ in range(inventory.count(transient_item)): + inventory.remove(transient_item) if transient_item not in locked_items: - locked_items.append(transient_item) + for _ in range(get_item_quantity(transient_item)): + 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) else: attempt_removal(item) - return inventory + locked_items + if not spider_mine_sources & self.logical_inventory: + inventory = [item for item in inventory if not item.name.endswith("(Spider Mine)")] + if not BARRACKS_UNITS & self.logical_inventory: + inventory = [item for item in inventory if + not (item.name.startswith("Progressive Infantry") or item.name == "Orbital Strike")] + if not FACTORY_UNITS & self.logical_inventory: + inventory = [item for item in inventory if not item.name.startswith("Progressive Vehicle")] + if not STARPORT_UNITS & self.logical_inventory: + inventory = [item for item in inventory if not item.name.startswith("Progressive Ship")] + + # Cull finished, adding locked items back into inventory + inventory += locked_items + + # Replacing empty space with generically useful items + replacement_items = [item for item in self.item_pool + if (item not in inventory + and item not in self.locked_items + and item.name in second_pass_placeable_items)] + self.multiworld.random.shuffle(replacement_items) + while len(inventory) < inventory_size and len(replacement_items) > 0: + item = replacement_items.pop() + inventory.append(item) + + return inventory def _read_logic(self): self._sc2wol_has_common_unit = lambda world, player: SC2WoLLogic._sc2wol_has_common_unit(self, world, player) self._sc2wol_has_air = lambda world, player: SC2WoLLogic._sc2wol_has_air(self, world, player) self._sc2wol_has_air_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_air_anti_air(self, world, player) self._sc2wol_has_competent_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_anti_air(self, world, player) + self._sc2wol_has_competent_ground_to_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_ground_to_air(self, world, player) self._sc2wol_has_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_anti_air(self, world, player) self._sc2wol_defense_rating = lambda world, player, zerg_enemy, air_enemy=False: SC2WoLLogic._sc2wol_defense_rating(self, world, player, zerg_enemy, air_enemy) self._sc2wol_has_competent_comp = lambda world, player: SC2WoLLogic._sc2wol_has_competent_comp(self, world, player) @@ -210,6 +293,8 @@ def _read_logic(self): self._sc2wol_has_protoss_common_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_common_units(self, world, player) self._sc2wol_has_protoss_medium_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_medium_units(self, world, player) self._sc2wol_has_mm_upgrade = lambda world, player: SC2WoLLogic._sc2wol_has_mm_upgrade(self, world, player) + self._sc2wol_welcome_to_the_jungle_requirement = lambda world, player: SC2WoLLogic._sc2wol_welcome_to_the_jungle_requirement(self, world, player) + self._sc2wol_can_respond_to_colony_infestations = lambda world, player: SC2WoLLogic._sc2wol_can_respond_to_colony_infestations(self, world, player) self._sc2wol_final_mission_requirements = lambda world, player: SC2WoLLogic._sc2wol_final_mission_requirements(self, world, player) def __init__(self, multiworld: MultiWorld, player: int, @@ -230,7 +315,7 @@ def __init__(self, multiworld: MultiWorld, player: int, self.min_units_per_structure = int(mission_count / 7) min_upgrades = 1 if mission_count < 10 else 2 for item in item_pool: - item_info = item_table[item.name] + item_info = get_full_item_list()[item.name] if item_info.type == "Upgrade": # Locking upgrades based on mission duration if item.name not in item_quantities: diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py index 033636662b59..f588ce7e982e 100644 --- a/worlds/sc2wol/Regions.py +++ b/worlds/sc2wol/Regions.py @@ -1,10 +1,14 @@ from typing import List, Set, Dict, Tuple, Optional, Callable from BaseClasses import MultiWorld, Region, Entrance, Location from .Locations import LocationData -from .Options import get_option_value -from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations, MissionPools +from .Options import get_option_value, MissionOrder +from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations, \ + MissionPools, vanilla_shuffle_order from .PoolFilter import filter_missions +PROPHECY_CHAIN_MISSION_COUNT = 4 + +VANILLA_SHUFFLED_FIRST_PROPHECY_MISSION = 21 def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location])\ -> Tuple[Dict[str, MissionInfo], int, str]: @@ -19,7 +23,7 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio names: Dict[str, int] = {} - if mission_order_type == 0: + if mission_order_type == MissionOrder.option_vanilla: # Generating all regions and locations for region_name in vanilla_mission_req_table.keys(): @@ -108,12 +112,17 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio removals = len(mission_order) - mission_pool_size # Removing entire Prophecy chain on vanilla shuffled when not shuffling protoss if remove_prophecy: - removals -= 4 + removals -= PROPHECY_CHAIN_MISSION_COUNT # Initial fill out of mission list and marking all-in mission for mission in mission_order: # Removing extra missions if mission pool is too small - if 0 < mission.removal_priority <= removals or mission.category == 'Prophecy' and remove_prophecy: + # Also handle lower removal priority than Prophecy + if 0 < mission.removal_priority <= removals or mission.category == 'Prophecy' and remove_prophecy \ + or (remove_prophecy and mission_order_type == MissionOrder.option_vanilla_shuffled + and mission.removal_priority > vanilla_shuffle_order[ + VANILLA_SHUFFLED_FIRST_PROPHECY_MISSION].removal_priority + and 0 < mission.removal_priority <= removals + PROPHECY_CHAIN_MISSION_COUNT): missions.append(None) elif mission.type == MissionPools.FINAL: missions.append(final_mission) @@ -191,22 +200,38 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio # TODO: Handle 'and' connections mission_req_table = {} + def build_connection_rule(mission_names: List[str], missions_req: int) -> Callable: + if len(mission_names) > 1: + return lambda state: state.has_all({f"Beat {name}" for name in mission_names}, player) and \ + state._sc2wol_cleared_missions(multiworld, player, missions_req) + else: + return lambda state: state.has(f"Beat {mission_names[0]}", player) and \ + state._sc2wol_cleared_missions(multiworld, player, missions_req) + for i, mission in enumerate(missions): if mission is None: continue connections = [] + all_connections = [] + for connection in mission_order[i].connect_to: + if connection == -1: + continue + while missions[connection] is None: + connection -= 1 + all_connections.append(missions[connection]) for connection in mission_order[i].connect_to: required_mission = missions[connection] if connection == -1: connect(multiworld, player, names, "Menu", mission) - elif required_mission is None: - continue else: + if required_mission is None and not mission_order[i].completion_critical: # Drop non-critical null slots + continue + while required_mission is None: # Substituting null slot with prior slot + connection -= 1 + required_mission = missions[connection] + required_missions = [required_mission] if mission_order[i].or_requirements else all_connections connect(multiworld, player, names, required_mission, mission, - (lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and - state._sc2wol_cleared_missions(multiworld, player, - missions_req))) - (missions[connection], mission_order[i].number)) + build_connection_rule(required_missions, mission_order[i].number)) connections.append(slot_map[connection]) mission_req_table.update({mission: MissionInfo( diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 490524290c8a..93aebb7ad15a 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -3,11 +3,11 @@ from typing import List, Set, Tuple, Dict from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification from worlds.AutoWorld import WebWorld, World -from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_full_item_list, \ - get_basic_units -from .Locations import get_locations +from .Items import StarcraftWoLItem, filler_items, item_name_groups, get_item_table, get_full_item_list, \ + get_basic_units, ItemData, upgrade_included_names, progressive_if_nco +from .Locations import get_locations, LocationType from .Regions import create_regions -from .Options import sc2wol_options, get_option_value +from .Options import sc2wol_options, get_option_value, LocationInclusion from .LogicMixin import SC2WoLLogic from .PoolFilter import filter_missions, filter_items, get_item_upgrades from .MissionTables import starting_mission_locations, MissionInfo @@ -36,7 +36,7 @@ class SC2WoLWorld(World): web = Starcraft2WoLWebWorld() data_version = 4 - item_name_to_id = {name: data.code for name, data in item_table.items()} + 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)} option_definitions = sc2wol_options @@ -69,6 +69,8 @@ def create_items(self): starter_items = assign_starter_items(self.multiworld, self.player, excluded_items, self.locked_locations) + filter_locations(self.multiworld, self.player, self.locked_locations, self.location_cache) + pool = get_item_pool(self.multiworld, self.player, self.mission_req_table, starter_items, excluded_items, self.location_cache) fill_item_pool_with_dummy_items(self, self.multiworld, self.player, self.locked_locations, self.location_cache, pool) @@ -109,16 +111,6 @@ def setup_events(player: int, locked_locations: typing.List[str], location_cache def get_excluded_items(multiworld: MultiWorld, player: int) -> Set[str]: excluded_items: Set[str] = set() - if get_option_value(multiworld, player, "upgrade_bonus") == 1: - excluded_items.add("Ultra-Capacitors") - else: - excluded_items.add("Vanadium Plating") - - if get_option_value(multiworld, player, "bunker_upgrade") == 1: - excluded_items.add("Shrike Turret") - else: - excluded_items.add("Fortified Bunker") - for item in multiworld.precollected_items[player]: excluded_items.add(item.name) @@ -167,7 +159,7 @@ def assign_starter_item(multiworld: MultiWorld, player: int, excluded_items: Set def get_item_pool(multiworld: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo], - starter_items: List[str], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]: + starter_items: List[Item], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]: pool: List[Item] = [] # For the future: goal items like Artifact Shards go here @@ -176,17 +168,43 @@ def get_item_pool(multiworld: MultiWorld, player: int, mission_req_table: Dict[s # YAML items yaml_locked_items = get_option_value(multiworld, player, 'locked_items') - for name, data in item_table.items(): - if name not in excluded_items: - for _ in range(data.quantity): - item = create_item_with_correct_settings(player, name) - if name in yaml_locked_items: - locked_items.append(item) - else: - pool.append(item) + # Adjust generic upgrade availability based on options + include_upgrades = get_option_value(multiworld, player, 'generic_upgrade_missions') == 0 + upgrade_items = get_option_value(multiworld, player, 'generic_upgrade_items') + + # Include items from outside Wings of Liberty + item_sets = {'wol'} + if get_option_value(multiworld, player, 'nco_items'): + item_sets.add('nco') + if get_option_value(multiworld, player, 'bw_items'): + item_sets.add('bw') + if get_option_value(multiworld, player, 'ext_items'): + item_sets.add('ext') + + def allowed_quantity(name: str, data: ItemData) -> int: + if name in excluded_items \ + or data.type == "Upgrade" and (not include_upgrades or name not in upgrade_included_names[upgrade_items]) \ + or not data.origin.intersection(item_sets): + return 0 + elif name in progressive_if_nco and 'nco' not in item_sets: + return 1 + else: + return data.quantity + + for name, data in get_item_table(multiworld, player).items(): + for i in range(allowed_quantity(name, data)): + item = create_item_with_correct_settings(player, name) + if name in yaml_locked_items: + locked_items.append(item) + else: + pool.append(item) existing_items = starter_items + [item for item in multiworld.precollected_items[player]] existing_names = [item.name for item in existing_items] + + # Check the parent item integrity, exclude items + pool[:] = [item for item in pool if pool_contains_parent(item, pool + locked_items + existing_items)] + # Removing upgrades for excluded items for item_name in excluded_items: if item_name in existing_names: @@ -207,8 +225,100 @@ def fill_item_pool_with_dummy_items(self: SC2WoLWorld, multiworld: MultiWorld, p def create_item_with_correct_settings(player: int, name: str) -> Item: - data = item_table[name] + data = get_full_item_list()[name] item = Item(name, data.classification, data.code, player) return item + + +def pool_contains_parent(item: Item, pool: [Item]): + item_data = get_full_item_list().get(item.name) + if item_data.parent_item is None: + # The item has not associated parent, the item is valid + return True + parent_item = item_data.parent_item + # Check if the pool contains the parent item + return parent_item in [pool_item.name for pool_item in pool] + + +def filter_locations(multiworld: MultiWorld, player, locked_locations: List[str], location_cache: List[Location]): + """ + Filters the locations in the world using a trash or Nothing item + :param multiworld: + :param player: + :param locked_locations: + :param location_cache: + :return: + """ + open_locations = [location for location in location_cache if location.item is None] + plando_locations = get_plando_locations(multiworld, player) + mission_progress_locations = get_option_value(multiworld, player, "mission_progress_locations") + bonus_locations = get_option_value(multiworld, player, "bonus_locations") + challenge_locations = get_option_value(multiworld, player, "challenge_locations") + optional_boss_locations = get_option_value(multiworld, player, "optional_boss_locations") + location_data = get_locations(multiworld, player) + for location in open_locations: + # Go through the locations that aren't locked yet (early unit, etc) + if location.name not in plando_locations: + # The location is not plando'd + sc2_location = [sc2_location for sc2_location in location_data if sc2_location.name == location.name][0] + location_type = sc2_location.type + + if location_type == LocationType.MISSION_PROGRESS \ + and mission_progress_locations != LocationInclusion.option_enabled: + item_name = get_exclusion_item(multiworld, mission_progress_locations) + place_exclusion_item(item_name, location, locked_locations, player) + + if location_type == LocationType.BONUS \ + and bonus_locations != LocationInclusion.option_enabled: + item_name = get_exclusion_item(multiworld, bonus_locations) + place_exclusion_item(item_name, location, locked_locations, player) + + if location_type == LocationType.CHALLENGE \ + and challenge_locations != LocationInclusion.option_enabled: + item_name = get_exclusion_item(multiworld, challenge_locations) + place_exclusion_item(item_name, location, locked_locations, player) + + if location_type == LocationType.OPTIONAL_BOSS \ + and optional_boss_locations != LocationInclusion.option_enabled: + item_name = get_exclusion_item(multiworld, optional_boss_locations) + place_exclusion_item(item_name, location, locked_locations, player) + + +def place_exclusion_item(item_name, location, locked_locations, player): + item = create_item_with_correct_settings(player, item_name) + location.place_locked_item(item) + locked_locations.append(location.name) + + +def get_exclusion_item(multiworld: MultiWorld, option) -> str: + """ + Gets the exclusion item according to settings (trash/nothing) + :param multiworld: + :param option: + :return: Item used for location exclusion + """ + if option == LocationInclusion.option_nothing: + return "Nothing" + elif option == LocationInclusion.option_trash: + index = multiworld.random.randint(0, len(filler_items) - 1) + return filler_items[index] + raise Exception(f"Unsupported option type: {option}") + + +def get_plando_locations(multiworld: MultiWorld, player) -> List[str]: + """ + + :param multiworld: + :param player: + :return: A list of locations affected by a plando in a world + """ + plando_locations = [] + for plando_setting in multiworld.plando_items[player]: + plando_locations += plando_setting.get("locations", []) + plando_setting_location = plando_setting.get("location", None) + if plando_setting_location is not None: + plando_locations.append(plando_setting_location) + + return plando_locations diff --git a/worlds/sc2wol/docs/setup_en.md b/worlds/sc2wol/docs/setup_en.md index 13c7cb91e3aa..419f98a7330d 100644 --- a/worlds/sc2wol/docs/setup_en.md +++ b/worlds/sc2wol/docs/setup_en.md @@ -49,7 +49,7 @@ specific description of what's going wrong and attach your log file to your mess ## Running in macOS -To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](https://archipelago.gg/tutorial/Archipelago/mac/en). Note: when running the client, you will need to run the command `python3 Starcraft2Client.py`. This is done to make sure that `/download_data` works correctly. +To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](https://archipelago.gg/tutorial/Archipelago/mac/en). Note: when running the client, you will need to run the command `python3 Starcraft2Client.py`. ## Running in Linux diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index aa03c0783bdf..21e29868eda5 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -765,7 +765,7 @@ def resolve_symbols_to_file_offset_based_dict(byte_edits_arr: List[ByteEdit]) -> romPatcher.writeObjectives(itemLocs, romPatcher.settings["tourian"]) romPatcher.writeItemsLocs(self.itemLocs) - # romPatcher.writeSplitLocs(self.variaRando.args.majorsSplit, itemLocs, progItemLocs) + romPatcher.writeSplitLocs(self.variaRando.args.majorsSplit, self.itemLocs, None) #progItemLocs) romPatcher.writeItemsNumber() if not romPatcher.settings["isPlando"]: romPatcher.writeSeed(romPatcher.settings["seed"]) # lol if race mode diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips index 0455364d8a7b..67863bb9f002 100644 Binary files a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips and b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips differ diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym index 2d231bc89961..470b7a8af023 100644 --- a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym +++ b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym @@ -2,7 +2,7 @@ ; generated by asar [labels] -82:FA23 :neg_1_1 +82:FA35 :neg_1_1 B8:83C1 :neg_1_2 B8:85DA :neg_1_3 B8:85F9 :neg_1_4 @@ -177,7 +177,7 @@ A0:FE00 setup_music A0:FE0B setup_music_quick A0:FE94 setup_samus A0:FEA5 setup_samus_normal -82:FA15 sm_fix_checksum +82:FA27 sm_fix_checksum B8:8800 sm_item_graphics B8:882E sm_item_plm_pickup_sequence_pointers B8:847B sm_save_done_hook @@ -192,7 +192,7 @@ B8:83F4 write_repeated_memory_loop [source files] 0000 fe019f55 main.asm -0001 91a745e3 ../common/fast_reload.asm +0001 be17692f ../common/fast_reload.asm 0002 06780555 ../common/nofanfare.asm 0003 7a8904b6 ../common/multiworld.asm 0004 f7e9db95 ../common/itemextras.asm @@ -200,163 +200,170 @@ B8:83F4 write_repeated_memory_loop 0006 dbfcb38d ../common/startitem.asm [rom checksum] -f95f09a7 +979ac031 [addr-to-line mapping] ff:ffff 0000:00000001 -ce:ff0a 0001:00000012 -82:f990 0001:00000016 -82:f991 0001:00000017 -82:f993 0001:00000018 -82:f994 0001:00000019 -82:f996 0001:0000001a -82:f999 0001:0000001b -82:f99c 0001:0000001c -82:f99e 0001:0000001d -82:f9a2 0001:0000001e -82:f9a4 0001:0000001f -82:f9a7 0001:00000020 -82:f9a9 0001:00000021 -82:f9ac 0001:00000022 -82:f9af 0001:00000023 -82:f9b1 0001:00000024 -82:f9b5 0001:00000025 -82:f9b8 0001:00000026 -82:f9ba 0001:00000027 -82:f9bd 0001:00000028 -82:f9c1 0001:00000029 -82:f9c3 0001:0000002a -82:f9c4 0001:0000002b -82:f9c5 0001:0000002c -82:f9c8 0001:0000002d -82:f9c9 0001:0000002f -82:f9ca 0001:00000030 -82:f9cb 0001:00000031 -82:f9cc 0001:00000034 -82:f9cd 0001:00000035 -82:f9cf 0001:00000036 -82:f9d2 0001:00000037 -82:f9d5 0001:00000039 -82:f9d9 0001:0000003a -82:f9dc 0001:0000003a -82:f9de 0001:0000003b -82:f9e1 0001:0000003d -82:f9e2 0001:0000003e -82:f9e5 0001:0000003e -82:f9e6 0001:0000003e -82:f9e9 0001:0000003f -82:f9ea 0001:0000003f -82:f9eb 0001:00000040 -82:f9ee 0001:00000040 -82:f9f1 0001:00000041 -82:f9f2 0001:00000042 -82:f9f6 0001:00000043 -82:f9fa 0001:00000044 -82:f9fe 0001:00000046 -82:fa02 0001:00000047 -82:fa05 0001:00000048 -82:fa09 0001:00000049 -82:fa0d 0001:0000004a -82:fa10 0001:0000004b -82:fa13 0001:0000004c -82:fa14 0001:0000004d -82:fa15 0001:00000050 -82:fa16 0001:00000051 +ce:ff0a 0001:00000013 +82:f990 0001:00000017 +82:f991 0001:00000018 +82:f993 0001:00000019 +82:f994 0001:0000001a +82:f996 0001:0000001b +82:f999 0001:0000001c +82:f99c 0001:0000001d +82:f99e 0001:0000001e +82:f9a2 0001:0000001f +82:f9a4 0001:00000020 +82:f9a7 0001:00000021 +82:f9a9 0001:00000022 +82:f9ac 0001:00000023 +82:f9af 0001:00000024 +82:f9b1 0001:00000025 +82:f9b5 0001:00000026 +82:f9b8 0001:00000027 +82:f9ba 0001:00000028 +82:f9bd 0001:00000029 +82:f9c1 0001:0000002a +82:f9c3 0001:0000002b +82:f9c4 0001:0000002c +82:f9c5 0001:0000002d +82:f9c8 0001:0000002e +82:f9c9 0001:00000030 +82:f9ca 0001:00000031 +82:f9cb 0001:00000032 +82:f9cc 0001:00000035 +82:f9cd 0001:00000036 +82:f9cf 0001:00000037 +82:f9d2 0001:00000038 +82:f9d5 0001:0000003a +82:f9d9 0001:0000003b +82:f9dc 0001:0000003b +82:f9de 0001:0000003c +82:f9e1 0001:0000003e +82:f9e2 0001:0000003f +82:f9e6 0001:00000040 +82:f9ea 0001:00000042 +82:f9ee 0001:00000043 +82:f9f0 0001:00000044 +82:f9f1 0001:00000045 +82:f9f2 0001:00000046 +82:f9f6 0001:00000047 +82:f9f9 0001:00000048 +82:f9fa 0001:00000049 +82:f9fb 0001:0000004a +82:f9fc 0001:0000004b +82:f9ff 0001:0000004b +82:fa00 0001:0000004b +82:fa04 0001:0000004c +82:fa05 0001:0000004d +82:fa08 0001:0000004d +82:fa0c 0001:0000004e +82:fa10 0001:00000050 +82:fa14 0001:00000051 82:fa17 0001:00000052 -82:fa18 0001:00000053 -82:fa19 0000:00000013 -82:fa1b 0001:00000057 -82:fa1d 0001:00000058 -82:fa1e 0001:00000059 -82:fa20 0001:0000005a -82:fa23 0001:0000005c -82:fa27 0001:0000005d -82:fa28 0001:0000005e -82:fa2a 0001:0000005f -82:fa2c 0001:00000060 +82:fa1b 0001:00000053 +82:fa1f 0001:00000054 +82:fa22 0001:00000055 +82:fa25 0001:00000056 +82:fa26 0001:00000057 +82:fa27 0001:0000005a +82:fa28 0001:0000005b +82:fa29 0001:0000005c +82:fa2a 0001:0000005d +82:fa2b 0000:00000013 82:fa2d 0001:00000061 -82:fa2e 0001:00000062 -82:fa31 0001:00000063 -82:fa33 0001:00000065 -82:fa36 0001:00000066 -82:fa38 0001:00000067 -82:fa3c 0001:00000068 -82:fa40 0001:00000069 -82:fa43 0001:0000006a -82:fa47 0001:0000006b -82:fa4b 0001:0000006c -82:fa4c 0001:0000006d -82:fa4e 0001:0000006f -82:fa4f 0001:00000070 -82:fa50 0001:00000071 -82:fa51 0001:00000072 +82:fa2f 0001:00000062 +82:fa30 0001:00000063 +82:fa32 0001:00000064 +82:fa35 0001:00000066 +82:fa39 0001:00000067 +82:fa3a 0001:00000068 +82:fa3c 0001:00000069 +82:fa3e 0001:0000006a +82:fa3f 0001:0000006b +82:fa40 0001:0000006c +82:fa43 0001:0000006d +82:fa45 0001:0000006f +82:fa48 0001:00000070 +82:fa4a 0001:00000071 +82:fa4e 0001:00000072 82:fa52 0001:00000073 -80:a088 0001:00000079 -80:a08c 0001:00000079 -80:a08d 0001:00000079 -80:a095 0001:0000007c -80:a0ce 0001:0000007f -80:a113 0001:00000082 -91:e164 0001:00000085 -91:e168 0001:00000085 -91:e169 0001:00000085 -a0:fe00 0001:0000008a -a0:fe03 0001:0000008b -a0:fe05 0001:0000008c -a0:fe08 0001:0000008d -a0:fe0b 0001:0000008f -a0:fe0c 0001:00000092 -a0:fe10 0001:00000093 -a0:fe13 0001:00000094 -a0:fe15 0001:00000095 -a0:fe18 0001:00000096 -a0:fe1b 0001:00000097 -a0:fe1f 0001:00000099 -a0:fe23 0001:0000009a -a0:fe27 0001:0000009b -a0:fe2b 0001:0000009c -a0:fe2f 0001:0000009f -a0:fe33 0001:000000a0 -a0:fe36 0001:000000a1 -a0:fe38 0001:000000a2 -a0:fe3c 0001:000000a3 -a0:fe40 0001:000000a5 -a0:fe44 0001:000000a8 -a0:fe48 0001:000000a9 -a0:fe49 0001:000000aa -a0:fe4c 0001:000000ab -a0:fe4e 0001:000000ac -a0:fe4f 0001:000000ad -a0:fe53 0001:000000ae -a0:fe57 0001:000000b0 -a0:fe58 0001:000000b1 -a0:fe5c 0001:000000b2 -a0:fe5f 0001:000000b3 -a0:fe62 0001:000000b4 -a0:fe65 0001:000000b5 -a0:fe67 0001:000000b6 -a0:fe6a 0001:000000b7 -a0:fe6d 0001:000000b8 -a0:fe6f 0001:000000b9 -a0:fe73 0001:000000bc -a0:fe76 0001:000000bd -a0:fe79 0001:000000be -a0:fe7c 0001:000000bf -a0:fe7f 0001:000000c1 -a0:fe82 0001:000000c2 -a0:fe85 0001:000000c3 -a0:fe89 0001:000000c4 -a0:fe8c 0001:000000c5 -a0:fe90 0001:000000c7 -a0:fe94 0001:000000ca -a0:fe97 0001:000000cb -a0:fe99 0001:000000cc -a0:fe9c 0001:000000cd -a0:fe9f 0001:000000ce -a0:fea2 0001:000000cf -a0:fea5 0001:000000d1 -a0:fea8 0001:000000d2 -a0:feab 0001:000000d3 +82:fa55 0001:00000074 +82:fa59 0001:00000075 +82:fa5d 0001:00000076 +82:fa5e 0001:00000077 +82:fa60 0001:00000079 +82:fa61 0001:0000007a +82:fa62 0001:0000007b +82:fa63 0001:0000007c +82:fa64 0001:0000007d +80:a088 0001:00000083 +80:a08c 0001:00000083 +80:a08d 0001:00000083 +80:a095 0001:00000086 +80:a0ce 0001:00000089 +80:a113 0001:0000008c +91:e164 0001:0000008f +91:e168 0001:0000008f +91:e169 0001:0000008f +a0:fe00 0001:00000094 +a0:fe03 0001:00000095 +a0:fe05 0001:00000096 +a0:fe08 0001:00000097 +a0:fe0b 0001:00000099 +a0:fe0c 0001:0000009c +a0:fe10 0001:0000009d +a0:fe13 0001:0000009e +a0:fe15 0001:0000009f +a0:fe18 0001:000000a0 +a0:fe1b 0001:000000a1 +a0:fe1f 0001:000000a3 +a0:fe23 0001:000000a4 +a0:fe27 0001:000000a5 +a0:fe2b 0001:000000a6 +a0:fe2f 0001:000000a9 +a0:fe33 0001:000000aa +a0:fe36 0001:000000ab +a0:fe38 0001:000000ac +a0:fe3c 0001:000000ad +a0:fe40 0001:000000af +a0:fe44 0001:000000b2 +a0:fe48 0001:000000b3 +a0:fe49 0001:000000b4 +a0:fe4c 0001:000000b5 +a0:fe4e 0001:000000b6 +a0:fe4f 0001:000000b7 +a0:fe53 0001:000000b8 +a0:fe57 0001:000000ba +a0:fe58 0001:000000bb +a0:fe5c 0001:000000bc +a0:fe5f 0001:000000bd +a0:fe62 0001:000000be +a0:fe65 0001:000000bf +a0:fe67 0001:000000c0 +a0:fe6a 0001:000000c1 +a0:fe6d 0001:000000c2 +a0:fe6f 0001:000000c3 +a0:fe73 0001:000000c6 +a0:fe76 0001:000000c7 +a0:fe79 0001:000000c8 +a0:fe7c 0001:000000c9 +a0:fe7f 0001:000000cb +a0:fe82 0001:000000cc +a0:fe85 0001:000000cd +a0:fe89 0001:000000ce +a0:fe8c 0001:000000cf +a0:fe90 0001:000000d1 +a0:fe94 0001:000000d4 +a0:fe97 0001:000000d5 +a0:fe99 0001:000000d6 +a0:fe9c 0001:000000d7 +a0:fe9f 0001:000000d8 +a0:fea2 0001:000000d9 +a0:fea5 0001:000000db +a0:fea8 0001:000000dc +a0:feab 0001:000000dd 85:ff00 0002:0000010b 85:ff03 0002:0000010c 85:ff06 0002:0000010d diff --git a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json index 1e5dcccb6efb..813937d3f43f 100644 --- a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json +++ b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json @@ -158,7 +158,7 @@ "setup_music_quick": "A0:FE0B", "setup_samus": "A0:FE94", "setup_samus_normal": "A0:FEA5", - "sm_fix_checksum": "82:FA15", + "sm_fix_checksum": "82:FA27", "sm_item_graphics": "B8:8800", "sm_item_plm_pickup_sequence_pointers": "B8:882E", "sm_save_done_hook": "B8:847B", @@ -172,10 +172,11 @@ "write_repeated_memory_loop": "B8:83F4", "deathhook82": "82:DDC7", "freespace82_start": "82:F990", - "freespace82_end": "82:FA53", + "freespace82_end": "82:FA65", "freespacea0": "a0:fe00", "SRAM_SAVING": "70:2604", "current_save_slot": "7e:0952", + "sram_save_slot_addresses": "81:812B", "ITEM_RAM": "7E:09A2", "SRAM_MW_ITEMS_RECV": "70:2000", "SRAM_MW_ITEMS_RECV_WCOUNT": "70:2602", diff --git a/worlds/sm/data/SMBasepatch_prebuilt/variapatches.ips b/worlds/sm/data/SMBasepatch_prebuilt/variapatches.ips index 37139ae5785d..efc38e262f19 100644 Binary files a/worlds/sm/data/SMBasepatch_prebuilt/variapatches.ips and b/worlds/sm/data/SMBasepatch_prebuilt/variapatches.ips differ diff --git a/worlds/sm/variaRandomizer/rom/rompatcher.py b/worlds/sm/variaRandomizer/rom/rompatcher.py index 913d0d8df8ef..1120eacf4b33 100644 --- a/worlds/sm/variaRandomizer/rom/rompatcher.py +++ b/worlds/sm/variaRandomizer/rom/rompatcher.py @@ -25,7 +25,9 @@ class RomPatcher: # faster MB cutscene transitions 'Mother_Brain_Cutscene_Edits', # "Balanced" suit mode - 'Removes_Gravity_Suit_heat_protection' + 'Removes_Gravity_Suit_heat_protection', + # new PLMs for indicating the color of the door on the other side + 'door_indicators_plms.ips' ], # VARIA tweaks 'VariaTweaks' : ['WS_Etank', 'LN_Chozo_SpaceJump_Check_Disable', 'ln_chozo_platform.ips', 'bomb_torizo.ips'], diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index 2397f2c807fe..7c50ba4708af 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -30,6 +30,8 @@ def set_rules(world, player: int, area_connections): fix_reg(entrance_ids, 20, 5, swaplist, world) # Guarantee BITFS is not mapped to DDD fix_reg(entrance_ids, 22, 8, swaplist, world) + if entrance_ids.index(22) == 5: # If BITFS is mapped to HMC... + fix_reg(entrance_ids, 20, 8, swaplist, world) # ... then dont allow COTMC to be mapped to DDD temp_assign = dict(zip(entrance_ids,destination_regions)) # Used for Rules only # Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code diff --git a/worlds/sm64ex/docs/en_Super Mario 64.md b/worlds/sm64ex/docs/en_Super Mario 64.md index 4586369e5ec7..def6e2a37536 100644 --- a/worlds/sm64ex/docs/en_Super Mario 64.md +++ b/worlds/sm64ex/docs/en_Super Mario 64.md @@ -14,7 +14,7 @@ as different Items from within SM64. As in most Mario Games, save the Princess! ## Which items can be in another player's world? -Any of the 120 Stars, and the two Caste Keys. Additionally, Cap Switches are also considered "Items" and the "!"-Boxes will only be active +Any of the 120 Stars, and the two Castle Keys. Additionally, Cap Switches are also considered "Items" and the "!"-Boxes will only be active when someone collects the corresponding Cap Switch Item. ## What does another world's item look like in SM64EX? @@ -25,4 +25,4 @@ and who will receive it. When you receive an Item, a Message will pop up to inform you where you received the Item from, and which one it is. -NOTE: The Secret Star count in the Menu is broken. \ No newline at end of file +NOTE: The Secret Star count in the Menu is broken. diff --git a/worlds/smw/__init__.py b/worlds/smw/__init__.py index 2c3b1114c3be..431287c32bef 100644 --- a/worlds/smw/__init__.py +++ b/worlds/smw/__init__.py @@ -270,5 +270,8 @@ def create_item(self, name: str, force_non_progression=False) -> Item: return created_item + def get_filler_item_name(self) -> str: + return ItemName.one_up_mushroom + def set_rules(self): set_rules(self.multiworld, self.player) diff --git a/worlds/stardew_valley/data/crops.csv b/worlds/stardew_valley/data/crops.csv index 1ca039b77dd3..e3d2dc8256db 100644 --- a/worlds/stardew_valley/data/crops.csv +++ b/worlds/stardew_valley/data/crops.csv @@ -7,6 +7,7 @@ Blueberry,Summer,Blueberry Seeds,Summer,"Pierre's General Store,JojaMart" Bok Choy,Fall,Bok Choy Seeds,Fall,"Pierre's General Store,JojaMart" Cactus Fruit,,Cactus Seeds,,Oasis Cauliflower,Spring,Cauliflower Seeds,Spring,"Pierre's General Store,JojaMart" +Coffee Bean,"Spring,Summer",Coffee Bean,"Summer,Fall","Traveling Cart" Corn,"Summer,Fall",Corn Seeds,"Summer,Fall","Pierre's General Store,JojaMart" Cranberries,Fall,Cranberry Seeds,Fall,"Pierre's General Store,JojaMart" Eggplant,Fall,Eggplant Seeds,Fall,"Pierre's General Store,JojaMart" diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index 80f352d19c1e..a3d61e8b58e0 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -272,6 +272,7 @@ id,name,classification,groups,mod_name 285,Ugly Baby,progression,"BABY", 286,Deluxe Scarecrow Recipe,progression,"FESTIVAL,RARECROW", 287,Treehouse,progression,"GINGER_ISLAND", +288,Coffee Bean,progression,CROPSANITY, 4001,Burnt,trap,TRAP, 4002,Darkness,trap,TRAP, 4003,Frozen,trap,TRAP, diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 50f0267a05db..ef56bf5a12ba 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -1036,6 +1036,7 @@ id,region,name,tags,mod_name 2344,Farm,Harvest Peach,"CROPSANITY", 2345,Farm,Harvest Banana,"CROPSANITY,GINGER_ISLAND", 2346,Farm,Harvest Mango,"CROPSANITY,GINGER_ISLAND", +2347,Farm,Harvest Coffee Bean,"CROPSANITY", 5001,Stardew Valley,Level 1 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill 5002,Stardew Valley,Level 2 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill 5003,Stardew Valley,Level 3 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill diff --git a/worlds/stardew_valley/data/museum_data.py b/worlds/stardew_valley/data/museum_data.py index eb42a17dd5db..b786f5b2d00c 100644 --- a/worlds/stardew_valley/data/museum_data.py +++ b/worlds/stardew_valley/data/museum_data.py @@ -43,8 +43,8 @@ def __repr__(self): unlikely = () -all_artifact_items: List[MuseumItem] = [] -all_mineral_items: List[MuseumItem] = [] +all_museum_artifacts: List[MuseumItem] = [] +all_museum_minerals: List[MuseumItem] = [] all_museum_items: List[MuseumItem] = [] @@ -56,7 +56,7 @@ def create_artifact(name: str, geodes: Union[str, Tuple[str, ...]] = (), monsters: Union[str, Tuple[str, ...]] = ()) -> MuseumItem: artifact_item = MuseumItem.of(name, item_id, difficulty, locations, geodes, monsters) - all_artifact_items.append(artifact_item) + all_museum_artifacts.append(artifact_item) all_museum_items.append(artifact_item) return artifact_item @@ -79,7 +79,7 @@ def create_mineral(name: str, difficulty += 31.0 / 2750.0 * 100 mineral_item = MuseumItem.of(name, item_id, difficulty, locations, geodes, monsters) - all_mineral_items.append(mineral_item) + all_museum_minerals.append(mineral_item) all_museum_items.append(mineral_item) return mineral_item diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md index 042163343eb9..a880a40b971a 100644 --- a/worlds/stardew_valley/docs/en_Stardew Valley.md +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -7,7 +7,7 @@ config file. ## What does randomization do to this game? -A vast number of optional objectives in stardew valley can be shuffled around the multiworld. Most of these are optional, and the player can customize their experience in their YAML file. +A vast number of objectives in Stardew Valley can be shuffled around the multiworld. Most of these are optional, and the player can customize their experience in their YAML file. For these objectives, if they have a vanilla reward, this reward will instead be an item in the multiworld. For the remaining number of such objectives, there are a number of "Resource Pack" items, which are simply an item or a stack of items that may be useful to the player. @@ -28,24 +28,24 @@ The player can choose from a number of goals, using their YAML settings. Location checks in Stardew Valley always include: - [Community Center Bundles](https://stardewvalleywiki.com/Bundles) -- [Mineshaft chest rewards](https://stardewvalleywiki.com/The_Mines#Remixed_Rewards) +- [Mineshaft Chest Rewards](https://stardewvalleywiki.com/The_Mines#Remixed_Rewards) - [Story Quests](https://stardewvalleywiki.com/Quests#List_of_Story_Quests) -- [Traveling Merchant items](https://stardewvalleywiki.com/Traveling_Cart) +- [Traveling Merchant Items](https://stardewvalleywiki.com/Traveling_Cart) - Isolated objectives such as the [beach bridge](https://stardewvalleywiki.com/The_Beach#Tide_Pools), [Old Master Cannoli](https://stardewvalleywiki.com/Secret_Woods#Old_Master_Cannoli), [Grim Reaper Statue](https://stardewvalleywiki.com/Golden_Scythe), etc There also are a number of location checks that are optional, and individual players choose to include them or not in their shuffling: - Tools and Fishing Rod Upgrades - Carpenter Buildings - Backpack Upgrades -- Mine elevator levels +- Mine Elevator Levels - Skill Levels - Arcade Machines -- Help Wanted quests +- Help Wanted Quests - Participating in Festivals - Special Orders from the town board, or from Mr Qi -- Cropsanity: Growing and harvesting individual crop types +- Cropsanity: Growing and Harvesting individual crop types - Fishsanity: Catching individual fish -- Museumsanity: Donating individual items to the museum, or reaching the museum milestones for donations +- Museumsanity: Donating individual items, or reaching milestones for museum donations - Friendsanity: Reaching specific friendship levels with NPCs ## Which items can be in another player's world? diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py index 48afe1d3527e..00b60696a91f 100644 --- a/worlds/stardew_valley/logic.py +++ b/worlds/stardew_valley/logic.py @@ -7,8 +7,9 @@ from . import options from .data import all_fish, FishItem, all_purchasable_seeds, SeedItem, all_crops, CropItem 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_artifact_items, dwarf_scrolls +from .data.museum_data import all_museum_items, MuseumItem, all_museum_artifacts, dwarf_scrolls, 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 @@ -139,7 +140,7 @@ def __post_init__(self): self.crop_rules.update({crop.name: self.can_grow_crop(crop) for crop in all_crops}) self.crop_rules.update({ Seed.coffee: (self.has_season(Season.spring) | self.has_season( - Season.summer)) & self.has_traveling_merchant(), + Season.summer)) & self.can_buy_seed(crops_by_name[Seed.coffee].seed), Fruit.ancient_fruit: (self.received("Ancient Seeds") | self.received("Ancient Seeds Recipe")) & self.can_reach_region(Region.greenhouse) & self.has(Machine.seed_maker), }) @@ -474,8 +475,8 @@ def __post_init__(self): FestivalCheck.mermaid_pearl: self.has_season(Season.winter) & self.can_reach_region(Region.beach), FestivalCheck.cone_hat: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(2500), FestivalCheck.iridium_fireplace: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(15000), - FestivalCheck.rarecrow_7: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(5000) & self.can_find_museum_artifacts(20), - FestivalCheck.rarecrow_8: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(5000) & self.can_find_museum_items(40), + FestivalCheck.rarecrow_7: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(5000) & self.can_donate_museum_artifacts(20), + FestivalCheck.rarecrow_8: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(5000) & self.can_donate_museum_items(40), FestivalCheck.lupini_red_eagle: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(1200), FestivalCheck.lupini_portrait_mermaid: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(1200), FestivalCheck.lupini_solar_kingdom: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(1200), @@ -491,35 +492,39 @@ def __post_init__(self): }) self.special_order_rules.update({ - SpecialOrder.island_ingredients: self.has_island_transport() & self.can_farm_perfectly() & - self.has(Vegetable.taro_root) & self.has(Fruit.pineapple) & self.has(Forageable.ginger), - SpecialOrder.cave_patrol: self.can_mine_perfectly() & self.can_mine_to_floor(120), - SpecialOrder.aquatic_overpopulation: self.can_fish_perfectly(), - SpecialOrder.biome_balance: self.can_fish_perfectly(), - SpecialOrder.rock_rejuivenation: self.has(Mineral.ruby) & self.has(Mineral.topaz) & self.has(Mineral.emerald) & - self.has(Mineral.jade) & self.has(Mineral.amethyst) & self.has_relationship(NPC.emily, 4) & - self.has(ArtisanGood.cloth) & self.can_reach_region(Region.haley_house), - SpecialOrder.gifts_for_george: self.has_season(Season.spring) & self.has(Forageable.leek), - SpecialOrder.fragments_of_the_past: self.can_reach_region(Region.dig_site), - SpecialOrder.gus_famous_omelet: self.has(AnimalProduct.any_egg), - SpecialOrder.crop_order: self.can_farm_perfectly(), - SpecialOrder.community_cleanup: self.can_crab_pot(), - SpecialOrder.the_strong_stuff: self.can_keg(Vegetable.potato), - SpecialOrder.pierres_prime_produce: self.can_farm_perfectly(), - SpecialOrder.robins_project: self.can_chop_perfectly() & self.has(Material.hardwood), - SpecialOrder.robins_resource_rush: self.can_chop_perfectly() & self.has(Fertilizer.tree) & self.can_mine_perfectly(), - SpecialOrder.juicy_bugs_wanted_yum: self.has(Loot.bug_meat), - SpecialOrder.tropical_fish: self.has_island_transport() & self.has(Fish.stingray) & self.has(Fish.blue_discus) & self.has(Fish.lionfish), - SpecialOrder.a_curious_substance: self.can_mine_perfectly() & self.can_mine_to_floor(80), - SpecialOrder.prismatic_jelly: self.can_mine_perfectly() & self.can_mine_to_floor(40), + SpecialOrder.island_ingredients: self.can_meet(NPC.caroline) & self.has_island_transport() & self.can_farm_perfectly() & + self.can_ship(Vegetable.taro_root) & self.can_ship(Fruit.pineapple) & self.can_ship(Forageable.ginger), + SpecialOrder.cave_patrol: self.can_meet(NPC.clint) & self.can_mine_perfectly() & self.can_mine_to_floor(120), + SpecialOrder.aquatic_overpopulation: self.can_meet(NPC.demetrius) & self.can_fish_perfectly(), + SpecialOrder.biome_balance: self.can_meet(NPC.demetrius) & self.can_fish_perfectly(), + SpecialOrder.rock_rejuivenation: self.has_relationship(NPC.emily, 4) & self.has(Mineral.ruby) & self.has(Mineral.topaz) & + self.has(Mineral.emerald) & self.has(Mineral.jade) & self.has(Mineral.amethyst) & + self.has(ArtisanGood.cloth) & self.can_reach_region(Region.haley_house), + SpecialOrder.gifts_for_george: self.can_reach_region(Region.alex_house) & self.has_season(Season.spring) & self.has(Forageable.leek), + SpecialOrder.fragments_of_the_past: self.can_reach_region(Region.museum) & self.can_reach_region(Region.dig_site) & self.has_tool(Tool.pickaxe), + SpecialOrder.gus_famous_omelet: self.can_reach_region(Region.saloon) & self.has(AnimalProduct.any_egg), + SpecialOrder.crop_order: self.can_farm_perfectly() & self.can_ship(), + SpecialOrder.community_cleanup: self.can_reach_region(Region.railroad) & self.can_crab_pot(), + SpecialOrder.the_strong_stuff: self.can_reach_region(Region.trailer) & self.can_keg(Vegetable.potato), + SpecialOrder.pierres_prime_produce: self.can_reach_region(Region.pierre_store) & self.can_farm_perfectly(), + SpecialOrder.robins_project: self.can_meet(NPC.robin) & self.can_reach_region(Region.carpenter) & self.can_chop_perfectly() & + self.has(Material.hardwood), + SpecialOrder.robins_resource_rush: self.can_meet(NPC.robin) & self.can_reach_region(Region.carpenter) & self.can_chop_perfectly() & + self.has(Fertilizer.tree) & self.can_mine_perfectly(), + SpecialOrder.juicy_bugs_wanted_yum: self.can_reach_region(Region.beach) & self.has(Loot.bug_meat), + SpecialOrder.tropical_fish: self.can_meet(NPC.willy) & self.received("Island Resort") & self.has_island_transport() & + self.has(Fish.stingray) & self.has(Fish.blue_discus) & self.has(Fish.lionfish), + SpecialOrder.a_curious_substance: self.can_reach_region(Region.wizard_tower) & self.can_mine_perfectly() & self.can_mine_to_floor(80), + SpecialOrder.prismatic_jelly: self.can_reach_region(Region.wizard_tower) & self.can_mine_perfectly() & self.can_mine_to_floor(40), SpecialOrder.qis_crop: self.can_farm_perfectly() & self.can_reach_region(Region.greenhouse) & self.can_reach_region(Region.island_west) & self.has_total_skill_level(50) & - self.has(Machine.seed_maker), + self.has(Machine.seed_maker) & self.has_building(Building.shipping_bin), SpecialOrder.lets_play_a_game: self.has_junimo_kart_max_level(), SpecialOrder.four_precious_stones: self.has_lived_months(MAX_MONTHS) & self.has("Prismatic Shard") & self.can_mine_perfectly_in_the_skull_cavern(), SpecialOrder.qis_hungry_challenge: self.can_mine_perfectly_in_the_skull_cavern() & self.has_max_buffs(), - SpecialOrder.qis_cuisine: self.can_cook() & (self.can_spend_money_at(Region.saloon, 205000) | self.can_spend_money_at(Region.pierre_store, 170000)), + SpecialOrder.qis_cuisine: self.can_cook() & (self.can_spend_money_at(Region.saloon, 205000) | self.can_spend_money_at(Region.pierre_store, 170000)) & + self.can_ship(), SpecialOrder.qis_kindness: self.can_give_loved_gifts_to_everyone(), SpecialOrder.extended_family: self.can_fish_perfectly() & self.has(Fish.angler) & self.has(Fish.glacierfish) & self.has(Fish.crimsonfish) & self.has(Fish.mutant_carp) & self.has(Fish.legend), @@ -1038,11 +1043,12 @@ def can_reproduce(self, number_children: int = 1) -> StardewRule: def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule: if hearts <= 0: return True_() - if self.options[options.Friendsanity] == options.Friendsanity.option_none: + friendsanity = self.options[options.Friendsanity] + if friendsanity == options.Friendsanity.option_none: return self.can_earn_relationship(npc, hearts) if npc not in all_villagers_by_name: if npc == NPC.pet: - if self.options[options.Friendsanity] == options.Friendsanity.option_bachelors: + if friendsanity == options.Friendsanity.option_bachelors: return self.can_befriend_pet(hearts) return self.received_hearts(NPC.pet, hearts) if npc == Generic.any or npc == Generic.bachelor: @@ -1072,12 +1078,12 @@ def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule: if not self.npc_is_in_current_slot(npc): return True_() villager = all_villagers_by_name[npc] - if self.options[options.Friendsanity] == options.Friendsanity.option_bachelors and not villager.bachelor: + if friendsanity == options.Friendsanity.option_bachelors and not villager.bachelor: return self.can_earn_relationship(npc, hearts) - if self.options[options.Friendsanity] == options.Friendsanity.option_starting_npcs and not villager.available: + if friendsanity == options.Friendsanity.option_starting_npcs and not villager.available: return self.can_earn_relationship(npc, hearts) - if self.options[ - options.Friendsanity] != options.Friendsanity.option_all_with_marriage and villager.bachelor and hearts > 8: + is_capped_at_8 = villager.bachelor and friendsanity != options.Friendsanity.option_all_with_marriage + if is_capped_at_8 and hearts > 8: return self.received_hearts(villager, 8) & self.can_earn_relationship(npc, hearts) return self.received_hearts(villager, hearts) @@ -1094,6 +1100,8 @@ def can_meet(self, npc: str) -> StardewRule: rules = [self.can_reach_any_region(villager.locations)] if npc == NPC.kent: rules.append(self.has_year_two()) + elif npc == NPC.leo: + rules.append(self.received("Island West Turtle")) return And(rules) @@ -1129,11 +1137,22 @@ def can_earn_relationship(self, npc: str, hearts: int = 0) -> StardewRule: rule_if_birthday = self.has_season(villager.birthday) & self.has_any_universal_love() & self.has_lived_months(hearts // 2) rule_if_not_birthday = self.has_lived_months(hearts) earn_rule = self.can_meet(npc) & (rule_if_birthday | rule_if_not_birthday) + if villager.bachelor: + if hearts > 8: + earn_rule = earn_rule & self.can_date(npc) + if hearts > 10: + earn_rule = earn_rule & self.can_marry(npc) else: earn_rule = self.has_lived_months(min(hearts // 2, 8)) return previous_heart_rule & earn_rule + def can_date(self, npc: str) -> StardewRule: + return self.has_relationship(npc, 8) & self.has(Gift.bouquet) + + def can_marry(self, npc: str) -> StardewRule: + return self.has_relationship(npc, 10) & self.has(Gift.mermaid_pendant) + def can_befriend_pet(self, hearts: int): if hearts <= 0: return True_() @@ -1154,7 +1173,7 @@ def can_complete_bundle(self, bundle_requirements: List[BundleItem], number_requ item_rules.append(bundle_item.item.name) if bundle_item.quality > highest_quality_yet: highest_quality_yet = bundle_item.quality - return self.has(item_rules, number_required) & self.can_grow_gold_quality(highest_quality_yet) + return self.can_reach_region(Region.wizard_tower) & self.has(item_rules, number_required) & self.can_grow_gold_quality(highest_quality_yet) def can_grow_gold_quality(self, quality: int) -> StardewRule: if quality <= 0: @@ -1212,7 +1231,7 @@ def can_finish_grandpa_evaluation(self) -> StardewRule: self.can_have_earned_total_money(1000000), # 1 000 000g second point self.has_total_skill_level(30), # Total Skills: 30 self.has_total_skill_level(50), # Total Skills: 50 - # Completing the museum not expected + self.can_complete_museum(), # Completing the museum for a point # Catching every fish not expected # Shipping every item not expected self.can_get_married() & self.has_house(2), @@ -1223,7 +1242,7 @@ def can_finish_grandpa_evaluation(self) -> StardewRule: self.can_complete_community_center(), # CC Ceremony first point self.can_complete_community_center(), # CC Ceremony second point self.received(Wallet.skull_key), # Skull Key obtained - self.has_rusty_key(), # Rusty key not expected + self.has_rusty_key(), # Rusty key obtained ] return Count(12, rules_worth_a_point) @@ -1265,9 +1284,21 @@ def has_year_three(self) -> StardewRule: def can_speak_dwarf(self) -> StardewRule: if self.options[options.Museumsanity] == options.Museumsanity.option_none: - return self.has([item.name for item in dwarf_scrolls]) + 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: + return self.can_reach_region(Region.museum) & self.can_find_museum_item(item) + + def can_donate_museum_items(self, number: int) -> StardewRule: + return self.can_reach_region(Region.museum) & self.can_find_museum_items(number) + + def can_donate_museum_artifacts(self, number: int) -> StardewRule: + return self.can_reach_region(Region.museum) & self.can_find_museum_artifacts(number) + + def can_donate_museum_minerals(self, number: int) -> StardewRule: + return self.can_reach_region(Region.museum) & self.can_find_museum_minerals(number) + def can_find_museum_item(self, item: MuseumItem) -> StardewRule: region_rule = self.can_reach_all_regions_except_one(item.locations) geodes_rule = And([self.can_open_geode(geode) for geode in item.geodes]) @@ -1280,9 +1311,15 @@ def can_find_museum_item(self, item: MuseumItem) -> StardewRule: def can_find_museum_artifacts(self, number: int) -> StardewRule: rules = [] - for donation in all_museum_items: - if donation in all_artifact_items: - rules.append(self.can_find_museum_item(donation)) + for artifact in all_museum_artifacts: + rules.append(self.can_find_museum_item(artifact)) + + return Count(number, rules) + + def can_find_museum_minerals(self, number: int) -> StardewRule: + rules = [] + for mineral in all_museum_minerals: + rules.append(self.can_find_museum_item(mineral)) return Count(number, rules) @@ -1294,7 +1331,7 @@ def can_find_museum_items(self, number: int) -> StardewRule: return Count(number, rules) def can_complete_museum(self) -> StardewRule: - rules = [self.can_mine_perfectly()] + rules = [self.can_reach_region(Region.museum), self.can_mine_perfectly()] if self.options[options.Museumsanity] != options.Museumsanity.option_none: rules.append(self.received("Traveling Merchant Metal Detector", 4)) @@ -1584,3 +1621,9 @@ def has_all_rarecrows(self) -> StardewRule: rules.append(self.received(f"Rarecrow #{rarecrow_number}")) return And(rules) + def can_ship(self, item: str = "") -> StardewRule: + shipping_bin_rule = self.has_building(Building.shipping_bin) + if item == "": + return shipping_bin_rule + return shipping_bin_rule & self.has(item) + diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index cd283dcbec3d..34ee1f807dd3 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -5,15 +5,14 @@ from worlds.generic import Rules as MultiWorldRules from . import options, locations from .bundles import Bundle -from .data.crops_data import crops_by_name from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, \ DeepWoodsEntrance, AlecEntrance, MagicEntrance -from .data.museum_data import all_museum_items, all_mineral_items, all_artifact_items, \ +from .data.museum_data import all_museum_items, all_museum_minerals, all_museum_artifacts, \ dwarf_scrolls, skeleton_front, \ - skeleton_middle, skeleton_back, all_museum_items_by_name + skeleton_middle, skeleton_back, all_museum_items_by_name, Artifact from .strings.region_names import Region from .mods.mod_data import ModNames -from .mods.logic import magic, skills, deepwoods +from .mods.logic import magic, deepwoods from .locations import LocationTags from .logic import StardewLogic, And, tool_upgrade_prices from .options import StardewOptions @@ -23,7 +22,6 @@ from .strings.craftable_names import Craftable from .strings.material_names import Material from .strings.metal_names import MetalBar -from .strings.spells import MagicSpell from .strings.skill_names import ModSkill, Skill from .strings.tool_names import Tool, ToolMaterial from .strings.villager_names import NPC, ModNPC @@ -357,25 +355,51 @@ def set_special_order_rules(all_location_names: List[str], logic: StardewLogic, MultiWorldRules.set_rule(multi_world.get_location(qi_order.name, player), order_rule.simplify()) +help_wanted_prefix = "Help Wanted:" +item_delivery = "Item Delivery" +gathering = "Gathering" +fishing = "Fishing" +slay_monsters = "Slay Monsters" + + def set_help_wanted_quests_rules(logic: StardewLogic, multi_world, player, world_options): - desired_number_help_wanted: int = world_options[options.HelpWantedLocations] // 7 - for i in range(0, desired_number_help_wanted): - prefix = "Help Wanted:" - delivery = "Item Delivery" - rule = logic.has_lived_months(i).simplify() - fishing_rule = rule & logic.can_fish() - slay_rule = rule & logic.can_do_combat_at_level("Basic") - item_delivery_index = (i * 4) + 1 - for j in range(item_delivery_index, item_delivery_index + 4): - location_name = f"{prefix} {delivery} {j}" - MultiWorldRules.set_rule(multi_world.get_location(location_name, player), rule) - - MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Gathering {i + 1}", player), - rule) - MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Fishing {i + 1}", player), - fishing_rule.simplify()) - MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Slay Monsters {i + 1}", player), - slay_rule.simplify()) + help_wanted_number = world_options[options.HelpWantedLocations] + for i in range(0, help_wanted_number): + set_number = i // 7 + month_rule = logic.has_lived_months(set_number).simplify() + quest_number = set_number + 1 + quest_number_in_set = i % 7 + if quest_number_in_set < 4: + quest_number = set_number * 4 + quest_number_in_set + 1 + set_help_wanted_delivery_rule(multi_world, player, month_rule, quest_number) + elif quest_number_in_set == 4: + set_help_wanted_fishing_rule(logic, multi_world, player, month_rule, quest_number) + elif quest_number_in_set == 5: + set_help_wanted_slay_monsters_rule(logic, multi_world, player, month_rule, quest_number) + elif quest_number_in_set == 6: + set_help_wanted_gathering_rule(multi_world, player, month_rule, quest_number) + + +def set_help_wanted_delivery_rule(multi_world, player, month_rule, quest_number): + location_name = f"{help_wanted_prefix} {item_delivery} {quest_number}" + MultiWorldRules.set_rule(multi_world.get_location(location_name, player), month_rule) + + +def set_help_wanted_gathering_rule(multi_world, player, month_rule, quest_number): + location_name = f"{help_wanted_prefix} {gathering} {quest_number}" + MultiWorldRules.set_rule(multi_world.get_location(location_name, player), month_rule) + + +def set_help_wanted_fishing_rule(logic: StardewLogic, multi_world, player, month_rule, quest_number): + location_name = f"{help_wanted_prefix} {fishing} {quest_number}" + fishing_rule = month_rule & logic.can_fish() + MultiWorldRules.set_rule(multi_world.get_location(location_name, player), fishing_rule.simplify()) + + +def set_help_wanted_slay_monsters_rule(logic: StardewLogic, multi_world, player, month_rule, quest_number): + location_name = f"{help_wanted_prefix} {slay_monsters} {quest_number}" + slay_rule = month_rule & logic.can_do_combat_at_level("Basic") + MultiWorldRules.set_rule(multi_world.get_location(location_name, player), slay_rule.simplify()) def set_fishsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int): @@ -406,7 +430,7 @@ def set_museum_individual_donations_rules(all_location_names, logic: StardewLogi if museum_location.name in all_location_names: donation_name = museum_location.name[len(museum_prefix):] required_detectors = counter * 5 // number_donations - rule = logic.has(donation_name) & logic.received("Traveling Merchant Metal Detector", required_detectors) + rule = logic.can_donate_museum_item(all_museum_items_by_name[donation_name]) & logic.received("Traveling Merchant Metal Detector", required_detectors) MultiWorldRules.set_rule(multi_world.get_location(museum_location.name, player), rule.simplify()) counter += 1 @@ -421,31 +445,31 @@ def set_museum_milestone_rule(logic: StardewLogic, multi_world: MultiWorld, muse metal_detector = "Traveling Merchant Metal Detector" rule = None if milestone_name.endswith(donations_suffix): - rule = get_museum_item_count_rule(logic, donations_suffix, milestone_name, all_museum_items) + rule = get_museum_item_count_rule(logic, donations_suffix, milestone_name, all_museum_items, logic.can_donate_museum_items) elif milestone_name.endswith(minerals_suffix): - rule = get_museum_item_count_rule(logic, minerals_suffix, milestone_name, all_mineral_items) + rule = get_museum_item_count_rule(logic, minerals_suffix, milestone_name, all_museum_minerals, logic.can_donate_museum_minerals) elif milestone_name.endswith(artifacts_suffix): - rule = get_museum_item_count_rule(logic, artifacts_suffix, milestone_name, all_artifact_items) + rule = get_museum_item_count_rule(logic, artifacts_suffix, milestone_name, all_museum_artifacts, logic.can_donate_museum_artifacts) elif milestone_name == "Dwarf Scrolls": - rule = logic.has([item.name for item in dwarf_scrolls]) & logic.received(metal_detector, 4) + rule = And([logic.can_donate_museum_item(item) for item in dwarf_scrolls]) & logic.received(metal_detector, 4) elif milestone_name == "Skeleton Front": - rule = logic.has([item.name for item in skeleton_front]) & logic.received(metal_detector, 4) + rule = And([logic.can_donate_museum_item(item) for item in skeleton_front]) & logic.received(metal_detector, 4) elif milestone_name == "Skeleton Middle": - rule = logic.has([item.name for item in skeleton_middle]) & logic.received(metal_detector, 4) + rule = And([logic.can_donate_museum_item(item) for item in skeleton_middle]) & logic.received(metal_detector, 4) elif milestone_name == "Skeleton Back": - rule = logic.has([item.name for item in skeleton_back]) & logic.received(metal_detector, 4) + rule = And([logic.can_donate_museum_item(item) for item in skeleton_back]) & logic.received(metal_detector, 4) elif milestone_name == "Ancient Seed": - rule = logic.has("Ancient Seed") & logic.received(metal_detector, 4) + rule = logic.can_donate_museum_item(Artifact.ancient_seed) & logic.received(metal_detector, 4) if rule is None: return MultiWorldRules.set_rule(multi_world.get_location(museum_milestone.name, player), rule.simplify()) -def get_museum_item_count_rule(logic: StardewLogic, suffix, milestone_name, accepted_items): +def get_museum_item_count_rule(logic: StardewLogic, suffix, milestone_name, accepted_items, donation_func): metal_detector = "Traveling Merchant Metal Detector" num = int(milestone_name[:milestone_name.index(suffix)]) required_detectors = (num - 1) * 5 // len(accepted_items) - rule = logic.has([item.name for item in accepted_items], num) & logic.received(metal_detector, required_detectors) + rule = donation_func(num) & logic.received(metal_detector, required_detectors) return rule diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py index be7105aa50b0..0847d8a63b95 100644 --- a/worlds/stardew_valley/test/TestRules.py +++ b/worlds/stardew_valley/test/TestRules.py @@ -2,10 +2,12 @@ from . import SVTestBase from .. import options +from ..locations import locations_by_tag, LocationTags, location_table from ..strings.animal_names import Animal from ..strings.animal_product_names import AnimalProduct from ..strings.artisan_good_names import ArtisanGood from ..strings.crop_names import Vegetable +from ..strings.entrance_names import Entrance from ..strings.food_names import Meal from ..strings.ingredient_names import Ingredient from ..strings.machine_names import Machine @@ -369,3 +371,136 @@ class TestRecipeLogic(SVTestBase): # self.assertTrue(logic.has(Machine.cheese_press)(self.multiworld.state)) # self.assertTrue(logic.has(ArtisanGood.cheese)(self.multiworld.state)) # self.assertTrue(logic.has(Meal.pizza)(self.multiworld.state)) + + +class TestDonationLogicAll(SVTestBase): + options = { + options.Museumsanity.internal_name: options.Museumsanity.option_all + } + + def test_cannot_make_any_donation_without_museum_access(self): + guild_item = "Adventurer's Guild" + swap_museum_and_guild(self.multiworld, self.player) + collect_all_except(self.multiworld, guild_item) + + for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: + self.assertFalse(self.world.logic.can_reach_location(donation.name)(self.multiworld.state)) + + self.multiworld.state.collect(self.world.create_item(guild_item), event=True) + + for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: + self.assertTrue(self.world.logic.can_reach_location(donation.name)(self.multiworld.state)) + + +class TestDonationLogicRandomized(SVTestBase): + options = { + options.Museumsanity.internal_name: options.Museumsanity.option_randomized + } + + def test_cannot_make_any_donation_without_museum_access(self): + guild_item = "Adventurer's Guild" + swap_museum_and_guild(self.multiworld, self.player) + collect_all_except(self.multiworld, guild_item) + donation_locations = [location for location in self.multiworld.get_locations() if not location.event and LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags] + + for donation in donation_locations: + self.assertFalse(self.world.logic.can_reach_location(donation.name)(self.multiworld.state)) + + self.multiworld.state.collect(self.world.create_item(guild_item), event=True) + + for donation in donation_locations: + self.assertTrue(self.world.logic.can_reach_location(donation.name)(self.multiworld.state)) + + +class TestDonationLogicMilestones(SVTestBase): + options = { + options.Museumsanity.internal_name: options.Museumsanity.option_milestones + } + + def test_cannot_make_any_donation_without_museum_access(self): + guild_item = "Adventurer's Guild" + swap_museum_and_guild(self.multiworld, self.player) + collect_all_except(self.multiworld, guild_item) + + for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: + self.assertFalse(self.world.logic.can_reach_location(donation.name)(self.multiworld.state)) + + self.multiworld.state.collect(self.world.create_item(guild_item), event=True) + + for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: + self.assertTrue(self.world.logic.can_reach_location(donation.name)(self.multiworld.state)) + + +def swap_museum_and_guild(multiworld, player): + museum_region = multiworld.get_region(Region.museum, player) + guild_region = multiworld.get_region(Region.adventurer_guild, player) + museum_entrance = multiworld.get_entrance(Entrance.town_to_museum, player) + guild_entrance = multiworld.get_entrance(Entrance.mountain_to_adventurer_guild, player) + museum_entrance.connect(guild_region) + guild_entrance.connect(museum_region) + + +def collect_all_except(multiworld, item_to_not_collect: str): + for item in multiworld.get_items(): + if item.name != item_to_not_collect: + multiworld.state.collect(item) + + +class TestFriendsanityDatingRules(SVTestBase): + options = { + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized_not_winter, + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, + options.FriendsanityHeartSize.internal_name: 3 + } + + def test_earning_dating_heart_requires_dating(self): + month_name = "Month End" + for i in range(12): + month_item = self.world.create_item(month_name) + self.multiworld.state.collect(month_item, event=True) + self.multiworld.state.collect(self.world.create_item("Beach Bridge"), event=False) + self.multiworld.state.collect(self.world.create_item("Progressive House"), event=False) + self.multiworld.state.collect(self.world.create_item("Adventurer's Guild"), event=False) + self.multiworld.state.collect(self.world.create_item("Galaxy Hammer"), event=False) + for i in range(3): + self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=False) + self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=False) + self.multiworld.state.collect(self.world.create_item("Progressive Barn"), event=False) + for i in range(10): + self.multiworld.state.collect(self.world.create_item("Foraging Level"), event=False) + self.multiworld.state.collect(self.world.create_item("Farming Level"), event=False) + self.multiworld.state.collect(self.world.create_item("Mining Level"), event=False) + self.multiworld.state.collect(self.world.create_item("Combat Level"), event=False) + self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False) + self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False) + + npc = "Abigail" + heart_name = f"{npc} <3" + step = 3 + + self.assert_can_reach_heart_up_to(npc, 3, step) + self.multiworld.state.collect(self.world.create_item(heart_name), event=False) + self.assert_can_reach_heart_up_to(npc, 6, step) + self.multiworld.state.collect(self.world.create_item(heart_name), event=False) + self.assert_can_reach_heart_up_to(npc, 8, step) + self.multiworld.state.collect(self.world.create_item(heart_name), event=False) + self.assert_can_reach_heart_up_to(npc, 10, step) + self.multiworld.state.collect(self.world.create_item(heart_name), event=False) + self.assert_can_reach_heart_up_to(npc, 14, step) + + def assert_can_reach_heart_up_to(self, npc: str, max_reachable: int, step: int): + prefix = "Friendsanity: " + suffix = " <3" + for i in range(1, max_reachable + 1): + if i % step != 0 and i != 14: + continue + location = f"{prefix}{npc} {i}{suffix}" + can_reach = self.world.logic.can_reach_location(location)(self.multiworld.state) + self.assertTrue(can_reach, f"Should be able to earn relationship up to {i} hearts") + for i in range(max_reachable + 1, 14 + 1): + if i % step != 0 and i != 14: + continue + location = f"{prefix}{npc} {i}{suffix}" + can_reach = self.world.logic.can_reach_location(location)(self.multiworld.state) + self.assertFalse(can_reach, f"Should not be able to earn relationship up to {i} hearts") + diff --git a/worlds/subnautica/docs/setup_en.md b/worlds/subnautica/docs/setup_en.md index e653f53145e1..83f4186bdfaf 100644 --- a/worlds/subnautica/docs/setup_en.md +++ b/worlds/subnautica/docs/setup_en.md @@ -36,7 +36,7 @@ Warning: Currently it is not checked whether a loaded savegame belongs to the mu The mod adds the following console commands: - `say` sends the text following it to Archipelago as a chat message. - - `!` is not an allowed character, use `/` in its place. For example, to use the [`!hint` command](/tutorial/Archipelago/commands/en#remote-commands), type `say /hint`. + - For example, to use the [`!hint` command](/tutorial/Archipelago/commands/en#remote-commands), type `say !hint`. - `silent` toggles Archipelago messages appearing. - `tracker` rotates through the possible settings for the in-game tracker that displays the closest uncollected location. - `deathlink` toggles death link. diff --git a/worlds/terraria/Rules.dsv b/worlds/terraria/Rules.dsv index a9f8e10dbd55..5f5551e465e1 100644 --- a/worlds/terraria/Rules.dsv +++ b/worlds/terraria/Rules.dsv @@ -295,7 +295,7 @@ Nuclear Rod; Calamity | Minions(1); Acid Rain Tier 2; Calamity | Location | Item; #Acid Rain Tier 1 & Aquatic Scourge; // The Twins -Soul of Sight; ; The Twins; +Soul of Sight; ; #The Twins; Steampunker; Npc; @mech_boss(1); Hammush; ; Truffle & @mech_boss(1); Rainbow Rod; ; Hardmode Anvil & Crystal Shard & Unicorn Horn & Pixie Dust & Soul of Light & Soul of Sight; @@ -428,7 +428,7 @@ Duke Fishron; Location | Item; Pumpkin Moon; ; Hardmode Anvil & Pumpkin & Ectoplasm & (@calamity | Hallowed Bar); Spooky Armor; ArmorMinions(4); Pumpkin Moon; Mourning Wood; Location | Item; Pumpkin Moon; -Necromantic Scroll; Minions(1); Mourning Wood; +Necromantic Scroll; Minions(1); #Mourning Wood; Papyrus Scarab; Minions(1); Tinkerer's Workshop & Hercules Beetle & Necromantic Scroll; Pumpking; Location | Item; Pumpkin Moon; The Horseman's Blade; ; #Pumpking; diff --git a/worlds/terraria/__init__.py b/worlds/terraria/__init__.py index a56f47608b87..a8c823bcb8ac 100644 --- a/worlds/terraria/__init__.py +++ b/worlds/terraria/__init__.py @@ -338,5 +338,7 @@ def check(state: CollectionState, location=location): def fill_slot_data(self) -> Dict[str, object]: return { "goal": list(self.goal_locations), + "achievements": self.multiworld.achievements[self.player].value, + "fill_extra_checks_with": self.multiworld.fill_extra_checks_with[self.player].value, "deathlink": bool(self.multiworld.death_link[self.player]), } diff --git a/worlds/terraria/docs/setup_en.md b/worlds/terraria/docs/setup_en.md index 008d069d249f..d0833b748452 100644 --- a/worlds/terraria/docs/setup_en.md +++ b/worlds/terraria/docs/setup_en.md @@ -16,7 +16,9 @@ highly recommended to use utility mods and features to speed up gameplay, such a - Ore Excavator - Magic Storage - Alchemist NPC Lite + - (May be used to break progression) - Reduced Grinding +- Upgraded Research ## Configuring your YAML File diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 5f4d230688bd..8b111849442c 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -60,7 +60,7 @@ class BossRando(Toggle): class BossScaling(DefaultOnToggle): - "When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Reccomended)" + "When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Recommended)" display_name = "Scale Random Boss Stats" diff --git a/worlds/tloz/Rom.py b/worlds/tloz/Rom.py index 16806a6476c0..58aa38805f73 100644 --- a/worlds/tloz/Rom.py +++ b/worlds/tloz/Rom.py @@ -1,10 +1,11 @@ +import hashlib import zlib import os import Utils from worlds.Files import APDeltaPatch -NA10CHECKSUM = 'D7AE93DF' +NA10CHECKSUM = '337bd6f1a1163df31bf2633665589ab0' ROM_PLAYER_LIMIT = 65535 ROM_NAME = 0x10 bit_positions = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80] @@ -44,7 +45,6 @@ class TLoZDeltaPatch(APDeltaPatch): - checksum = NA10CHECKSUM hash = NA10CHECKSUM game = "The Legend of Zelda" patch_file_ending = ".aptloz" @@ -61,9 +61,10 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: file_name = get_base_rom_path(file_name) base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb"))) - basechecksum = str(hex(zlib.crc32(base_rom_bytes))).upper()[2:] - if NA10CHECKSUM != basechecksum: - raise Exception('Supplied Base Rom does not match known CRC-32 for NA (1.0) release. ' + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if NA10CHECKSUM != basemd5.hexdigest(): + raise Exception('Supplied Base Rom does not match known MD5 for NA (1.0) release. ' 'Get the correct game and version, then dump it') get_base_rom_bytes.base_rom_bytes = base_rom_bytes return base_rom_bytes diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 2227a7c5e2ed..20ab003ead61 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -192,7 +192,7 @@ def apply_base_patch(self, rom): # Remove map/compass check so they're always on # Removing a bit from the boss roars flags, so we can have more dungeon items. This allows us to # go past 0x1F items for dungeon items. - base_patch = get_data(__name__, os.path.join(os.path.dirname(__file__), "z1_base_patch.bsdiff4")) + base_patch = get_data(__name__, "z1_base_patch.bsdiff4") rom_data = bsdiff4.patch(rom.read(), base_patch) rom_data = bytearray(rom_data) # Set every item to the new nothing value, but keep room flags. Type 2 boss roars should diff --git a/worlds/undertale/Items.py b/worlds/undertale/Items.py index 50811bd1dec6..033102664c82 100644 --- a/worlds/undertale/Items.py +++ b/worlds/undertale/Items.py @@ -88,10 +88,10 @@ class UndertaleItem(Item): "ATK Up": ItemData(77065, ItemClassification.useful), "DEF Up": ItemData(77066, ItemClassification.useful), "HP Up": ItemData(77067, ItemClassification.useful), - "FIGHT": ItemData(77077, ItemClassification.progression), - "ACT": ItemData(77078, ItemClassification.progression), - "ITEM": ItemData(77079, ItemClassification.progression), - "MERCY": ItemData(77080, ItemClassification.progression), + "FIGHT": ItemData(77077, ItemClassification.useful), + "ACT": ItemData(77078, ItemClassification.useful), + "ITEM": ItemData(77079, ItemClassification.useful), + "MERCY": ItemData(77080, ItemClassification.useful), "Ruins Key": ItemData(77081, ItemClassification.progression), "Snowdin Key": ItemData(77082, ItemClassification.progression), "Waterfall Key": ItemData(77083, ItemClassification.progression), diff --git a/worlds/undertale/Locations.py b/worlds/undertale/Locations.py index c000a46de320..2f7de44512fa 100644 --- a/worlds/undertale/Locations.py +++ b/worlds/undertale/Locations.py @@ -77,7 +77,6 @@ def __init__(self, player: int, name: str, address: typing.Optional[int], parent "True Lab Plot": AdvData(79063, "Hotland"), "Left New Home Key": AdvData(79064, "New Home"), "Right New Home Key": AdvData(79065, "New Home"), - "Starting Key": AdvData(79067, "Hub"), "LOVE 2": AdvData(79902, "???"), "LOVE 3": AdvData(79903, "???"), "LOVE 4": AdvData(79904, "???"), diff --git a/worlds/undertale/Options.py b/worlds/undertale/Options.py index d4fd1488f920..146a7838f766 100644 --- a/worlds/undertale/Options.py +++ b/worlds/undertale/Options.py @@ -12,6 +12,17 @@ class RouteRequired(Choice): default = 0 +class StartingArea(Choice): + """Which area to start with access to.""" + display_name = "Starting Area" + option_ruins = 0 + option_snowdin = 1 + option_waterfall = 2 + option_hotland = 3 + option_core = 4 + default = 0 + + class IncludeTemy(Toggle): """Adds Temmy Armor to the item pool.""" display_name = "Include Temy Armor" @@ -77,6 +88,7 @@ class RandoBattleOptions(Toggle): undertale_options: typing.Dict[str, type(Option)] = { "route_required": RouteRequired, + "starting_area": StartingArea, "key_hunt": KeyHunt, "key_pieces": KeyPieces, "rando_love": RandomizeLove, diff --git a/worlds/undertale/Rules.py b/worlds/undertale/Rules.py index eb99e8ca3534..02c21f53f73c 100644 --- a/worlds/undertale/Rules.py +++ b/worlds/undertale/Rules.py @@ -76,21 +76,6 @@ def set_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_entrance("Waterfall Hub", player), lambda state: state.has("Waterfall Key", player)) set_rule(multiworld.get_entrance("Hotland Hub", player), lambda state: state.has("Hotland Key", player)) set_rule(multiworld.get_entrance("Core Hub", player), lambda state: state.has("Core Key", player)) - if _undertale_is_route(multiworld.state, player, 1): - add_rule(multiworld.get_entrance("Snowdin Hub", player), lambda state: state.has("ACT", player) and state.has("MERCY", player)) - add_rule(multiworld.get_entrance("Waterfall Hub", player), lambda state: state.has("ACT", player) and state.has("MERCY", player)) - add_rule(multiworld.get_entrance("Hotland Hub", player), lambda state: state.has("ACT", player) and state.has("MERCY", player)) - add_rule(multiworld.get_entrance("Core Hub", player), lambda state: state.has("ACT", player) and state.has("MERCY", player)) - if _undertale_is_route(multiworld.state, player, 2) or _undertale_is_route(multiworld.state, player, 3): - add_rule(multiworld.get_entrance("Snowdin Hub", player), lambda state: state.has("FIGHT", player)) - add_rule(multiworld.get_entrance("Waterfall Hub", player), lambda state: state.has("FIGHT", player)) - add_rule(multiworld.get_entrance("Hotland Hub", player), lambda state: state.has("FIGHT", player)) - add_rule(multiworld.get_entrance("Core Hub", player), lambda state: state.has("FIGHT", player)) - if _undertale_is_route(multiworld.state, player, 0): - add_rule(multiworld.get_entrance("Snowdin Hub", player), lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player))) - add_rule(multiworld.get_entrance("Waterfall Hub", player), lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player))) - add_rule(multiworld.get_entrance("Hotland Hub", player), lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player))) - add_rule(multiworld.get_entrance("Core Hub", player), lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player))) set_rule(multiworld.get_entrance("Core Exit", player), lambda state: _undertale_has_plot(state, player, "Mettaton Plush")) set_rule(multiworld.get_entrance("New Home Exit", player), @@ -153,7 +138,7 @@ def set_rules(multiworld: MultiWorld, player: int): if multiworld.rando_stats[player]: set_rule(multiworld.get_location(("ATK "+str(maxlv)), player), lambda state: False) set_rule(multiworld.get_location(("HP "+str(maxlv)), player), lambda state: False) - if maxlv == 9 or maxlv == 13 or maxlv == 17: + if maxlv in {5, 9, 13, 17}: set_rule(multiworld.get_location(("DEF "+str(maxlv)), player), lambda state: False) maxlv = 1 while maxlv < 20: @@ -165,7 +150,7 @@ def set_rules(multiworld: MultiWorld, player: int): lambda state: (state.can_reach("Old Home", "Region", player)), combine="or") add_rule(multiworld.get_location(("HP "+str(maxlv)), player), lambda state: (state.can_reach("Old Home", "Region", player)), combine="or") - if maxlv == 9 or maxlv == 13 or maxlv == 17: + if maxlv == 5 or maxlv == 9 or maxlv == 13 or maxlv == 17: add_rule(multiworld.get_location(("DEF "+str(maxlv)), player), lambda state: (state.can_reach("Old Home", "Region", player)), combine="or") elif curarea == "Snowdin Town": @@ -173,7 +158,7 @@ def set_rules(multiworld: MultiWorld, player: int): lambda state: (state.can_reach("Snowdin Town", "Region", player)), combine="or") add_rule(multiworld.get_location(("HP "+str(maxlv)), player), lambda state: (state.can_reach("Snowdin Town", "Region", player)), combine="or") - if maxlv == 9 or maxlv == 13 or maxlv == 17: + if maxlv == 5 or maxlv == 9 or maxlv == 13 or maxlv == 17: add_rule(multiworld.get_location(("DEF "+str(maxlv)), player), lambda state: (state.can_reach("Snowdin Town", "Region", player)), combine="or") elif curarea == "Waterfall": @@ -181,7 +166,7 @@ def set_rules(multiworld: MultiWorld, player: int): lambda state: (state.can_reach("Waterfall", "Region", player)), combine="or") add_rule(multiworld.get_location(("HP "+str(maxlv)), player), lambda state: (state.can_reach("Waterfall", "Region", player)), combine="or") - if maxlv == 9 or maxlv == 13 or maxlv == 17: + if maxlv == 5 or maxlv == 9 or maxlv == 13 or maxlv == 17: add_rule(multiworld.get_location(("DEF "+str(maxlv)), player), lambda state: (state.can_reach("Waterfall", "Region", player)), combine="or") elif curarea == "News Show": @@ -189,7 +174,7 @@ def set_rules(multiworld: MultiWorld, player: int): lambda state: (state.can_reach("News Show", "Region", player)), combine="or") add_rule(multiworld.get_location(("HP "+str(maxlv)), player), lambda state: (state.can_reach("News Show", "Region", player)), combine="or") - if maxlv == 9 or maxlv == 13 or maxlv == 17: + if maxlv == 5 or maxlv == 9 or maxlv == 13 or maxlv == 17: add_rule(multiworld.get_location(("DEF "+str(maxlv)), player), lambda state: (state.can_reach("News Show", "Region", player)), combine="or") elif curarea == "Core": @@ -197,7 +182,7 @@ def set_rules(multiworld: MultiWorld, player: int): lambda state: (state.can_reach("Core Exit", "Entrance", player)), combine="or") add_rule(multiworld.get_location(("HP "+str(maxlv)), player), lambda state: (state.can_reach("Core Exit", "Entrance", player)), combine="or") - if maxlv == 9 or maxlv == 13 or maxlv == 17: + if maxlv == 5 or maxlv == 9 or maxlv == 13 or maxlv == 17: add_rule(multiworld.get_location(("DEF "+str(maxlv)), player), lambda state: (state.can_reach("Core Exit", "Entrance", player)), combine="or") elif curarea == "Sans": @@ -205,13 +190,13 @@ def set_rules(multiworld: MultiWorld, player: int): lambda state: (state.can_reach("New Home Exit", "Entrance", player)), combine="or") add_rule(multiworld.get_location(("HP "+str(maxlv)), player), lambda state: (state.can_reach("New Home Exit", "Entrance", player)), combine="or") - if maxlv == 9 or maxlv == 13 or maxlv == 17: + if maxlv == 5 or maxlv == 9 or maxlv == 13 or maxlv == 17: add_rule(multiworld.get_location(("DEF "+str(maxlv)), player), lambda state: (state.can_reach("New Home Exit", "Entrance", player)), combine="or") if multiworld.rando_love[player]: if curarea == "Old Home": add_rule(multiworld.get_location(("LOVE "+str(maxlv)), player), - lambda state: ( state.can_reach("Old Home", "Region", player)), combine="or") + lambda state: (state.can_reach("Old Home", "Region", player)), combine="or") elif curarea == "Snowdin Town": add_rule(multiworld.get_location(("LOVE "+str(maxlv)), player), lambda state: (state.can_reach("Snowdin Town", "Region", player)), combine="or") @@ -247,46 +232,8 @@ def set_rules(multiworld: MultiWorld, player: int): curarea = "Sans" maxlv = 1 exp = 99999 - set_rule(multiworld.get_entrance("??? Exit", player), lambda state: state.has("FIGHT", player)) set_rule(multiworld.get_location("Snowman", player), lambda state: state.can_reach("Snowdin Town", "Region", player)) - add_item_rule(multiworld.get_location("Starting Key", player), lambda item: item.name == "Ruins Key" or - item.name == "Snowdin Key" or - item.name == "Waterfall Key" or - item.name == "Hotland Key") - if _undertale_is_route(multiworld.state, player, 1): - set_rule(multiworld.get_location("Donut Sale", player), - lambda state: state.has("ACT", player) and state.has("MERCY", player)) - set_rule(multiworld.get_location("Cider Sale", player), - lambda state: state.has("ACT", player) and state.has("MERCY", player)) - set_rule(multiworld.get_location("Ribbon Cracks", player), - lambda state: state.has("ACT", player) and state.has("MERCY", player)) - set_rule(multiworld.get_location("Toy Knife Edge", player), - lambda state: state.has("ACT", player) and state.has("MERCY", player)) - set_rule(multiworld.get_location("B.Scotch Pie Given", player), - lambda state: state.has("ACT", player) and state.has("MERCY", player)) - if _undertale_is_route(multiworld.state, player, 2) or _undertale_is_route(multiworld.state, player, 3): - set_rule(multiworld.get_location("Donut Sale", player), - lambda state: state.has("FIGHT", player)) - set_rule(multiworld.get_location("Cider Sale", player), - lambda state: state.has("FIGHT", player)) - set_rule(multiworld.get_location("Ribbon Cracks", player), - lambda state: state.has("FIGHT", player)) - set_rule(multiworld.get_location("Toy Knife Edge", player), - lambda state: state.has("FIGHT", player)) - set_rule(multiworld.get_location("B.Scotch Pie Given", player), - lambda state: state.has("FIGHT", player)) - if _undertale_is_route(multiworld.state, player, 0): - set_rule(multiworld.get_location("Donut Sale", player), - lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player))) - set_rule(multiworld.get_location("Cider Sale", player), - lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player))) - set_rule(multiworld.get_location("Ribbon Cracks", player), - lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player))) - set_rule(multiworld.get_location("Toy Knife Edge", player), - lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player))) - set_rule(multiworld.get_location("B.Scotch Pie Given", player), - lambda state: ((state.has("ACT", player) and state.has("MERCY", player)) or state.has("FIGHT", player))) set_rule(multiworld.get_location("Mettaton Plot", player), lambda state: state.can_reach("Core Exit", "Entrance", player)) set_rule(multiworld.get_location("Bunny 1", player), @@ -357,7 +304,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) and state.has("FIGHT", player) + completion_requirements = lambda state: state.can_reach("New Home Exit", "Entrance", player) if _undertale_is_route(multiworld.state, player, 1): completion_requirements = lambda state: state.can_reach("True Lab", "Region", player) diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py index 717b5c0f78fd..3a34a162c478 100644 --- a/worlds/undertale/__init__.py +++ b/worlds/undertale/__init__.py @@ -52,7 +52,7 @@ class UndertaleWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {name: data.id for name, data in advancement_table.items()} - data_version = 6 + data_version = 7 def _get_undertale_data(self): return { @@ -63,6 +63,7 @@ def _get_undertale_data(self): "client_version": self.required_client_version, "race": self.multiworld.is_race, "route": self.multiworld.route_required[self.player].current_key, + "starting_area": self.multiworld.starting_area[self.player].current_key, "temy_armor_include": bool(self.multiworld.temy_include[self.player].value), "only_flakes": bool(self.multiworld.only_flakes[self.player].value), "no_equips": bool(self.multiworld.no_equips[self.player].value), @@ -153,6 +154,10 @@ def create_items(self): if item == "Heart Locket" else item for item in itempool] if self.multiworld.only_flakes[self.player]: itempool = [item for item in itempool if item not in non_key_items] + + starting_key = self.multiworld.starting_area[self.player].current_key.title() + " Key" + itempool.remove(starting_key) + self.multiworld.push_precollected(self.create_item(starting_key)) # Choose locations to automatically exclude based on settings exclusion_pool = set() exclusion_pool.update(exclusion_table[self.multiworld.route_required[self.player].current_key]) diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt index a55c22441e28..b373c7417c24 100644 --- a/worlds/witness/WitnessLogicExpert.txt +++ b/worlds/witness/WitnessLogicExpert.txt @@ -14,7 +14,7 @@ Tutorial (Tutorial) - Outside Tutorial - True: 158005 - 0x0A3B5 (Back Left) - True - Dots & Full Dots 158006 - 0x0A3B2 (Back Right) - True - Dots & Full Dots 158007 - 0x03629 (Gate Open) - 0x002C2 - Symmetry & Dots -158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - False +158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - True 158009 - 0x0C335 (Pillar) - True - Triangles 158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots 159512 - 0x33530 (Cloud EP) - True - True diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index ccbacb4180cb..f05202751fdb 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -234,6 +234,11 @@ def fill_slot_data(self) -> dict: return slot_data def create_item(self, item_name: str) -> Item: + # If the player's plando options are malformed, the item_name parameter could be a dictionary containing the + # name of the item, rather than the item itself. This is a workaround to prevent a crash. + if type(item_name) is dict: + item_name = list(item_name.keys())[0] + # this conditional is purely for unit tests, which need to be able to create an item before generate_early item_data: ItemData if hasattr(self, 'items') and self.items and item_name in self.items.item_data: diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index d108e8bfe42b..5d8bd5d3702c 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -10,7 +10,7 @@ "You can do it!", "I believe in you!", "The person playing is cute. <3", - "dash dot, dash dash dash, dash, dot dot dot dot, dot dot, dash dot, dash dash dot", + "dash dot, dash dash dash,\ndash, dot dot dot dot, dot dot,\ndash dot, dash dash dot", "When you think about it, there are actually a lot of bubbles in a stream.", "Never gonna give you up\nNever gonna let you down\nNever gonna run around and desert you", "Thanks to the Archipelago developers for making this possible.", diff --git a/worlds/witness/items.py b/worlds/witness/items.py index b8e439d78e5f..82c79047f3fb 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -152,7 +152,7 @@ def get_mandatory_items(self) -> Dict[str, int]: """ Returns the list of items that must be in the pool for the game to successfully generate. """ - return self._mandatory_items + return self._mandatory_items.copy() def get_filler_items(self, quantity: int) -> Dict[str, int]: """ @@ -219,17 +219,21 @@ def get_early_items(self) -> List[str]: # regardless of whether or not they actually wind up being manually placed. for plando_setting in self._world.plando_items[self._player_id]: if plando_setting.get("from_pool", True): - for item_setting_key in (key for key in ["item", "items"] if key in plando_setting): + for item_setting_key in [key for key in ["item", "items"] if key in plando_setting]: if type(plando_setting[item_setting_key]) is str: - output.remove(plando_setting[item_setting_key]) + output -= {plando_setting[item_setting_key]} elif type(plando_setting[item_setting_key]) is dict: output -= {item for item, weight in plando_setting[item_setting_key].items() if weight} else: # Assume this is some other kind of iterable. - output -= plando_setting[item_setting_key] + for inner_item in plando_setting[item_setting_key]: + if type(inner_item) is str: + output -= {inner_item} + elif type(inner_item) is dict: + output -= {item for item, weight in inner_item.items() if weight} # Sort the output for consistency across versions if the implementation changes but the logic does not. - return sorted(output) + return sorted(list(output)) def get_door_ids_in_pool(self) -> List[int]: """ diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 2ea7ffdea5cd..7c927c10eb92 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -10,6 +10,7 @@ from BaseClasses import ItemClassification, LocationProgressType, \ MultiWorld, Item, CollectionState, Entrance, Tutorial +from .config import detect_test from .logic import cs_to_zz_locs from .region import ZillionLocation, ZillionRegion from .options import ZillionStartChar, zillion_options, validate @@ -145,8 +146,7 @@ def generate_early(self) -> None: self._item_counts = item_counts - import __main__ - rom_dir_name = "" if "test" in __main__.__file__ else os.path.dirname(get_base_rom_path()) + rom_dir_name = "" if detect_test() else os.path.dirname(get_base_rom_path()) with redirect_stdout(self.lsi): # type: ignore self.zz_system.make_patcher(rom_dir_name) self.zz_system.make_randomizer(zz_op) diff --git a/worlds/zillion/config.py b/worlds/zillion/config.py index ca02f9a99f41..db61d0c45347 100644 --- a/worlds/zillion/config.py +++ b/worlds/zillion/config.py @@ -2,3 +2,20 @@ base_id = 8675309 zillion_map = os.path.join(os.path.dirname(__file__), "empty-zillion-map-row-col-labels-281.png") + + +def detect_test() -> bool: + """ + Parts of generation that are in unit tests need the rom. + This is to detect whether we are running unit tests + so we can work around the need for the rom. + """ + import __main__ + try: + if "test" in __main__.__file__: + return True + except AttributeError: + # In some environments, __main__ doesn't have __file__ + # We'll assume that's not unit tests. + pass + return False
+ Starting Resources
+ Weapon & Armor Upgrades
+ Base
 
+ Infantry + Vehicles +
- Vehicles -
- Starships -
+ Starships +
- Dominion -
+ Mercenaries
- Lab Upgrades + + General Upgrades
+ Protoss Units