diff --git a/UndertaleClient.py b/UndertaleClient.py index 415d7e7f21a3..dfacee148abc 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -29,7 +29,7 @@ def _cmd_resync(self): def _cmd_patch(self): """Patch the game. Only use this command if /auto_patch fails.""" if isinstance(self.ctx, UndertaleContext): - os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) + os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True) self.ctx.patch_game() self.output("Patched.") @@ -43,7 +43,7 @@ def _cmd_savepath(self, directory: str): def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None): """Patch the game automatically.""" if isinstance(self.ctx, UndertaleContext): - os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) + os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True) tempInstall = steaminstall if not os.path.isfile(os.path.join(tempInstall, "data.win")): tempInstall = None @@ -62,7 +62,7 @@ def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None): for file_name in os.listdir(tempInstall): if file_name != "steam_api.dll": shutil.copy(os.path.join(tempInstall, file_name), - os.path.join(os.getcwd(), "Undertale", file_name)) + Utils.user_path("Undertale", file_name)) self.ctx.patch_game() self.output("Patching successful!") @@ -111,12 +111,12 @@ def __init__(self, server_address, password): self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE") def patch_game(self): - with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "rb") as f: + with open(Utils.user_path("Undertale", "data.win"), "rb") as f: patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff")) - with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "wb") as f: + with open(Utils.user_path("Undertale", "data.win"), "wb") as f: f.write(patchedFile) - os.makedirs(name=os.path.join(os.getcwd(), "Undertale", "Custom Sprites"), exist_ok=True) - with open(os.path.expandvars(os.path.join(os.getcwd(), "Undertale", "Custom Sprites", + os.makedirs(name=Utils.user_path("Undertale", "Custom Sprites"), exist_ok=True) + with open(os.path.expandvars(Utils.user_path("Undertale", "Custom Sprites", "Which Character.txt")), "w") as f: f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only " "line other than this one.\n", "frisk"]) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 9f70165b61e5..ccffc40b384d 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -325,10 +325,12 @@ def _done(self, task: asyncio.Future): def run(self): while 1: next_room = rooms_to_run.get(block=True, timeout=None) + gc.collect(0) task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop) self._tasks.append(task) task.add_done_callback(self._done) logging.info(f"Starting room {next_room} on {name}.") + del task # delete reference to task object starter = Starter() starter.daemon = True diff --git a/kvui.py b/kvui.py index 500203a8818f..a63d636960a7 100644 --- a/kvui.py +++ b/kvui.py @@ -595,8 +595,8 @@ def command_button_action(self, button): "!help for server commands.") def connect_button_action(self, button): + self.ctx.username = None if self.ctx.server: - self.ctx.username = None async_start(self.ctx.disconnect()) else: async_start(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", ""))) @@ -836,6 +836,10 @@ def _handle_color(self, node: JSONMessagePart): return self._handle_text(node) def _handle_text(self, node: JSONMessagePart): + # All other text goes through _handle_color, and we don't want to escape markup twice, + # or mess up text that already has intentional markup applied to it + if node.get("type", "text") == "text": + node["text"] = escape_markup(node["text"]) for ref in node.get("refs", []): node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]" self.ref_count += 1 diff --git a/setup.py b/setup.py index 85c0f9f7ff13..cb4d1a7511b6 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it try: - requirement = 'cx-Freeze==7.0.0' + requirement = 'cx-Freeze==7.2.0' import pkg_resources try: pkg_resources.require(requirement) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 0ba0f5b9a5a4..c6aeaa357799 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -292,6 +292,9 @@ # See above comment "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations", "Murder on the Owl Express"], + + # was causing test failures + "Time Rift - Balcony": ["Alpine Free Roam"], } diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 71f74b17d7ed..b0513c433289 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -863,6 +863,8 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): if world.is_dlc1(): for entrance in regions["Time Rift - Balcony"].entrances: add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale")) + reg_act_connection(world, world.multiworld.get_entrance("The Arctic Cruise - Finale", + world.player).connected_region, entrance) for entrance in regions["Time Rift - Deep Sea"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) @@ -939,6 +941,7 @@ def set_default_rift_rules(world: "HatInTimeWorld"): if world.is_dlc1(): for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances: add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale")) + reg_act_connection(world, "Rock the Boat", entrance.name) for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 171c82f9b226..67684a6f3ced 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -488,7 +488,7 @@ def global_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) set_rule(multiworld.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player))) set_rule(multiworld.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) - set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10)) + set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10) and can_bomb_or_bonk(state, player)) set_rule(multiworld.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player) or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player)) set_rule(multiworld.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player)) diff --git a/worlds/bomb_rush_cyberfunk/Locations.py b/worlds/bomb_rush_cyberfunk/Locations.py index 863e2ad020c0..7ea959019067 100644 --- a/worlds/bomb_rush_cyberfunk/Locations.py +++ b/worlds/bomb_rush_cyberfunk/Locations.py @@ -762,7 +762,7 @@ class EventDict(TypedDict): 'game_id': "graf385"}, {'name': "Tagged 389 Graffiti Spots", 'stage': Stages.Misc, - 'game_id': "graf379"}, + 'game_id': "graf389"}, ] diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 4d6771a7350d..970063d58542 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -3265,7 +3265,6 @@ door: Traveled Entrance Color Hallways: door: Color Hallways Entrance - warp: True panels: Achievement: id: Countdown Panels/Panel_traveled_traveled diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 6c8c925138aa..065308628b9f 100644 Binary files a/worlds/lingo/data/generated.dat and b/worlds/lingo/data/generated.dat differ diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index 9834f04f9de7..9773f22d9196 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -159,7 +159,7 @@ def create_regions(world: "LingoWorld") -> None: RoomAndDoor("Pilgrim Antechamber", "Sun Painting"), EntranceType.PAINTING, False, world) if early_color_hallways: - connect_entrance(regions, regions["Starting Room"], regions["Outside The Undeterred"], "Early Color Hallways", + connect_entrance(regions, regions["Starting Room"], regions["Color Hallways"], "Early Color Hallways", None, EntranceType.PAINTING, False, world) if painting_shuffle: diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py index f93441baeac1..5b2aaa1ffcd0 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -558,6 +558,10 @@ def get_location(location: str): get_location("NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON"), lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) ) + set_rule( + get_location("NPC_GIFT_RECEIVED_COIN_CASE"), + lambda state: state.has("EVENT_BUY_HARBOR_MAIL", world.player) + ) # Route 117 set_rule( @@ -1638,10 +1642,6 @@ def get_location(location: str): get_location("NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON"), lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) and state.has("EVENT_TURN_OFF_GENERATOR", world.player) ) - set_rule( - get_location("NPC_GIFT_RECEIVED_COIN_CASE"), - lambda state: state.has("EVENT_BUY_HARBOR_MAIL", world.player) - ) # Fallarbor Town set_rule( diff --git a/worlds/raft/Options.py b/worlds/raft/Options.py index 696d4dbab477..efe460b50353 100644 --- a/worlds/raft/Options.py +++ b/worlds/raft/Options.py @@ -1,4 +1,5 @@ -from Options import Range, Toggle, DefaultOnToggle, Choice, DeathLink +from dataclasses import dataclass +from Options import Range, Toggle, DefaultOnToggle, Choice, DeathLink, PerGameCommonOptions class MinimumResourcePackAmount(Range): """The minimum amount of resources available in a resource pack""" @@ -47,6 +48,8 @@ class IslandFrequencyLocations(Choice): option_progressive = 4 option_anywhere = 5 default = 2 + def is_filling_frequencies_in_world(self): + return self.value <= self.option_random_on_island_random_order class IslandGenerationDistance(Choice): """Sets how far away islands spawn from you when you input their coordinates into the Receiver.""" @@ -76,16 +79,16 @@ class PaddleboardMode(Toggle): """Sets later story islands to be in logic without an Engine or Steering Wheel. May require lots of paddling.""" display_name = "Paddleboard Mode" -raft_options = { - "minimum_resource_pack_amount": MinimumResourcePackAmount, - "maximum_resource_pack_amount": MaximumResourcePackAmount, - "duplicate_items": DuplicateItems, - "filler_item_types": FillerItemTypes, - "island_frequency_locations": IslandFrequencyLocations, - "island_generation_distance": IslandGenerationDistance, - "expensive_research": ExpensiveResearch, - "progressive_items": ProgressiveItems, - "big_island_early_crafting": BigIslandEarlyCrafting, - "paddleboard_mode": PaddleboardMode, - "death_link": DeathLink -} +@dataclass +class RaftOptions(PerGameCommonOptions): + minimum_resource_pack_amount: MinimumResourcePackAmount + maximum_resource_pack_amount: MaximumResourcePackAmount + duplicate_items: DuplicateItems + filler_item_types: FillerItemTypes + island_frequency_locations: IslandFrequencyLocations + island_generation_distance: IslandGenerationDistance + expensive_research: ExpensiveResearch + progressive_items: ProgressiveItems + big_island_early_crafting: BigIslandEarlyCrafting + paddleboard_mode: PaddleboardMode + death_link: DeathLink diff --git a/worlds/raft/Rules.py b/worlds/raft/Rules.py index e84068a6f584..b6bd49c187cd 100644 --- a/worlds/raft/Rules.py +++ b/worlds/raft/Rules.py @@ -5,10 +5,10 @@ class RaftLogic(LogicMixin): def raft_paddleboard_mode_enabled(self, player): - return self.multiworld.paddleboard_mode[player].value + return bool(self.multiworld.worlds[player].options.paddleboard_mode) def raft_big_islands_available(self, player): - return self.multiworld.big_island_early_crafting[player].value or self.raft_can_access_radio_tower(player) + return bool(self.multiworld.worlds[player].options.big_island_early_crafting) or self.raft_can_access_radio_tower(player) def raft_can_smelt_items(self, player): return self.has("Smelter", player) diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index e96cd4471268..71d5d1c7e44b 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -6,7 +6,7 @@ from .Regions import create_regions, getConnectionName from .Rules import set_rules -from .Options import raft_options +from .Options import RaftOptions from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, Tutorial from ..AutoWorld import World, WebWorld @@ -37,16 +37,17 @@ class RaftWorld(World): lastItemId = max(filter(lambda val: val is not None, item_name_to_id.values())) location_name_to_id = locations_lookup_name_to_id - option_definitions = raft_options + options_dataclass = RaftOptions + options: RaftOptions required_client_version = (0, 3, 4) def create_items(self): - minRPSpecified = self.multiworld.minimum_resource_pack_amount[self.player].value - maxRPSpecified = self.multiworld.maximum_resource_pack_amount[self.player].value + minRPSpecified = self.options.minimum_resource_pack_amount.value + maxRPSpecified = self.options.maximum_resource_pack_amount.value minimumResourcePackAmount = min(minRPSpecified, maxRPSpecified) maximumResourcePackAmount = max(minRPSpecified, maxRPSpecified) - isFillingFrequencies = self.multiworld.island_frequency_locations[self.player].value <= 3 + isFillingFrequencies = self.options.island_frequency_locations.is_filling_frequencies_in_world() # Generate item pool pool = [] frequencyItems = [] @@ -64,20 +65,20 @@ def create_items(self): extraItemNamePool = [] extras = len(location_table) - len(item_table) - 1 # Victory takes up 1 unaccounted-for slot if extras > 0: - if (self.multiworld.filler_item_types[self.player].value != 1): # Use resource packs + if (self.options.filler_item_types != self.options.filler_item_types.option_duplicates): # Use resource packs for packItem in resourcePackItems: for i in range(minimumResourcePackAmount, maximumResourcePackAmount + 1): extraItemNamePool.append(createResourcePackName(i, packItem)) - if self.multiworld.filler_item_types[self.player].value != 0: # Use duplicate items + if self.options.filler_item_types != self.options.filler_item_types.option_resource_packs: # Use duplicate items dupeItemPool = item_table.copy() # Remove frequencies if necessary - if self.multiworld.island_frequency_locations[self.player].value != 5: # Not completely random locations + if self.options.island_frequency_locations != self.options.island_frequency_locations.option_anywhere: # Not completely random locations # If we let frequencies stay in with progressive-frequencies, the progressive-frequency item # will be included 7 times. This is a massive flood of progressive-frequency items, so we # instead add progressive-frequency as its own item a smaller amount of times to prevent # flooding the duplicate item pool with them. - if self.multiworld.island_frequency_locations[self.player].value == 4: + if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive: for _ in range(2): # Progressives are not in item_pool, need to create faux item for duplicate item pool # This can still be filtered out later by duplicate_items setting @@ -86,9 +87,9 @@ def create_items(self): dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"]) # Remove progression or non-progression items if necessary - if (self.multiworld.duplicate_items[self.player].value == 0): # Progression only + if (self.options.duplicate_items == self.options.duplicate_items.option_progression): # Progression only dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True) - elif (self.multiworld.duplicate_items[self.player].value == 1): # Non-progression only + elif (self.options.duplicate_items == self.options.duplicate_items.option_non_progression): # Non-progression only dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False) dupeItemPool = list(dupeItemPool) @@ -115,14 +116,14 @@ def create_regions(self): create_regions(self.multiworld, self.player) def get_pre_fill_items(self): - if self.multiworld.island_frequency_locations[self.player] in [0, 1, 2, 3]: + if self.options.island_frequency_locations.is_filling_frequencies_in_world(): return [loc.item for loc in self.multiworld.get_filled_locations()] return [] def create_item_replaceAsNecessary(self, name: str) -> Item: isFrequency = "Frequency" in name - shouldUseProgressive = ((isFrequency and self.multiworld.island_frequency_locations[self.player].value == 4) - or (not isFrequency and self.multiworld.progressive_items[self.player].value)) + shouldUseProgressive = bool((isFrequency and self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive) + or (not isFrequency and self.options.progressive_items)) if shouldUseProgressive and name in progressive_table: name = progressive_table[name] return self.create_item(name) @@ -152,7 +153,7 @@ def collect_item(self, state, item, remove=False): return super(RaftWorld, self).collect_item(state, item, remove) def pre_fill(self): - if self.multiworld.island_frequency_locations[self.player] == 0: # Vanilla + if self.options.island_frequency_locations == self.options.island_frequency_locations.option_vanilla: self.setLocationItem("Radio Tower Frequency to Vasagatan", "Vasagatan Frequency") self.setLocationItem("Vasagatan Frequency to Balboa", "Balboa Island Frequency") self.setLocationItem("Relay Station quest", "Caravan Island Frequency") @@ -160,7 +161,7 @@ def pre_fill(self): self.setLocationItem("Tangaroa Frequency to Varuna Point", "Varuna Point Frequency") self.setLocationItem("Varuna Point Frequency to Temperance", "Temperance Frequency") self.setLocationItem("Temperance Frequency to Utopia", "Utopia Frequency") - elif self.multiworld.island_frequency_locations[self.player] == 1: # Random on island + elif self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_on_island: self.setLocationItemFromRegion("RadioTower", "Vasagatan Frequency") self.setLocationItemFromRegion("Vasagatan", "Balboa Island Frequency") self.setLocationItemFromRegion("BalboaIsland", "Caravan Island Frequency") @@ -168,7 +169,10 @@ def pre_fill(self): self.setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency") self.setLocationItemFromRegion("Varuna Point", "Temperance Frequency") self.setLocationItemFromRegion("Temperance", "Utopia Frequency") - elif self.multiworld.island_frequency_locations[self.player] in [2, 3]: + elif self.options.island_frequency_locations in [ + self.options.island_frequency_locations.option_random_island_order, + self.options.island_frequency_locations.option_random_on_island_random_order + ]: locationToFrequencyItemMap = { "Vasagatan": "Vasagatan Frequency", "BalboaIsland": "Balboa Island Frequency", @@ -196,9 +200,9 @@ def pre_fill(self): else: currentLocation = availableLocationList[0] # Utopia (only one left in list) availableLocationList.remove(currentLocation) - if self.multiworld.island_frequency_locations[self.player] == 2: # Random island order + if self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_island_order: self.setLocationItem(locationToVanillaFrequencyLocationMap[previousLocation], locationToFrequencyItemMap[currentLocation]) - elif self.multiworld.island_frequency_locations[self.player] == 3: # Random on island random order + elif self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_on_island_random_order: self.setLocationItemFromRegion(previousLocation, locationToFrequencyItemMap[currentLocation]) previousLocation = currentLocation @@ -215,9 +219,9 @@ def setLocationItemFromRegion(self, region: str, itemName: str): def fill_slot_data(self): return { - "IslandGenerationDistance": self.multiworld.island_generation_distance[self.player].value, - "ExpensiveResearch": bool(self.multiworld.expensive_research[self.player].value), - "DeathLink": bool(self.multiworld.death_link[self.player].value) + "IslandGenerationDistance": self.options.island_generation_distance.value, + "ExpensiveResearch": bool(self.options.expensive_research), + "DeathLink": bool(self.options.death_link) } def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): diff --git a/worlds/shorthike/Locations.py b/worlds/shorthike/Locations.py index 319ad8f20e1b..657035a03011 100644 --- a/worlds/shorthike/Locations.py +++ b/worlds/shorthike/Locations.py @@ -328,7 +328,7 @@ class LocationInfo(TypedDict): {"name": "Boat Rental", "id": base_id + 55, "inGameId": "DadDeer[0]", - "needsShovel": False, "purchase": True, + "needsShovel": False, "purchase": 100, "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, {"name": "Boat Challenge Reward", "id": base_id + 56, diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 07235ad2983a..1aba9af7ab56 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -255,7 +255,7 @@ def setup_victory(self): Event.victory) elif self.options.goal == Goal.option_greatest_walnut_hunter: self.create_event_location(location_table[GoalName.greatest_walnut_hunter], - self.logic.has_walnut(130), + self.logic.walnut.has_walnut(130), Event.victory) elif self.options.goal == Goal.option_protector_of_the_valley: self.create_event_location(location_table[GoalName.protector_of_the_valley], diff --git a/worlds/stardew_valley/content/vanilla/ginger_island.py b/worlds/stardew_valley/content/vanilla/ginger_island.py index d824deff3903..2fbcb032799e 100644 --- a/worlds/stardew_valley/content/vanilla/ginger_island.py +++ b/worlds/stardew_valley/content/vanilla/ginger_island.py @@ -3,6 +3,7 @@ from ...data import villagers_data, fish_data from ...data.game_item import ItemTag, Tag from ...data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource +from ...data.requirement import WalnutRequirement from ...data.shop import ShopSource from ...strings.book_names import Book from ...strings.crop_names import Fruit, Vegetable @@ -10,7 +11,7 @@ from ...strings.forageable_names import Forageable, Mushroom from ...strings.fruit_tree_names import Sapling from ...strings.metal_names import Fossil, Mineral -from ...strings.region_names import Region +from ...strings.region_names import Region, LogicRegion from ...strings.season_names import Season from ...strings.seed_names import Seed @@ -62,6 +63,9 @@ def harvest_source_hook(self, content: StardewContent): Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), ShopSource(items_price=((10, Mineral.diamond),), shop_region=Region.volcano_dwarf_shop), ), + Book.queen_of_sauce_cookbook: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=50000, shop_region=LogicRegion.bookseller_2, other_requirements=(WalnutRequirement(100),)),), # Worst book ever }, fishes=( diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 2c687eacbdde..917e8cca220a 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -290,9 +290,6 @@ Book.woodcutters_weekly: ( Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), - Book.queen_of_sauce_cookbook: ( - Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), - ShopSource(money_price=50000, shop_region=LogicRegion.bookseller_2),), # Worst book ever }, fishes=( fish_data.albacore, diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index bb2ed2e2ce1f..6e30d2b8c858 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2221,7 +2221,7 @@ id,region,name,tags,mod_name 3817,Shipping,Shipsanity: Raisins,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 3818,Shipping,Shipsanity: Dried Fruit,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 3819,Shipping,Shipsanity: Dried Mushrooms,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", -3820,Shipping,Shipsanity: Stardrop Tea,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3820,Shipping,Shipsanity: Stardrop Tea,"SHIPSANITY", 3821,Shipping,Shipsanity: Prize Ticket,"SHIPSANITY", 3822,Shipping,Shipsanity: Treasure Totem,"SHIPSANITY,REQUIRES_MASTERIES", 3823,Shipping,Shipsanity: Challenge Bait,"SHIPSANITY,REQUIRES_MASTERIES", @@ -2252,7 +2252,7 @@ id,region,name,tags,mod_name 3848,Shipping,Shipsanity: Way Of The Wind pt. 1,"SHIPSANITY", 3849,Shipping,Shipsanity: Mapping Cave Systems,"SHIPSANITY", 3850,Shipping,Shipsanity: Price Catalogue,"SHIPSANITY", -3851,Shipping,Shipsanity: Queen Of Sauce Cookbook,"SHIPSANITY", +3851,Shipping,Shipsanity: Queen Of Sauce Cookbook,"SHIPSANITY,GINGER_ISLAND", 3852,Shipping,Shipsanity: The Diamond Hunter,"SHIPSANITY,GINGER_ISLAND", 3853,Shipping,Shipsanity: Book of Mysteries,"SHIPSANITY", 3854,Shipping,Shipsanity: Animal Catalogue,"SHIPSANITY", @@ -2292,7 +2292,7 @@ id,region,name,tags,mod_name 4032,Farm,Read Book Of Stars,"BOOKSANITY,BOOKSANITY_SKILL", 4033,Farm,Read Combat Quarterly,"BOOKSANITY,BOOKSANITY_SKILL", 4034,Farm,Read Mining Monthly,"BOOKSANITY,BOOKSANITY_SKILL", -4035,Farm,Read Queen Of Sauce Cookbook,"BOOKSANITY,BOOKSANITY_SKILL", +4035,Farm,Read Queen Of Sauce Cookbook,"BOOKSANITY,BOOKSANITY_SKILL,GINGER_ISLAND", 4036,Farm,Read Stardew Valley Almanac,"BOOKSANITY,BOOKSANITY_SKILL", 4037,Farm,Read Woodcutter's Weekly,"BOOKSANITY,BOOKSANITY_SKILL", 4051,Museum,Read Tips on Farming,"BOOKSANITY,BOOKSANITY_LOST", diff --git a/worlds/stardew_valley/data/requirement.py b/worlds/stardew_valley/data/requirement.py index 7e9466630fc3..4744f9dffdfe 100644 --- a/worlds/stardew_valley/data/requirement.py +++ b/worlds/stardew_valley/data/requirement.py @@ -29,3 +29,8 @@ class SeasonRequirement(Requirement): @dataclass(frozen=True) class YearRequirement(Requirement): year: int + + +@dataclass(frozen=True) +class WalnutRequirement(Requirement): + amount: int diff --git a/worlds/stardew_valley/logic/bundle_logic.py b/worlds/stardew_valley/logic/bundle_logic.py index 4ca5fd81fc76..98fda1c73c7d 100644 --- a/worlds/stardew_valley/logic/bundle_logic.py +++ b/worlds/stardew_valley/logic/bundle_logic.py @@ -27,8 +27,8 @@ def __init__(self, *args, **kwargs): self.bundle = BundleLogic(*args, **kwargs) -class BundleLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, MoneyLogicMixin, QualityLogicMixin, FishingLogicMixin, SkillLogicMixin, -QuestLogicMixin]]): +class BundleLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, MoneyLogicMixin, QualityLogicMixin, FishingLogicMixin, +SkillLogicMixin, QuestLogicMixin]]): # Should be cached def can_complete_bundle(self, bundle: Bundle) -> StardewRule: item_rules = [] @@ -45,7 +45,7 @@ def can_complete_bundle(self, bundle: Bundle) -> StardewRule: qualities.append(bundle_item.quality) quality_rules = self.get_quality_rules(qualities) item_rules = self.logic.has_n(*item_rules, count=bundle.number_required) - time_rule = True_() if time_to_grind <= 0 else self.logic.time.has_lived_months(time_to_grind) + time_rule = self.logic.time.has_lived_months(time_to_grind) return can_speak_junimo & item_rules & quality_rules & time_rule def get_quality_rules(self, qualities: List[str]) -> StardewRule: diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py index 74cdaf2374e1..fb0d938fbb1e 100644 --- a/worlds/stardew_valley/logic/logic.py +++ b/worlds/stardew_valley/logic/logic.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -from functools import cached_property from typing import Collection, Callable from .ability_logic import AbilityLogicMixin @@ -43,6 +42,7 @@ from .tool_logic import ToolLogicMixin from .traveling_merchant_logic import TravelingMerchantLogicMixin from .wallet_logic import WalletLogicMixin +from .walnut_logic import WalnutLogicMixin from ..content.game_content import StardewContent from ..data.craftable_data import all_crafting_recipes from ..data.museum_data import all_museum_items @@ -50,16 +50,14 @@ from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.mod_logic import ModLogicMixin from ..mods.mod_data import ModNames -from ..options import SpecialOrderLocations, ExcludeGingerIsland, FestivalLocations, StardewValleyOptions, Walnutsanity +from ..options import ExcludeGingerIsland, FestivalLocations, StardewValleyOptions from ..stardew_rule import False_, True_, StardewRule from ..strings.animal_names import Animal from ..strings.animal_product_names import AnimalProduct -from ..strings.ap_names.ap_option_names import OptionName from ..strings.ap_names.community_upgrade_names import CommunityUpgrade -from ..strings.ap_names.event_names import Event from ..strings.artisan_good_names import ArtisanGood from ..strings.building_names import Building -from ..strings.craftable_names import Consumable, Furniture, Ring, Fishing, Lighting, WildSeeds +from ..strings.craftable_names import Consumable, Ring, Fishing, Lighting, WildSeeds from ..strings.crop_names import Fruit, Vegetable from ..strings.currency_names import Currency from ..strings.decoration_names import Decoration @@ -96,7 +94,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin, SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin, SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin, - RequirementLogicMixin, BookLogicMixin, GrindLogicMixin): + RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, WalnutLogicMixin): player: int options: StardewValleyOptions content: StardewContent @@ -461,32 +459,6 @@ def setup_events(self, register_event: Callable[[str, str, StardewRule], None]) def can_smelt(self, item: str) -> StardewRule: return self.has(Machine.furnace) & self.has(item) - @cached_property - def can_start_field_office(self) -> StardewRule: - field_office = self.region.can_reach(Region.field_office) - professor_snail = self.received("Open Professor Snail Cave") - return field_office & professor_snail - - def can_complete_large_animal_collection(self) -> StardewRule: - fossils = self.has_all(Fossil.fossilized_leg, Fossil.fossilized_ribs, Fossil.fossilized_skull, Fossil.fossilized_spine, Fossil.fossilized_tail) - return self.can_start_field_office & fossils - - def can_complete_snake_collection(self) -> StardewRule: - fossils = self.has_all(Fossil.snake_skull, Fossil.snake_vertebrae) - return self.can_start_field_office & fossils - - def can_complete_frog_collection(self) -> StardewRule: - fossils = self.has_all(Fossil.mummified_frog) - return self.can_start_field_office & fossils - - def can_complete_bat_collection(self) -> StardewRule: - fossils = self.has_all(Fossil.mummified_bat) - return self.can_start_field_office & fossils - - def can_complete_field_office(self) -> StardewRule: - return self.can_complete_large_animal_collection() & self.can_complete_snake_collection() & \ - self.can_complete_frog_collection() & self.can_complete_bat_collection() - def can_finish_grandpa_evaluation(self) -> StardewRule: # https://stardewvalleywiki.com/Grandpa rules_worth_a_point = [ @@ -566,86 +538,6 @@ def has_island_trader(self) -> StardewRule: return False_() return self.region.can_reach(Region.island_trader) - def has_walnut(self, number: int) -> StardewRule: - if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: - return False_() - if number <= 0: - return True_() - - if self.options.walnutsanity == Walnutsanity.preset_none: - return self.can_get_walnuts(number) - if self.options.walnutsanity == Walnutsanity.preset_all: - return self.has_received_walnuts(number) - puzzle_walnuts = 61 - bush_walnuts = 25 - dig_walnuts = 18 - repeatable_walnuts = 33 - total_walnuts = puzzle_walnuts + bush_walnuts + dig_walnuts + repeatable_walnuts - walnuts_to_receive = 0 - walnuts_to_collect = number - if OptionName.walnutsanity_puzzles in self.options.walnutsanity: - puzzle_walnut_rate = puzzle_walnuts / total_walnuts - puzzle_walnuts_required = round(puzzle_walnut_rate * number) - walnuts_to_receive += puzzle_walnuts_required - walnuts_to_collect -= puzzle_walnuts_required - if OptionName.walnutsanity_bushes in self.options.walnutsanity: - bush_walnuts_rate = bush_walnuts / total_walnuts - bush_walnuts_required = round(bush_walnuts_rate * number) - walnuts_to_receive += bush_walnuts_required - walnuts_to_collect -= bush_walnuts_required - if OptionName.walnutsanity_dig_spots in self.options.walnutsanity: - dig_walnuts_rate = dig_walnuts / total_walnuts - dig_walnuts_required = round(dig_walnuts_rate * number) - walnuts_to_receive += dig_walnuts_required - walnuts_to_collect -= dig_walnuts_required - if OptionName.walnutsanity_repeatables in self.options.walnutsanity: - repeatable_walnuts_rate = repeatable_walnuts / total_walnuts - repeatable_walnuts_required = round(repeatable_walnuts_rate * number) - walnuts_to_receive += repeatable_walnuts_required - walnuts_to_collect -= repeatable_walnuts_required - return self.has_received_walnuts(walnuts_to_receive) & self.can_get_walnuts(walnuts_to_collect) - - def has_received_walnuts(self, number: int) -> StardewRule: - return self.received(Event.received_walnuts, number) - - def can_get_walnuts(self, number: int) -> StardewRule: - # https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations - reach_south = self.region.can_reach(Region.island_south) - reach_north = self.region.can_reach(Region.island_north) - reach_west = self.region.can_reach(Region.island_west) - reach_hut = self.region.can_reach(Region.leo_hut) - reach_southeast = self.region.can_reach(Region.island_south_east) - reach_field_office = self.region.can_reach(Region.field_office) - reach_pirate_cove = self.region.can_reach(Region.pirate_cove) - reach_outside_areas = self.logic.and_(reach_south, reach_north, reach_west, reach_hut) - reach_volcano_regions = [self.region.can_reach(Region.volcano), - self.region.can_reach(Region.volcano_secret_beach), - self.region.can_reach(Region.volcano_floor_5), - self.region.can_reach(Region.volcano_floor_10)] - reach_volcano = self.logic.or_(*reach_volcano_regions) - reach_all_volcano = self.logic.and_(*reach_volcano_regions) - reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office] - reach_caves = self.logic.and_(self.region.can_reach(Region.qi_walnut_room), self.region.can_reach(Region.dig_site), - self.region.can_reach(Region.gourmand_frog_cave), - self.region.can_reach(Region.colored_crystals_cave), - self.region.can_reach(Region.shipwreck), self.combat.has_slingshot) - reach_entire_island = self.logic.and_(reach_outside_areas, reach_all_volcano, - reach_caves, reach_southeast, reach_field_office, reach_pirate_cove) - if number <= 5: - return self.logic.or_(reach_south, reach_north, reach_west, reach_volcano) - if number <= 10: - return self.count(2, *reach_walnut_regions) - if number <= 15: - return self.count(3, *reach_walnut_regions) - if number <= 20: - return self.logic.and_(*reach_walnut_regions) - if number <= 50: - return reach_entire_island - gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz) - return reach_entire_island & self.has(Fruit.banana) & self.has_all(*gems) & self.ability.can_mine_perfectly() & \ - self.ability.can_fish_perfectly() & self.has(Furniture.flute_block) & self.has(Seed.melon) & self.has(Seed.wheat) & self.has(Seed.garlic) & \ - self.can_complete_field_office() - def has_all_stardrops(self) -> StardewRule: other_rules = [] number_of_stardrops_to_receive = 0 diff --git a/worlds/stardew_valley/logic/museum_logic.py b/worlds/stardew_valley/logic/museum_logic.py index 4ba5364f5524..36ba62b31fcb 100644 --- a/worlds/stardew_valley/logic/museum_logic.py +++ b/worlds/stardew_valley/logic/museum_logic.py @@ -41,7 +41,7 @@ def can_find_museum_item(self, item: MuseumItem) -> StardewRule: else: geodes_rule = False_() # monster_rule = self.can_farm_monster(item.monsters) - time_needed_to_grind = (20 - item.difficulty) / 2 + time_needed_to_grind = int((20 - item.difficulty) // 2) time_rule = self.logic.time.has_lived_months(time_needed_to_grind) pan_rule = False_() if item.item_name == Mineral.earth_crystal or item.item_name == Mineral.fire_quartz or item.item_name == Mineral.frozen_tear: diff --git a/worlds/stardew_valley/logic/requirement_logic.py b/worlds/stardew_valley/logic/requirement_logic.py index 87d9ee021524..9356440ac6a8 100644 --- a/worlds/stardew_valley/logic/requirement_logic.py +++ b/worlds/stardew_valley/logic/requirement_logic.py @@ -9,8 +9,9 @@ from .skill_logic import SkillLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin +from .walnut_logic import WalnutLogicMixin from ..data.game_item import Requirement -from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement +from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, WalnutRequirement class RequirementLogicMixin(BaseLogicMixin): @@ -20,7 +21,7 @@ def __init__(self, *args, **kwargs): class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin, -SeasonLogicMixin, TimeLogicMixin]]): +SeasonLogicMixin, TimeLogicMixin, WalnutLogicMixin]]): def meet_all_requirements(self, requirements: Iterable[Requirement]): if not requirements: @@ -50,3 +51,7 @@ def _(self, requirement: SeasonRequirement): @meet_requirement.register def _(self, requirement: YearRequirement): return self.logic.time.has_year(requirement.year) + + @meet_requirement.register + def _(self, requirement: WalnutRequirement): + return self.logic.walnut.has_walnut(requirement.amount) diff --git a/worlds/stardew_valley/logic/time_logic.py b/worlds/stardew_valley/logic/time_logic.py index 94e0e277c86c..2ba76579ff45 100644 --- a/worlds/stardew_valley/logic/time_logic.py +++ b/worlds/stardew_valley/logic/time_logic.py @@ -26,8 +26,10 @@ class TimeLogic(BaseLogic[Union[TimeLogicMixin, HasLogicMixin]]): @cache_self1 def has_lived_months(self, number: int) -> StardewRule: + assert isinstance(number, int), "Can't have lived a fraction of a month. Use // instead of / when dividing." if number <= 0: return self.logic.true_ + number = min(number, MAX_MONTHS) return HasProgressionPercent(self.player, number * MONTH_COEFFICIENT) diff --git a/worlds/stardew_valley/logic/walnut_logic.py b/worlds/stardew_valley/logic/walnut_logic.py new file mode 100644 index 000000000000..14fe1c339090 --- /dev/null +++ b/worlds/stardew_valley/logic/walnut_logic.py @@ -0,0 +1,135 @@ +from functools import cached_property +from typing import Union + +from .ability_logic import AbilityLogicMixin +from .base_logic import BaseLogic, BaseLogicMixin +from .combat_logic import CombatLogicMixin +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from ..strings.ap_names.event_names import Event +from ..options import ExcludeGingerIsland, Walnutsanity +from ..stardew_rule import StardewRule, False_, True_ +from ..strings.ap_names.ap_option_names import OptionName +from ..strings.craftable_names import Furniture +from ..strings.crop_names import Fruit +from ..strings.metal_names import Mineral, Fossil +from ..strings.region_names import Region +from ..strings.seed_names import Seed + + +class WalnutLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.walnut = WalnutLogic(*args, **kwargs) + + +class WalnutLogic(BaseLogic[Union[WalnutLogicMixin, ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, CombatLogicMixin, + AbilityLogicMixin]]): + + def has_walnut(self, number: int) -> StardewRule: + if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: + return False_() + if number <= 0: + return True_() + + if self.options.walnutsanity == Walnutsanity.preset_none: + return self.can_get_walnuts(number) + if self.options.walnutsanity == Walnutsanity.preset_all: + return self.has_received_walnuts(number) + puzzle_walnuts = 61 + bush_walnuts = 25 + dig_walnuts = 18 + repeatable_walnuts = 33 + total_walnuts = puzzle_walnuts + bush_walnuts + dig_walnuts + repeatable_walnuts + walnuts_to_receive = 0 + walnuts_to_collect = number + if OptionName.walnutsanity_puzzles in self.options.walnutsanity: + puzzle_walnut_rate = puzzle_walnuts / total_walnuts + puzzle_walnuts_required = round(puzzle_walnut_rate * number) + walnuts_to_receive += puzzle_walnuts_required + walnuts_to_collect -= puzzle_walnuts_required + if OptionName.walnutsanity_bushes in self.options.walnutsanity: + bush_walnuts_rate = bush_walnuts / total_walnuts + bush_walnuts_required = round(bush_walnuts_rate * number) + walnuts_to_receive += bush_walnuts_required + walnuts_to_collect -= bush_walnuts_required + if OptionName.walnutsanity_dig_spots in self.options.walnutsanity: + dig_walnuts_rate = dig_walnuts / total_walnuts + dig_walnuts_required = round(dig_walnuts_rate * number) + walnuts_to_receive += dig_walnuts_required + walnuts_to_collect -= dig_walnuts_required + if OptionName.walnutsanity_repeatables in self.options.walnutsanity: + repeatable_walnuts_rate = repeatable_walnuts / total_walnuts + repeatable_walnuts_required = round(repeatable_walnuts_rate * number) + walnuts_to_receive += repeatable_walnuts_required + walnuts_to_collect -= repeatable_walnuts_required + return self.has_received_walnuts(walnuts_to_receive) & self.can_get_walnuts(walnuts_to_collect) + + def has_received_walnuts(self, number: int) -> StardewRule: + return self.logic.received(Event.received_walnuts, number) + + def can_get_walnuts(self, number: int) -> StardewRule: + # https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations + reach_south = self.logic.region.can_reach(Region.island_south) + reach_north = self.logic.region.can_reach(Region.island_north) + reach_west = self.logic.region.can_reach(Region.island_west) + reach_hut = self.logic.region.can_reach(Region.leo_hut) + reach_southeast = self.logic.region.can_reach(Region.island_south_east) + reach_field_office = self.logic.region.can_reach(Region.field_office) + reach_pirate_cove = self.logic.region.can_reach(Region.pirate_cove) + reach_outside_areas = self.logic.and_(reach_south, reach_north, reach_west, reach_hut) + reach_volcano_regions = [self.logic.region.can_reach(Region.volcano), + self.logic.region.can_reach(Region.volcano_secret_beach), + self.logic.region.can_reach(Region.volcano_floor_5), + self.logic.region.can_reach(Region.volcano_floor_10)] + reach_volcano = self.logic.or_(*reach_volcano_regions) + reach_all_volcano = self.logic.and_(*reach_volcano_regions) + reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office] + reach_caves = self.logic.and_(self.logic.region.can_reach(Region.qi_walnut_room), self.logic.region.can_reach(Region.dig_site), + self.logic.region.can_reach(Region.gourmand_frog_cave), + self.logic.region.can_reach(Region.colored_crystals_cave), + self.logic.region.can_reach(Region.shipwreck), self.logic.combat.has_slingshot) + reach_entire_island = self.logic.and_(reach_outside_areas, reach_all_volcano, + reach_caves, reach_southeast, reach_field_office, reach_pirate_cove) + if number <= 5: + return self.logic.or_(reach_south, reach_north, reach_west, reach_volcano) + if number <= 10: + return self.logic.count(2, *reach_walnut_regions) + if number <= 15: + return self.logic.count(3, *reach_walnut_regions) + if number <= 20: + return self.logic.and_(*reach_walnut_regions) + if number <= 50: + return reach_entire_island + gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz) + return reach_entire_island & self.logic.has(Fruit.banana) & self.logic.has_all(*gems) & \ + self.logic.ability.can_mine_perfectly() & self.logic.ability.can_fish_perfectly() & \ + self.logic.has(Furniture.flute_block) & self.logic.has(Seed.melon) & self.logic.has(Seed.wheat) & \ + self.logic.has(Seed.garlic) & self.can_complete_field_office() + + @cached_property + def can_start_field_office(self) -> StardewRule: + field_office = self.logic.region.can_reach(Region.field_office) + professor_snail = self.logic.received("Open Professor Snail Cave") + return field_office & professor_snail + + def can_complete_large_animal_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.fossilized_leg, Fossil.fossilized_ribs, Fossil.fossilized_skull, Fossil.fossilized_spine, Fossil.fossilized_tail) + return self.can_start_field_office & fossils + + def can_complete_snake_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.snake_skull, Fossil.snake_vertebrae) + return self.can_start_field_office & fossils + + def can_complete_frog_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.mummified_frog) + return self.can_start_field_office & fossils + + def can_complete_bat_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.mummified_bat) + return self.can_start_field_office & fossils + + def can_complete_field_office(self) -> StardewRule: + return self.can_complete_large_animal_collection() & self.can_complete_snake_collection() & \ + self.can_complete_frog_collection() & self.can_complete_bat_collection() diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index c30d04c8a6f2..7c1fdbda3cf4 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -375,7 +375,7 @@ def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_optio MultiWorldRules.add_rule(multiworld.get_location("Open Professor Snail Cave", player), logic.has(Bomb.cherry_bomb)) MultiWorldRules.add_rule(multiworld.get_location("Complete Island Field Office", player), - logic.can_complete_field_office()) + logic.walnut.can_complete_field_office()) set_walnut_rules(logic, multiworld, player, world_options) @@ -432,10 +432,10 @@ def set_island_entrances_rules(logic: StardewLogic, multiworld, player, world_op def set_island_parrot_rules(logic: StardewLogic, multiworld, player): # Logic rules require more walnuts than in reality, to allow the player to spend them "wrong" - has_walnut = logic.has_walnut(5) - has_5_walnut = logic.has_walnut(15) - has_10_walnut = logic.has_walnut(40) - has_20_walnut = logic.has_walnut(60) + has_walnut = logic.walnut.has_walnut(5) + has_5_walnut = logic.walnut.has_walnut(15) + has_10_walnut = logic.walnut.has_walnut(40) + has_20_walnut = logic.walnut.has_walnut(60) MultiWorldRules.add_rule(multiworld.get_location("Leo's Parrot", player), has_walnut) MultiWorldRules.add_rule(multiworld.get_location("Island West Turtle", player), @@ -471,7 +471,7 @@ def set_walnut_rules(logic: StardewLogic, multiworld, player, world_options: Sta set_walnut_repeatable_rules(logic, multiworld, player, world_options) -def set_walnut_puzzle_rules(logic, multiworld, player, world_options): +def set_walnut_puzzle_rules(logic: StardewLogic, multiworld, player, world_options): if OptionName.walnutsanity_puzzles not in world_options.walnutsanity: return @@ -482,15 +482,17 @@ def set_walnut_puzzle_rules(logic, multiworld, player, world_options): logic.has(Mineral.emerald) & logic.has(Mineral.ruby) & logic.has(Mineral.topaz) & logic.region.can_reach_all((Region.island_north, Region.island_west, Region.island_east, Region.island_south))) MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Melon", player), logic.has(Fruit.melon) & logic.region.can_reach(Region.island_west)) - MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) & logic.region.can_reach(Region.island_west)) - MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & logic.region.can_reach(Region.island_west)) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) & + logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Melon")) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & + logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Wheat")) MultiWorldRules.add_rule(multiworld.get_location("Whack A Mole", player), logic.tool.has_tool(Tool.watering_can, ToolMaterial.iridium)) - MultiWorldRules.add_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.can_complete_large_animal_collection()) - MultiWorldRules.add_rule(multiworld.get_location("Complete Snake Collection", player), logic.can_complete_snake_collection()) - MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Frog Collection", player), logic.can_complete_frog_collection()) - MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Bat Collection", player), logic.can_complete_bat_collection()) - MultiWorldRules.add_rule(multiworld.get_location("Purple Flowers Island Survey", player), logic.can_start_field_office) - MultiWorldRules.add_rule(multiworld.get_location("Purple Starfish Island Survey", player), logic.can_start_field_office) + MultiWorldRules.add_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.walnut.can_complete_large_animal_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Snake Collection", player), logic.walnut.can_complete_snake_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Frog Collection", player), logic.walnut.can_complete_frog_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Bat Collection", player), logic.walnut.can_complete_bat_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Purple Flowers Island Survey", player), logic.walnut.can_start_field_office) + MultiWorldRules.add_rule(multiworld.get_location("Purple Starfish Island Survey", player), logic.walnut.can_start_field_office) MultiWorldRules.add_rule(multiworld.get_location("Protruding Tree Walnut", player), logic.combat.has_slingshot) MultiWorldRules.add_rule(multiworld.get_location("Starfish Tide Pool", player), logic.tool.has_fishing_rod(1)) MultiWorldRules.add_rule(multiworld.get_location("Mermaid Song", player), logic.has(Furniture.flute_block)) diff --git a/worlds/stardew_valley/stardew_rule/base.py b/worlds/stardew_valley/stardew_rule/base.py index 576cd36851fb..3e6eb327ea99 100644 --- a/worlds/stardew_valley/stardew_rule/base.py +++ b/worlds/stardew_valley/stardew_rule/base.py @@ -431,7 +431,7 @@ def rules_count(self): return len(self.rules) def __repr__(self): - return f"Received {self.count} {repr(self.rules)}" + return f"Received {self.count} [{', '.join(f'{value}x {repr(rule)}' for rule, value in self.counter.items())}]" @dataclass(frozen=True) diff --git a/worlds/stardew_valley/stardew_rule/rule_explain.py b/worlds/stardew_valley/stardew_rule/rule_explain.py index 61a88ceb6996..a9767c7b72d5 100644 --- a/worlds/stardew_valley/stardew_rule/rule_explain.py +++ b/worlds/stardew_valley/stardew_rule/rule_explain.py @@ -34,7 +34,7 @@ def __str__(self, depth=0): if not self.sub_rules: return self.summary(depth) - return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__str__(i, depth + 1) + return self.summary(depth) + "\n" + "\n".join(i.__str__(depth + 1) if i.result is not self.expected else i.summary(depth + 1) for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) @@ -42,7 +42,7 @@ def __repr__(self, depth=0): if not self.sub_rules: return self.summary(depth) - return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__repr__(i, depth + 1) + return self.summary(depth) + "\n" + "\n".join(i.__repr__(depth + 1) for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) @cached_property @@ -61,6 +61,33 @@ def explained_sub_rules(self) -> List[RuleExplanation]: return [_explain(i, self.state, self.expected, self.explored_rules_key) for i in self.sub_rules] +@dataclass +class CountSubRuleExplanation(RuleExplanation): + count: int = 1 + + @staticmethod + def from_explanation(expl: RuleExplanation, count: int) -> CountSubRuleExplanation: + return CountSubRuleExplanation(expl.rule, expl.state, expl.expected, expl.sub_rules, expl.explored_rules_key, expl.current_rule_explored, count) + + def summary(self, depth=0) -> str: + summary = " " * depth + f"{self.count}x {str(self.rule)} -> {self.result}" + if self.current_rule_explored: + summary += " [Already explained]" + return summary + + +@dataclass +class CountExplanation(RuleExplanation): + rule: Count + + @cached_property + def explained_sub_rules(self) -> List[RuleExplanation]: + return [ + CountSubRuleExplanation.from_explanation(_explain(rule, self.state, self.expected, self.explored_rules_key), count) + for rule, count in self.rule.counter.items() + ] + + def explain(rule: CollectionRule, state: CollectionState, expected: bool = True) -> RuleExplanation: if isinstance(rule, StardewRule): return _explain(rule, state, expected, explored_spots=set()) @@ -80,7 +107,7 @@ def _(rule: AggregatingStardewRule, state: CollectionState, expected: bool, expl @_explain.register def _(rule: Count, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: - return RuleExplanation(rule, state, expected, rule.rules, explored_rules_key=explored_spots) + return CountExplanation(rule, state, expected, rule.rules, explored_rules_key=explored_spots) @_explain.register diff --git a/worlds/stardew_valley/stardew_rule/state.py b/worlds/stardew_valley/stardew_rule/state.py index cf0996a63bbc..5f5e61b3d4e5 100644 --- a/worlds/stardew_valley/stardew_rule/state.py +++ b/worlds/stardew_valley/stardew_rule/state.py @@ -122,4 +122,4 @@ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRul return self, self(state) def __repr__(self): - return f"Received {self.percent}% progression items." + return f"Received {self.percent}% progression items" diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 856117469e55..58d8fa543a6d 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -44,7 +44,7 @@ class SubnauticaWorld(World): location_name_to_id = all_locations options_dataclass = options.SubnauticaOptions options: options.SubnauticaOptions - required_client_version = (0, 4, 1) + required_client_version = (0, 5, 0) creatures_to_scan: List[str] diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 8689a51b7601..0bd8c5e80681 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -67,7 +67,7 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: "Eastern Vault West Fuses": "Eastern Vault Fortress", "Eastern Vault East Fuse": "Eastern Vault Fortress", "Quarry Connector Fuse": "Quarry Connector", - "Quarry Fuse": "Quarry", + "Quarry Fuse": "Quarry Entry", "Ziggurat Fuse": "Rooted Ziggurat Lower Back", "West Garden Fuse": "West Garden", "Library Fuse": "Library Lab", diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index 2d87140fe50f..09916228163d 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -208,15 +208,15 @@ class TunicLocationData(NamedTuple): "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"), "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="Holy Cross"), "Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"), - "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Near Telescope": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Upper Floor": TunicLocationData("Quarry", "Quarry"), - "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Obscured Near Winding Staircase": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Obscured Beneath Scaffolding": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Obscured Near Telescope": TunicLocationData("Quarry", "Quarry"), "Quarry - [Back Entrance] Obscured Behind Wall": TunicLocationData("Quarry Back", "Quarry Back"), - "Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [Central] Top Floor Overhang": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Near Bridge": TunicLocationData("Quarry", "Quarry"), "Quarry - [Central] Above Ladder": TunicLocationData("Quarry", "Quarry Monastery Entry"), diff --git a/worlds/yugioh06/boosterpacks.py b/worlds/yugioh06/boosterpacks.py index f6f4ec7732c3..645977d28def 100644 --- a/worlds/yugioh06/boosterpacks.py +++ b/worlds/yugioh06/boosterpacks.py @@ -1,13 +1,13 @@ -from typing import Dict, Set +from typing import Dict, List -booster_contents: Dict[str, Set[str]] = { - "LEGEND OF B.E.W.D.": { +booster_contents: Dict[str, List[str]] = { + "LEGEND OF B.E.W.D.": [ "Exodia", "Dark Magician", "Polymerization", "Skull Servant" - }, - "METAL RAIDERS": { + ], + "METAL RAIDERS": [ "Petit Moth", "Cocoon of Evolution", "Time Wizard", @@ -30,8 +30,8 @@ "Solemn Judgment", "Dream Clown", "Heavy Storm" - }, - "PHARAOH'S SERVANT": { + ], + "PHARAOH'S SERVANT": [ "Beast of Talwar", "Jinzo", "Gearfried the Iron Knight", @@ -43,8 +43,8 @@ "The Shallow Grave", "Nobleman of Crossout", "Magic Drain" - }, - "PHARAONIC GUARDIAN": { + ], + "PHARAONIC GUARDIAN": [ "Don Zaloog", "Reasoning", "Dark Snake Syndrome", @@ -71,8 +71,8 @@ "Book of Taiyou", "Dust Tornado", "Raigeki Break" - }, - "SPELL RULER": { + ], + "SPELL RULER": [ "Ritual", "Messenger of Peace", "Megamorph", @@ -94,8 +94,8 @@ "Senju of the Thousand Hands", "Sonic Bird", "Mystical Space Typhoon" - }, - "LABYRINTH OF NIGHTMARE": { + ], + "LABYRINTH OF NIGHTMARE": [ "Destiny Board", "Spirit Message 'I'", "Spirit Message 'N'", @@ -119,8 +119,8 @@ "United We Stand", "Earthbound Spirit", "The Masked Beast" - }, - "LEGACY OF DARKNESS": { + ], + "LEGACY OF DARKNESS": [ "Last Turn", "Yata-Garasu", "Opticlops", @@ -143,8 +143,8 @@ "Maharaghi", "Susa Soldier", "Emergency Provisions", - }, - "MAGICIAN'S FORCE": { + ], + "MAGICIAN'S FORCE": [ "Huge Revolution", "Oppressed People", "United Resistance", @@ -185,8 +185,8 @@ "Royal Magical Library", "Spell Shield Type-8", "Tribute Doll", - }, - "DARK CRISIS": { + ], + "DARK CRISIS": [ "Final Countdown", "Ojama Green", "Dark Scorpion Combination", @@ -213,8 +213,8 @@ "Spell Reproduction", "Contract with the Abyss", "Dark Master - Zorc" - }, - "INVASION OF CHAOS": { + ], + "INVASION OF CHAOS": [ "Ojama Delta Hurricane", "Ojama Yellow", "Ojama Black", @@ -241,8 +241,8 @@ "Cursed Seal of the Forbidden Spell", "Stray Lambs", "Manju of the Ten Thousand Hands" - }, - "ANCIENT SANCTUARY": { + ], + "ANCIENT SANCTUARY": [ "Monster Gate", "Wall of Revealing Light", "Mystik Wok", @@ -255,8 +255,8 @@ "King of the Swamp", "Enemy Controller", "Enchanting Fitting Room" - }, - "SOUL OF THE DUELIST": { + ], + "SOUL OF THE DUELIST": [ "Ninja Grandmaster Sasuke", "Mystic Swordsman LV2", "Mystic Swordsman LV4", @@ -272,8 +272,8 @@ "Level Up!", "Howling Insect", "Mobius the Frost Monarch" - }, - "RISE OF DESTINY": { + ], + "RISE OF DESTINY": [ "Homunculus the Alchemic Being", "Thestalos the Firestorm Monarch", "Roc from the Valley of Haze", @@ -283,8 +283,8 @@ "Ultimate Insect Lv3", "Divine Wrath", "Serial Spell" - }, - "FLAMING ETERNITY": { + ], + "FLAMING ETERNITY": [ "Insect Knight", "Chiron the Mage", "Granmarg the Rock Monarch", @@ -297,8 +297,8 @@ "Golem Sentry", "Rescue Cat", "Blade Rabbit" - }, - "THE LOST MILLENIUM": { + ], + "THE LOST MILLENIUM": [ "Ritual", "Megarock Dragon", "D.D. Survivor", @@ -311,8 +311,8 @@ "Elemental Hero Thunder Giant", "Aussa the Earth Charmer", "Brain Control" - }, - "CYBERNETIC REVOLUTION": { + ], + "CYBERNETIC REVOLUTION": [ "Power Bond", "Cyber Dragon", "Cyber Twin Dragon", @@ -322,8 +322,8 @@ "Miracle Fusion", "Elemental Hero Bubbleman", "Jerry Beans Man" - }, - "ELEMENTAL ENERGY": { + ], + "ELEMENTAL ENERGY": [ "V-Tiger Jet", "W-Wing Catapult", "VW-Tiger Catapult", @@ -344,8 +344,8 @@ "Elemental Hero Bladedge", "Pot of Avarice", "B.E.S. Tetran" - }, - "SHADOW OF INFINITY": { + ], + "SHADOW OF INFINITY": [ "Hamon, Lord of Striking Thunder", "Raviel, Lord of Phantasms", "Uria, Lord of Searing Flames", @@ -357,8 +357,8 @@ "Gokipon", "Demise, King of Armageddon", "Anteatereatingant" - }, - "GAME GIFT COLLECTION": { + ], + "GAME GIFT COLLECTION": [ "Ritual", "Valkyrion the Magna Warrior", "Alpha the Magnet Warrior", @@ -383,8 +383,8 @@ "Card Destruction", "Dark Magic Ritual", "Calamity of the Wicked" - }, - "Special Gift Collection": { + ], + "Special Gift Collection": [ "Gate Guardian", "Scapegoat", "Gil Garth", @@ -398,8 +398,8 @@ "Curse of Vampire", "Elemental Hero Flame Wingman", "Magician of Black Chaos" - }, - "Fairy Collection": { + ], + "Fairy Collection": [ "Silpheed", "Dunames Dark Witch", "Hysteric Fairy", @@ -416,8 +416,8 @@ "Asura Priest", "Manju of the Ten Thousand Hands", "Senju of the Thousand Hands" - }, - "Dragon Collection": { + ], + "Dragon Collection": [ "Victory D.", "Chaos Emperor Dragon - Envoy of the End", "Kaiser Glider", @@ -434,16 +434,16 @@ "Troop Dragon", "Horus the Black Flame Dragon LV4", "Pitch-Dark Dragon" - }, - "Warrior Collection A": { + ], + "Warrior Collection A": [ "Gate Guardian", "Gearfried the Iron Knight", "Dimensional Warrior", "Command Knight", "The Last Warrior from Another Planet", "Dream Clown" - }, - "Warrior Collection B": { + ], + "Warrior Collection B": [ "Don Zaloog", "Dark Scorpion - Chick the Yellow", "Dark Scorpion - Meanae the Thorn", @@ -467,8 +467,8 @@ "Blade Knight", "Marauding Captain", "Toon Goblin Attack Force" - }, - "Fiend Collection A": { + ], + "Fiend Collection A": [ "Sangan", "Castle of Dark Illusions", "Barox", @@ -480,8 +480,8 @@ "Spear Cretin", "Versago the Destroyer", "Toon Summoned Skull" - }, - "Fiend Collection B": { + ], + "Fiend Collection B": [ "Raviel, Lord of Phantasms", "Yata-Garasu", "Helpoemer", @@ -505,15 +505,15 @@ "Jowls of Dark Demise", "D. D. Trainer", "Earthbound Spirit" - }, - "Machine Collection A": { + ], + "Machine Collection A": [ "Cyber-Stein", "Mechanicalchaser", "Jinzo", "UFO Turtle", "Cyber-Tech Alligator" - }, - "Machine Collection B": { + ], + "Machine Collection B": [ "X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", @@ -531,8 +531,8 @@ "Red Gadget", "Yellow Gadget", "B.E.S. Tetran" - }, - "Spellcaster Collection A": { + ], + "Spellcaster Collection A": [ "Exodia", "Dark Sage", "Dark Magician", @@ -544,8 +544,8 @@ "Injection Fairy Lily", "Cosmo Queen", "Magician of Black Chaos" - }, - "Spellcaster Collection B": { + ], + "Spellcaster Collection B": [ "Jowgen the Spiritualist", "Tsukuyomi", "Manticore of Darkness", @@ -574,8 +574,8 @@ "Royal Magical Library", "Aussa the Earth Charmer", - }, - "Zombie Collection": { + ], + "Zombie Collection": [ "Skull Servant", "Regenerating Mummy", "Ryu Kokki", @@ -590,8 +590,8 @@ "Des Lacooda", "Wandering Mummy", "Royal Keeper" - }, - "Special Monsters A": { + ], + "Special Monsters A": [ "X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", @@ -626,8 +626,8 @@ "Fushi No Tori", "Maharaghi", "Susa Soldier" - }, - "Special Monsters B": { + ], + "Special Monsters B": [ "Polymerization", "Mystic Swordsman LV2", "Mystic Swordsman LV4", @@ -656,8 +656,8 @@ "Level Up!", "Ultimate Insect Lv3", "Ultimate Insect Lv5" - }, - "Reverse Collection": { + ], + "Reverse Collection": [ "Magical Merchant", "Castle of Dark Illusions", "Magician of Faith", @@ -675,8 +675,8 @@ "Spear Cretin", "Nobleman of Crossout", "Aussa the Earth Charmer" - }, - "LP Recovery Collection": { + ], + "LP Recovery Collection": [ "Mystik Wok", "Poison of the Old Man", "Hysteric Fairy", @@ -691,8 +691,8 @@ "Elemental Hero Steam Healer", "Fushi No Tori", "Emergency Provisions" - }, - "Special Summon Collection A": { + ], + "Special Summon Collection A": [ "Perfectly Ultimate Great Moth", "Dark Sage", "Polymerization", @@ -726,8 +726,8 @@ "Morphing Jar #2", "Spear Cretin", "Dark Magic Curtain" - }, - "Special Summon Collection B": { + ], + "Special Summon Collection B": [ "Monster Gate", "Chaos Emperor Dragon - Envoy of the End", "Ojama Trio", @@ -756,8 +756,8 @@ "Tribute Doll", "Enchanting Fitting Room", "Stray Lambs" - }, - "Special Summon Collection C": { + ], + "Special Summon Collection C": [ "Hamon, Lord of Striking Thunder", "Raviel, Lord of Phantasms", "Uria, Lord of Searing Flames", @@ -782,13 +782,13 @@ "Ultimate Insect Lv5", "Rescue Cat", "Anteatereatingant" - }, - "Equipment Collection": { + ], + "Equipment Collection": [ "Megamorph", "Cestus of Dagla", "United We Stand" - }, - "Continuous Spell/Trap A": { + ], + "Continuous Spell/Trap A": [ "Destiny Board", "Spirit Message 'I'", "Spirit Message 'N'", @@ -801,8 +801,8 @@ "Solemn Wishes", "Embodiment of Apophis", "Toon World" - }, - "Continuous Spell/Trap B": { + ], + "Continuous Spell/Trap B": [ "Hamon, Lord of Striking Thunder", "Uria, Lord of Searing Flames", "Wave-Motion Cannon", @@ -815,8 +815,8 @@ "Skull Zoma", "Pitch-Black Power Stone", "Metal Reflect Slime" - }, - "Quick/Counter Collection": { + ], + "Quick/Counter Collection": [ "Mystik Wok", "Poison of the Old Man", "Scapegoat", @@ -841,8 +841,8 @@ "Book of Moon", "Serial Spell", "Mystical Space Typhoon" - }, - "Direct Damage Collection": { + ], + "Direct Damage Collection": [ "Hamon, Lord of Striking Thunder", "Chaos Emperor Dragon - Envoy of the End", "Dark Snake Syndrome", @@ -868,8 +868,8 @@ "Jowls of Dark Demise", "Stealth Bird", "Elemental Hero Bladedge", - }, - "Direct Attack Collection": { + ], + "Direct Attack Collection": [ "Victory D.", "Dark Scorpion Combination", "Spirit Reaper", @@ -880,8 +880,8 @@ "Toon Mermaid", "Toon Summoned Skull", "Toon Dark Magician Girl" - }, - "Monster Destroy Collection": { + ], + "Monster Destroy Collection": [ "Hamon, Lord of Striking Thunder", "Inferno", "Ninja Grandmaster Sasuke", @@ -912,12 +912,12 @@ "Offerings to the Doomed", "Divine Wrath", "Dream Clown" - }, + ], } def get_booster_locations(booster: str) -> Dict[str, str]: return { f"{booster} {i}": content - for i, content in enumerate(booster_contents[booster]) + for i, content in enumerate(booster_contents[booster], 1) } diff --git a/worlds/yugioh06/structure_deck.py b/worlds/yugioh06/structure_deck.py index d58223f2e216..3559e7c5153e 100644 --- a/worlds/yugioh06/structure_deck.py +++ b/worlds/yugioh06/structure_deck.py @@ -1,7 +1,7 @@ -from typing import Dict, Set +from typing import Dict, List -structure_contents: Dict[str, Set] = { - "dragons_roar": { +structure_contents: Dict[str, List[str]] = { + "dragons_roar": [ "Luster Dragon", "Armed Dragon LV3", "Armed Dragon LV5", @@ -14,9 +14,9 @@ "Stamping Destruction", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "zombie_madness": { + "Mystical Space Typhoon" + ], + "zombie_madness": [ "Pyramid Turtle", "Regenerating Mummy", "Ryu Kokki", @@ -26,9 +26,9 @@ "Reload", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "blazing_destruction": { + "Mystical Space Typhoon" + ], + "blazing_destruction": [ "Inferno", "Solar Flare Dragon", "UFO Turtle", @@ -38,9 +38,9 @@ "Level Limit - Area B", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "fury_from_the_deep": { + "Mystical Space Typhoon" + ], + "fury_from_the_deep": [ "Mother Grizzly", "Water Beaters", "Gravity Bind", @@ -48,9 +48,9 @@ "Mobius the Frost Monarch", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "warriors_triumph": { + "Mystical Space Typhoon" + ], + "warriors_triumph": [ "Gearfried the Iron Knight", "D.D. Warrior Lady", "Marauding Captain", @@ -60,9 +60,9 @@ "Reload", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "spellcasters_judgement": { + "Mystical Space Typhoon" + ], + "spellcasters_judgement": [ "Dark Magician", "Apprentice Magician", "Breaker the Magical Warrior", @@ -70,14 +70,18 @@ "Skilled Dark Magician", "Tsukuyomi", "Magical Dimension", - "Mage PowerSpell-Counter Cards", + "Mage Power", + "Spell-Counter Cards", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "none": {}, + "Mystical Space Typhoon" + ], + "none": [], } def get_deck_content_locations(deck: str) -> Dict[str, str]: - return {f"{deck} {i}": content for i, content in enumerate(structure_contents[deck])} + return { + f"{deck} {i}": content + for i, content in enumerate(structure_contents[deck], 1) + } diff --git a/worlds/zillion/client.py b/worlds/zillion/client.py index be32028463c7..09d0565e1c5e 100644 --- a/worlds/zillion/client.py +++ b/worlds/zillion/client.py @@ -347,6 +347,11 @@ def process_from_game_queue(self) -> None: "operations": [{"operation": "replace", "value": doors_b64}] } async_start(self.send_msgs([payload])) + elif isinstance(event_from_game, events.MapEventFromGame): + row = event_from_game.map_index // 8 + col = event_from_game.map_index % 8 + room_name = f"({chr(row + 64)}-{col + 1})" + logger.info(f"You are at {room_name}") else: logger.warning(f"WARNING: unhandled event from game {event_from_game}") diff --git a/worlds/zillion/gen_data.py b/worlds/zillion/gen_data.py index aa24ff8961b3..13cbee9ced20 100644 --- a/worlds/zillion/gen_data.py +++ b/worlds/zillion/gen_data.py @@ -28,6 +28,13 @@ def to_json(self) -> str: def from_json(gen_data_str: str) -> "GenData": """ the reverse of `to_json` """ from_json = json.loads(gen_data_str) + + # backwards compatibility for seeds generated before new map_gen options + room_gen = from_json["zz_game"]["options"].get("room_gen", None) + if room_gen is not None: + from_json["zz_game"]["options"]["map_gen"] = {False: "none", True: "rooms"}.get(room_gen, "none") + del from_json["zz_game"]["options"]["room_gen"] + return GenData( from_json["multi_items"], ZzGame.from_jsonable(from_json["zz_game"]), diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt index b4f554902f48..d6b01ac107ae 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1,2 +1,2 @@ -zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@4a2fec0aa1c529df866e510cdfcf6dca4d53679b#0.8.0 +zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@33045067f626266850f91c8045b9d3a9f52d02b0#0.9.0 typing-extensions>=4.7, <5