diff --git a/BaseClasses.py b/BaseClasses.py index 88857f8032..1c7dad7f3b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -680,13 +680,13 @@ class CollectionState(): def can_reach_region(self, spot: str, player: int) -> bool: return self.multiworld.get_region(spot, player).can_reach(self) - def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None: + def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None: if locations is None: locations = self.multiworld.get_filled_locations() reachable_events = True # since the loop has a good chance to run more than once, only filter the events once - locations = {location for location in locations if location.advancement and location not in self.events and - not key_only or getattr(location.item, "locked_dungeon_item", False)} + locations = {location for location in locations if location.advancement and location not in self.events} + while reachable_events: reachable_events = {location for location in locations if location.can_reach(self)} locations -= reachable_events @@ -1291,8 +1291,6 @@ class Spoiler: state = CollectionState(multiworld) collection_spheres = [] while required_locations: - state.sweep_for_events(key_only=True) - sphere = set(filter(state.can_reach, required_locations)) for location in sphere: diff --git a/CommonClient.py b/CommonClient.py index f8d1fcb7a2..09937e4b9a 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -61,6 +61,7 @@ class ClientCommandProcessor(CommandProcessor): if address: self.ctx.server_address = None self.ctx.username = None + self.ctx.password = None elif not self.ctx.server_address: self.output("Please specify an address.") return False @@ -514,6 +515,7 @@ class CommonContext: async def shutdown(self): self.server_address = "" self.username = None + self.password = None self.cancel_autoreconnect() if self.server and not self.server.socket.closed: await self.server.socket.close() diff --git a/Fill.py b/Fill.py index 4967ff0736..5185bbb60e 100644 --- a/Fill.py +++ b/Fill.py @@ -646,7 +646,6 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None: def get_sphere_locations(sphere_state: CollectionState, locations: typing.Set[Location]) -> typing.Set[Location]: - sphere_state.sweep_for_events(key_only=True, locations=locations) return {loc for loc in locations if sphere_state.can_reach(loc)} def item_percentage(player: int, num: int) -> float: diff --git a/Main.py b/Main.py index de6b467f93..56b3a6545d 100644 --- a/Main.py +++ b/Main.py @@ -124,14 +124,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for player in multiworld.player_ids: exclusion_rules(multiworld, player, multiworld.worlds[player].options.exclude_locations.value) multiworld.worlds[player].options.priority_locations.value -= multiworld.worlds[player].options.exclude_locations.value + world_excluded_locations = set() for location_name in multiworld.worlds[player].options.priority_locations.value: try: location = multiworld.get_location(location_name, player) - except KeyError as e: # failed to find the given location. Check if it's a legitimate location - if location_name not in multiworld.worlds[player].location_name_to_id: - raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e - else: + except KeyError: + continue + + if location.progress_type != LocationProgressType.EXCLUDED: location.progress_type = LocationProgressType.PRIORITY + else: + logger.warning(f"Unable to prioritize location \"{location_name}\" in player {player}'s world because the world excluded it.") + world_excluded_locations.add(location_name) + multiworld.worlds[player].options.priority_locations.value -= world_excluded_locations # Set local and non-local item rules. if multiworld.players > 1: diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 8e567afc35..75b5fb0202 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -79,7 +79,7 @@ class TrackerData: # Normal lookup tables as well. self.item_name_to_id[game] = game_package["item_name_to_id"] - self.location_name_to_id[game] = game_package["item_name_to_id"] + self.location_name_to_id[game] = game_package["location_name_to_id"] def get_seed_name(self) -> str: """Retrieves the seed name.""" diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 3b40d7e77a..ab841e65ee 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -1,8 +1,8 @@ # Archipelago World Code Owners / Maintainers Document # -# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder. For any pull -# requests that modify these worlds, a code owner must approve the PR in addition to a core maintainer. This is not to -# be used for files/folders outside the /worlds folder, those will always need sign off from a core maintainer. +# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder as well as +# certain documentation. For any pull requests that modify these worlds/docs, a code owner must approve the PR in +# addition to a core maintainer. All other files and folders are owned and maintained by core maintainers directly. # # All usernames must be GitHub usernames (and are case sensitive). @@ -226,3 +226,11 @@ # Ori and the Blind Forest # /worlds_disabled/oribf/ + +################### +## Documentation ## +################### + +# Apworld Dev Faq +/docs/apworld_dev_faq.md @qwint @ScipioWright + diff --git a/docs/apworld_dev_faq.md b/docs/apworld_dev_faq.md new file mode 100644 index 0000000000..8d9429afa3 --- /dev/null +++ b/docs/apworld_dev_faq.md @@ -0,0 +1,45 @@ +# APWorld Dev FAQ + +This document is meant as a reference tool to show solutions to common problems when developing an apworld. +It is not intended to answer every question about Archipelago and it assumes you have read the other docs, +including [Contributing](contributing.md), [Adding Games](), and [World API](). + +--- + +### My game has a restrictive start that leads to fill errors + +Hint to the Generator that an item needs to be in sphere one with local_early_items. Here, `1` represents the number of "Sword" items to attempt to place in sphere one. +```py +early_item_name = "Sword" +self.multiworld.local_early_items[self.player][early_item_name] = 1 +``` + +Some alternative ways to try to fix this problem are: +* Add more locations to sphere one of your world, potentially only when there would be a restrictive start +* Pre-place items yourself, such as during `create_items` +* Put items into the player's starting inventory using `push_precollected` +* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a restrictive start + +--- + +### I have multiple settings that change the item/location pool counts and need to balance them out + +In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be unbalanced. But in real, complex situations, that might be unfeasible. + +If that's the case, you can create extra filler based on the difference between your unfilled locations and your itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations) to your list of items to submit + +Note: to use self.create_filler(), self.get_filler_item_name() should be defined to only return valid filler item names +```py +total_locations = len(self.multiworld.get_unfilled_locations(self.player)) +item_pool = self.create_non_filler_items() + +for _ in range(total_locations - len(item_pool)): + item_pool.append(self.create_filler()) + +self.multiworld.itempool += item_pool +``` + +A faster alternative to the `for` loop would be to use a [list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions): +```py +item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))] +``` diff --git a/kvui.py b/kvui.py index a63d636960..f83590a819 100644 --- a/kvui.py +++ b/kvui.py @@ -596,6 +596,7 @@ class GameManager(App): def connect_button_action(self, button): self.ctx.username = None + self.ctx.password = None if self.ctx.server: async_start(self.ctx.disconnect()) else: diff --git a/settings.py b/settings.py index 7ab618c344..7927705214 100644 --- a/settings.py +++ b/settings.py @@ -3,6 +3,7 @@ Application settings / host.yaml interface using type hints. This is different from player options. """ +import os import os.path import shutil import sys @@ -11,7 +12,6 @@ import warnings from enum import IntEnum from threading import Lock from typing import cast, Any, BinaryIO, ClassVar, Dict, Iterator, List, Optional, TextIO, Tuple, Union, TypeVar -import os __all__ = [ "get_settings", "fmt_doc", "no_gui", @@ -798,6 +798,7 @@ class Settings(Group): atexit.register(autosave) def save(self, location: Optional[str] = None) -> None: # as above + from Utils import parse_yaml location = location or self._filename assert location, "No file specified" temp_location = location + ".tmp" # not using tempfile to test expected file access @@ -807,10 +808,18 @@ class Settings(Group): # can't use utf-8-sig because it breaks backward compat: pyyaml on Windows with bytes does not strip the BOM with open(temp_location, "w", encoding="utf-8") as f: self.dump(f) - # replace old with new - if os.path.exists(location): + f.flush() + if hasattr(os, "fsync"): + os.fsync(f.fileno()) + # validate new file is valid yaml + with open(temp_location, encoding="utf-8") as f: + parse_yaml(f.read()) + # replace old with new, try atomic operation first + try: + os.rename(temp_location, location) + except (OSError, FileExistsError): os.unlink(location) - os.rename(temp_location, location) + os.rename(temp_location, location) self._filename = location def dump(self, f: TextIO, level: int = 0) -> None: @@ -832,7 +841,6 @@ def get_settings() -> Settings: with _lock: # make sure we only have one instance res = getattr(get_settings, "_cache", None) if not res: - import os from Utils import user_path, local_path filenames = ("options.yaml", "host.yaml") locations: List[str] = [] diff --git a/test/general/test_host_yaml.py b/test/general/test_host_yaml.py index 7174befca4..3edbd34a51 100644 --- a/test/general/test_host_yaml.py +++ b/test/general/test_host_yaml.py @@ -1,11 +1,12 @@ import os +import os.path import unittest from io import StringIO -from tempfile import TemporaryFile +from tempfile import TemporaryDirectory, TemporaryFile from typing import Any, Dict, List, cast import Utils -from settings import Settings, Group +from settings import Group, Settings, ServerOptions class TestIDs(unittest.TestCase): @@ -80,3 +81,27 @@ class TestSettingsDumper(unittest.TestCase): self.assertEqual(value_spaces[2], value_spaces[0]) # start of sub-list self.assertGreater(value_spaces[3], value_spaces[0], f"{value_lines[3]} should have more indentation than {value_lines[0]} in {lines}") + + +class TestSettingsSave(unittest.TestCase): + def test_save(self) -> None: + """Test that saving and updating works""" + with TemporaryDirectory() as d: + filename = os.path.join(d, "host.yaml") + new_release_mode = ServerOptions.ReleaseMode("enabled") + # create default host.yaml + settings = Settings(None) + settings.save(filename) + self.assertTrue(os.path.exists(filename), + "Default settings could not be saved") + self.assertNotEqual(settings.server_options.release_mode, new_release_mode, + "Unexpected default release mode") + # update host.yaml + settings.server_options.release_mode = new_release_mode + settings.save(filename) + self.assertFalse(os.path.exists(filename + ".tmp"), + "Temp file was not removed during save") + # read back host.yaml + settings = Settings(filename) + self.assertEqual(settings.server_options.release_mode, new_release_mode, + "Settings were not overwritten") diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 3ef83fe81e..54c6e6b5d3 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -39,7 +39,7 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]: continue else: if name == "Scooter Badge": - if world.options.CTRLogic is CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE: + if world.options.CTRLogic == CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE: item_type = ItemClassification.progression elif name == "No Bonk Badge" and world.is_dw(): item_type = ItemClassification.progression diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index c6aeaa3577..8cb3782bde 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -659,6 +659,10 @@ def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region, if exit_act.name not in chapter_finales: return False + exit_chapter: str = act_chapters.get(exit_act.name) + # make sure that certain time rift combinations never happen + always_block: bool = exit_chapter != "Mafia Town" and exit_chapter != "Subcon Forest" + if not ignore_certain_rules or always_block: if entrance_act.name in rift_access_regions and exit_act.name in rift_access_regions[entrance_act.name]: return False @@ -684,9 +688,12 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: if act.name not in guaranteed_first_acts: return False + if world.options.ActRandomizer == ActRandomizer.option_light and "Time Rift" in act.name: + return False + # If there's only a single level in the starting chapter, only allow Mafia Town or Subcon Forest levels start_chapter = world.options.StartingChapter - if start_chapter is ChapterIndex.ALPINE or start_chapter is ChapterIndex.SUBCON: + if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON: if "Time Rift" in act.name: return False @@ -723,7 +730,8 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: elif act.name == "Contractual Obligations" and world.options.ShuffleSubconPaintings: return False - if world.options.ShuffleSubconPaintings and act_chapters.get(act.name, "") == "Subcon Forest": + if world.options.ShuffleSubconPaintings and "Time Rift" not in act.name \ + and act_chapters.get(act.name, "") == "Subcon Forest": # Only allow Subcon levels if painting skips are allowed if diff < Difficulty.MODERATE or world.options.NoPaintingSkips: return False diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index b0513c4332..b716b793a7 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -1,7 +1,6 @@ from worlds.AutoWorld import CollectionState from worlds.generic.Rules import add_rule, set_rule -from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \ - shop_locations, event_locs +from .Locations import location_table, zipline_unlocks, is_location_valid, shop_locations, event_locs from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HitType from BaseClasses import Location, Entrance, Region from typing import TYPE_CHECKING, List, Callable, Union, Dict @@ -148,14 +147,14 @@ def set_rules(world: "HatInTimeWorld"): if world.is_dlc1(): chapter_list.append(ChapterIndex.CRUISE) - if world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + if world.is_dlc2() and final_chapter != ChapterIndex.METRO: chapter_list.append(ChapterIndex.METRO) chapter_list.remove(starting_chapter) world.random.shuffle(chapter_list) # Make sure Alpine is unlocked before any DLC chapters are, as the Alpine door needs to be open to access them - if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()): + if starting_chapter != ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()): index1 = 69 index2 = 69 pos: int @@ -165,7 +164,7 @@ def set_rules(world: "HatInTimeWorld"): if world.is_dlc1(): index1 = chapter_list.index(ChapterIndex.CRUISE) - if world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + if world.is_dlc2() and final_chapter != ChapterIndex.METRO: index2 = chapter_list.index(ChapterIndex.METRO) lowest_index = min(index1, index2) @@ -242,9 +241,6 @@ def set_rules(world: "HatInTimeWorld"): if not is_location_valid(world, key): continue - if key in contract_locations.keys(): - continue - loc = world.multiworld.get_location(key, world.player) for hat in data.required_hats: @@ -256,7 +252,7 @@ def set_rules(world: "HatInTimeWorld"): if data.paintings > 0 and world.options.ShuffleSubconPaintings: add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) - if data.hit_type is not HitType.none and world.options.UmbrellaLogic: + if data.hit_type != HitType.none and world.options.UmbrellaLogic: if data.hit_type == HitType.umbrella: add_rule(loc, lambda state: state.has("Umbrella", world.player)) @@ -518,7 +514,7 @@ def set_hard_rules(world: "HatInTimeWorld"): lambda state: can_use_hat(state, world, HatType.ICE)) # Hard: clear Rush Hour with Brewing Hat only - if world.options.NoTicketSkips is not NoTicketSkips.option_true: + if world.options.NoTicketSkips != NoTicketSkips.option_true: set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: can_use_hat(state, world, HatType.BREWING)) else: diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 15140379b9..dd5e88abbc 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,15 +1,16 @@ from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \ - calculate_yarn_costs + calculate_yarn_costs, alps_hooks from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \ get_total_locations -from .Rules import set_rules +from .Rules import set_rules, has_paintings from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal, create_option_groups -from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item +from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item, Difficulty from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses from worlds.AutoWorld import World, WebWorld, CollectionState +from worlds.generic.Rules import add_rule from typing import List, Dict, TextIO from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type from Utils import local_path @@ -86,19 +87,27 @@ class HatInTimeWorld(World): if self.is_dw_only(): return - # If our starting chapter is 4 and act rando isn't on, force hookshot into inventory - # If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock - start_chapter: ChapterIndex = ChapterIndex(self.options.StartingChapter) + # Take care of some extremely restrictive starts in other chapters with act shuffle off + if not self.options.ActRandomizer: + start_chapter = self.options.StartingChapter + if start_chapter == ChapterIndex.ALPINE: + self.multiworld.push_precollected(self.create_item("Hookshot Badge")) + if self.options.UmbrellaLogic: + self.multiworld.push_precollected(self.create_item("Umbrella")) - if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON: - if not self.options.ActRandomizer: - if start_chapter == ChapterIndex.ALPINE: - self.multiworld.push_precollected(self.create_item("Hookshot Badge")) - if self.options.UmbrellaLogic: - self.multiworld.push_precollected(self.create_item("Umbrella")) - - if start_chapter == ChapterIndex.SUBCON and self.options.ShuffleSubconPaintings: + if self.options.ShuffleAlpineZiplines: + ziplines = list(alps_hooks.keys()) + ziplines.remove("Zipline Unlock - The Twilight Bell Path") # not enough checks from this one + self.multiworld.push_precollected(self.create_item(self.random.choice(ziplines))) + elif start_chapter == ChapterIndex.SUBCON: + if self.options.ShuffleSubconPaintings: self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock")) + elif start_chapter == ChapterIndex.BIRDS: + if self.options.UmbrellaLogic: + if self.options.LogicDifficulty < Difficulty.EXPERT: + self.multiworld.push_precollected(self.create_item("Umbrella")) + elif self.options.LogicDifficulty < Difficulty.MODERATE: + self.multiworld.push_precollected(self.create_item("Umbrella")) def create_regions(self): # noinspection PyClassVar @@ -119,7 +128,10 @@ class HatInTimeWorld(World): # place vanilla contract locations if contract shuffle is off if not self.options.ShuffleActContracts: for name in contract_locations.keys(): - self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name)) + loc = self.get_location(name) + loc.place_locked_item(create_item(self, name)) + if self.options.ShuffleSubconPaintings and loc.name != "Snatcher's Contract - The Subcon Well": + add_rule(loc, lambda state: has_paintings(state, self, 1)) def create_items(self): if self.has_yarn(): @@ -317,7 +329,7 @@ class HatInTimeWorld(World): def remove(self, state: "CollectionState", item: "Item") -> bool: old_count: int = state.count(item.name, self.player) - change = super().collect(state, item) + change = super().remove(state, item) if change and old_count == 1: if "Stamp" in item.name: if "2 Stamp" in item.name: diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index 509869fc25..23b3490707 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -12,41 +12,29 @@ ## Instructions -1. Have Steam running. Open the Steam console with this link: [steam://open/console](steam://open/console) -This may not work for some browsers. If that's the case, and you're on Windows, open the Run dialog using Win+R, -paste the link into the box, and hit Enter. +1. **BACK UP YOUR SAVE FILES IN YOUR MAIN INSTALL IF YOU CARE ABOUT THEM!!!** + Go to `steamapps/common/HatinTime/HatinTimeGame/SaveData/` and copy everything inside that folder over to a safe place. + **This is important! Changing the game version CAN and WILL break your existing save files!!!** -2. In the Steam console, enter the following command: -`download_depot 253230 253232 7770543545116491859`. ***Wait for the console to say the download is finished!*** -This can take a while to finish (30+ minutes) depending on your connection speed, so please be patient. Additionally, -**try to prevent your connection from being interrupted or slowed while Steam is downloading the depot,** -or else the download may potentially become corrupted (see first FAQ issue below). +2. In your Steam library, right-click on **A Hat in Time** in the list of games and click on **Properties**. -3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. +3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`. + While it downloads, you can subscribe to the [Archipelago workshop mod.]((https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601)) -4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder. +4. Once the game finishes downloading, start it up. + In Game Settings, make sure **Enable Developer Console** is checked. -5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. -In this new text file, input the number **253230** on the first line. - - -6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. -You will use this shortcut to open the Archipelago-compatible version of A Hat in Time. - - -7. Start up the game using your new shortcut. To confirm if you are on the correct version, -go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running -the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked. +5. You should now be good to go. See below for more details on how to use the mod and connect to an Archipelago game. ## Connecting to the Archipelago server -To connect to the multiworld server, simply run the **ArchipelagoAHITClient** -(or run it from the Launcher if you have the apworld installed) and connect it to the Archipelago server. +To connect to the multiworld server, simply run the **Archipelago AHIT Client** from the Launcher +and connect it to the Archipelago server. The game will connect to the client automatically when you create a new save file. @@ -61,33 +49,8 @@ make sure ***Enable Developer Console*** is checked in Game Settings and press t ## FAQ/Common Issues -### I followed the setup, but I receive an odd error message upon starting the game or creating a save file! -If you receive an error message such as -**"Failed to find default engine .ini to retrieve My Documents subdirectory to use. Force quitting."** or -**"Failed to load map "hub_spaceship"** after booting up the game or creating a save file respectively, then the depot -download was likely corrupted. The only way to fix this is to start the entire download all over again. -Unfortunately, this appears to be an underlying issue with Steam's depot downloader. The only way to really prevent this -from happening is to ensure that your connection is not interrupted or slowed while downloading. -### The game keeps crashing on startup after the splash screen! -This issue is unfortunately very hard to fix, and the underlying cause is not known. If it does happen however, -try the following: - -- Close Steam **entirely**. -- Open the downpatched version of the game (with Steam closed) and allow it to load to the titlescreen. -- Close the game, and then open Steam again. -- After launching the game, the issue should hopefully disappear. If not, repeat the above steps until it does. - -### I followed the setup, but "Live Game Events" still shows up in the options menu! -The most common cause of this is the `steam_appid.txt` file. If you're on Windows 10, file extensions are hidden by -default (thanks Microsoft). You likely made the mistake of still naming the file `steam_appid.txt`, which, since file -extensions are hidden, would result in the file being named `steam_appid.txt.txt`, which is incorrect. -To show file extensions in Windows 10, open any folder, click the View tab at the top, and check -"File name extensions". Then you can correct the name of the file. If the name of the file is correct, -and you're still running into the issue, re-read the setup guide again in case you missed a step. -If you still can't get it to work, ask for help in the Discord thread. - -### The game is running on the older version, but it's not connecting when starting a new save! +### The game is not connecting when starting a new save! For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu (rocket icon) in-game, and re-enable the mod. diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py index 769dcc1998..328e28da93 100644 --- a/worlds/alttp/SubClasses.py +++ b/worlds/alttp/SubClasses.py @@ -76,10 +76,6 @@ class ALttPItem(Item): if self.type in {"SmallKey", "BigKey", "Map", "Compass"}: return self.type - @property - def locked_dungeon_item(self): - return self.location.locked and self.dungeon_item - class LTTPRegionType(IntEnum): LightWorld = 1 diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 8ce0b45a5f..ace231e12b 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -660,11 +660,18 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi end local tech local force = game.forces["player"] + if call.parameter == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + end chunks = split(call.parameter, "\t") local item_name = chunks[1] local index = chunks[2] local source = chunks[3] or "Archipelago" - if index == -1 then -- for coop sync and restoring from an older savegame + if index == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + elseif index == -1 then -- for coop sync and restoring from an older savegame tech = force.technologies[item_name] if tech.researched ~= true then game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."}) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 38be2cd794..e2602036a2 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -1,10 +1,12 @@ import typing import re +from dataclasses import dataclass, make_dataclass + from .ExtractedData import logic_options, starts, pool_options from .Rules import cost_terms from schema import And, Schema, Optional -from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink +from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink, PerGameCommonOptions from .Charms import vanilla_costs, names as charm_names if typing.TYPE_CHECKING: @@ -538,3 +540,5 @@ hollow_knight_options: typing.Dict[str, type(Option)] = { }, **cost_sanity_weights } + +HKOptions = make_dataclass("HKOptions", [(name, option) for name, option in hollow_knight_options.items()], bases=(PerGameCommonOptions,)) diff --git a/worlds/hk/Rules.py b/worlds/hk/Rules.py index a3c7e13cf0..e162e1dfa8 100644 --- a/worlds/hk/Rules.py +++ b/worlds/hk/Rules.py @@ -49,3 +49,42 @@ def set_rules(hk_world: World): if term == "GEO": # No geo logic! continue add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount) + + +def _hk_nail_combat(state, player) -> bool: + return state.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player) + + +def _hk_can_beat_thk(state, player) -> bool: + return ( + state.has('Opened_Black_Egg_Temple', player) + and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1 + and _hk_nail_combat(state, player) + and ( + state.has_any({'LEFTDASH', 'RIGHTDASH'}, player) + or state._hk_option(player, 'ProficientCombat') + ) + and state.has('FOCUS', player) + ) + + +def _hk_siblings_ending(state, player) -> bool: + return _hk_can_beat_thk(state, player) and state.has('WHITEFRAGMENT', player, 3) + + +def _hk_can_beat_radiance(state, player) -> bool: + return ( + state.has('Opened_Black_Egg_Temple', player) + and _hk_nail_combat(state, player) + and state.has('WHITEFRAGMENT', player, 3) + and state.has('DREAMNAIL', player) + and ( + (state.has('LEFTCLAW', player) and state.has('RIGHTCLAW', player)) + or state.has('WINGS', player) + ) + and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1 + and ( + (state.has('LEFTDASH', player, 2) and state.has('RIGHTDASH', player, 2)) # Both Shade Cloaks + or (state._hk_option(player, 'ProficientCombat') and state.has('QUAKE', player)) # or Dive + ) + ) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 78287305df..e5065876dd 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -10,9 +10,9 @@ logger = logging.getLogger("Hollow Knight") from .Items import item_table, lookup_type_to_names, item_name_groups from .Regions import create_regions -from .Rules import set_rules, cost_terms +from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \ - shop_to_option + shop_to_option, HKOptions from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \ event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs from .Charms import names as charm_names @@ -142,7 +142,8 @@ class HKWorld(World): As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils. """ # from https://www.hollowknight.com game: str = "Hollow Knight" - option_definitions = hollow_knight_options + options_dataclass = HKOptions + options: HKOptions web = HKWeb() @@ -155,8 +156,8 @@ class HKWorld(World): charm_costs: typing.List[int] cached_filler_items = {} - def __init__(self, world, player): - super(HKWorld, self).__init__(world, player) + def __init__(self, multiworld, player): + super(HKWorld, self).__init__(multiworld, player) self.created_multi_locations: typing.Dict[str, typing.List[HKLocation]] = { location: list() for location in multi_locations } @@ -165,29 +166,29 @@ class HKWorld(World): self.vanilla_shop_costs = deepcopy(vanilla_shop_costs) def generate_early(self): - world = self.multiworld - charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random) - self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs) - # world.exclude_locations[self.player].value.update(white_palace_locations) + options = self.options + charm_costs = options.RandomCharmCosts.get_costs(self.random) + self.charm_costs = options.PlandoCharmCosts.get_costs(charm_costs) + # options.exclude_locations.value.update(white_palace_locations) for term, data in cost_terms.items(): - mini = getattr(world, f"Minimum{data.option}Price")[self.player] - maxi = getattr(world, f"Maximum{data.option}Price")[self.player] + mini = getattr(options, f"Minimum{data.option}Price") + maxi = getattr(options, f"Maximum{data.option}Price") # if minimum > maximum, set minimum to maximum mini.value = min(mini.value, maxi.value) self.ranges[term] = mini.value, maxi.value - world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key], + self.multiworld.push_precollected(HKItem(starts[options.StartLocation.current_key], True, None, "Event", self.player)) def white_palace_exclusions(self): exclusions = set() - wp = self.multiworld.WhitePalace[self.player] + wp = self.options.WhitePalace if wp <= WhitePalace.option_nopathofpain: exclusions.update(path_of_pain_locations) if wp <= WhitePalace.option_kingfragment: exclusions.update(white_palace_checks) if wp == WhitePalace.option_exclude: exclusions.add("King_Fragment") - if self.multiworld.RandomizeCharms[self.player]: + if self.options.RandomizeCharms: # If charms are randomized, this will be junk-filled -- so transitions and events are not progression exclusions.update(white_palace_transitions) exclusions.update(white_palace_events) @@ -200,7 +201,7 @@ class HKWorld(World): # check for any goal that godhome events are relevant to all_event_names = event_names.copy() - if self.multiworld.Goal[self.player] in [Goal.option_godhome, Goal.option_godhome_flower]: + if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower]: from .GodhomeData import godhome_event_names all_event_names.update(set(godhome_event_names)) @@ -230,12 +231,12 @@ class HKWorld(World): pool: typing.List[HKItem] = [] wp_exclusions = self.white_palace_exclusions() junk_replace: typing.Set[str] = set() - if self.multiworld.RemoveSpellUpgrades[self.player]: + if self.options.RemoveSpellUpgrades: junk_replace.update(("Abyss_Shriek", "Shade_Soul", "Descending_Dark")) randomized_starting_items = set() for attr, items in randomizable_starting_items.items(): - if getattr(self.multiworld, attr)[self.player]: + if getattr(self.options, attr): randomized_starting_items.update(items) # noinspection PyShadowingNames @@ -257,7 +258,7 @@ class HKWorld(World): if item_name in junk_replace: item_name = self.get_filler_item_name() - item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.multiworld.AddUnshuffledLocations[self.player] else self.create_event(item_name) + item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name) if location_name == "Start": if item_name in randomized_starting_items: @@ -281,55 +282,55 @@ class HKWorld(World): location.progress_type = LocationProgressType.EXCLUDED for option_key, option in hollow_knight_randomize_options.items(): - randomized = getattr(self.multiworld, option_key)[self.player] - if all([not randomized, option_key in logicless_options, not self.multiworld.AddUnshuffledLocations[self.player]]): + randomized = getattr(self.options, option_key) + if all([not randomized, option_key in logicless_options, not self.options.AddUnshuffledLocations]): continue for item_name, location_name in zip(option.items, option.locations): if item_name in junk_replace: item_name = self.get_filler_item_name() - if (item_name == "Crystal_Heart" and self.multiworld.SplitCrystalHeart[self.player]) or \ - (item_name == "Mothwing_Cloak" and self.multiworld.SplitMothwingCloak[self.player]): + if (item_name == "Crystal_Heart" and self.options.SplitCrystalHeart) or \ + (item_name == "Mothwing_Cloak" and self.options.SplitMothwingCloak): _add("Left_" + item_name, location_name, randomized) _add("Right_" + item_name, "Split_" + location_name, randomized) continue - if item_name == "Mantis_Claw" and self.multiworld.SplitMantisClaw[self.player]: + if item_name == "Mantis_Claw" and self.options.SplitMantisClaw: _add("Left_" + item_name, "Left_" + location_name, randomized) _add("Right_" + item_name, "Right_" + location_name, randomized) continue - if item_name == "Shade_Cloak" and self.multiworld.SplitMothwingCloak[self.player]: - if self.multiworld.random.randint(0, 1): + if item_name == "Shade_Cloak" and self.options.SplitMothwingCloak: + if self.random.randint(0, 1): item_name = "Left_Mothwing_Cloak" else: item_name = "Right_Mothwing_Cloak" - if item_name == "Grimmchild2" and self.multiworld.RandomizeGrimmkinFlames[self.player] and self.multiworld.RandomizeCharms[self.player]: + if item_name == "Grimmchild2" and self.options.RandomizeGrimmkinFlames and self.options.RandomizeCharms: _add("Grimmchild1", location_name, randomized) continue _add(item_name, location_name, randomized) - if self.multiworld.RandomizeElevatorPass[self.player]: + if self.options.RandomizeElevatorPass: randomized = True _add("Elevator_Pass", "Elevator_Pass", randomized) for shop, locations in self.created_multi_locations.items(): - for _ in range(len(locations), getattr(self.multiworld, shop_to_option[shop])[self.player].value): + for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value): loc = self.create_location(shop) unfilled_locations += 1 # Balance the pool item_count = len(pool) - additional_shop_items = max(item_count - unfilled_locations, self.multiworld.ExtraShopSlots[self.player].value) + additional_shop_items = max(item_count - unfilled_locations, self.options.ExtraShopSlots.value) # Add additional shop items, as needed. if additional_shop_items > 0: shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16) - if not self.multiworld.EggShopSlots[self.player].value: # No eggshop, so don't place items there + if not self.options.EggShopSlots: # No eggshop, so don't place items there shops.remove('Egg_Shop') if shops: for _ in range(additional_shop_items): - shop = self.multiworld.random.choice(shops) + shop = self.random.choice(shops) loc = self.create_location(shop) unfilled_locations += 1 if len(self.created_multi_locations[shop]) >= 16: @@ -355,7 +356,7 @@ class HKWorld(World): loc.costs = costs def apply_costsanity(self): - setting = self.multiworld.CostSanity[self.player].value + setting = self.options.CostSanity.value if not setting: return # noop @@ -369,10 +370,10 @@ class HKWorld(World): return {k: v for k, v in weights.items() if v} - random = self.multiworld.random - hybrid_chance = getattr(self.multiworld, f"CostSanityHybridChance")[self.player].value + random = self.random + hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value weights = { - data.term: getattr(self.multiworld, f"CostSanity{data.option}Weight")[self.player].value + data.term: getattr(self.options, f"CostSanity{data.option}Weight").value for data in cost_terms.values() } weights_geoless = dict(weights) @@ -427,22 +428,22 @@ class HKWorld(World): location.sort_costs() def set_rules(self): - world = self.multiworld + multiworld = self.multiworld player = self.player - goal = world.Goal[player] + goal = self.options.Goal if goal == Goal.option_hollowknight: - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) elif goal == Goal.option_siblings: - world.completion_condition[player] = lambda state: state._hk_siblings_ending(player) + multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player) elif goal == Goal.option_radiance: - world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_radiance(state, player) elif goal == Goal.option_godhome: - world.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) + multiworld.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) elif goal == Goal.option_godhome_flower: - world.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) + multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) else: # Any goal - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) or _hk_can_beat_radiance(state, player) set_rules(self) @@ -450,8 +451,8 @@ class HKWorld(World): slot_data = {} options = slot_data["options"] = {} - for option_name in self.option_definitions: - option = getattr(self.multiworld, option_name)[self.player] + for option_name in hollow_knight_options: + option = getattr(self.options, option_name) try: optionvalue = int(option.value) except TypeError: @@ -460,10 +461,10 @@ class HKWorld(World): options[option_name] = optionvalue # 32 bit int - slot_data["seed"] = self.multiworld.per_slot_randoms[self.player].randint(-2147483647, 2147483646) + slot_data["seed"] = self.random.randint(-2147483647, 2147483646) # Backwards compatibility for shop cost data (HKAP < 0.1.0) - if not self.multiworld.CostSanity[self.player]: + if not self.options.CostSanity: for shop, terms in shop_cost_types.items(): unit = cost_terms[next(iter(terms))].option if unit == "Geo": @@ -498,7 +499,7 @@ class HKWorld(World): basename = name if name in shop_cost_types: costs = { - term: self.multiworld.random.randint(*self.ranges[term]) + term: self.random.randint(*self.ranges[term]) for term in shop_cost_types[name] } elif name in vanilla_location_costs: @@ -512,7 +513,7 @@ class HKWorld(World): region = self.multiworld.get_region("Menu", self.player) - if vanilla and not self.multiworld.AddUnshuffledLocations[self.player]: + if vanilla and not self.options.AddUnshuffledLocations: loc = HKLocation(self.player, name, None, region, costs=costs, vanilla=vanilla, basename=basename) @@ -554,31 +555,32 @@ class HKWorld(World): for effect_name, effect_value in item_effects.get(item.name, {}).items(): if state.prog_items[item.player][effect_name] == effect_value: del state.prog_items[item.player][effect_name] - state.prog_items[item.player][effect_name] -= effect_value + else: + state.prog_items[item.player][effect_name] -= effect_value return change @classmethod - def stage_write_spoiler(cls, world: MultiWorld, spoiler_handle): - hk_players = world.get_game_players(cls.game) + def stage_write_spoiler(cls, multiworld: MultiWorld, spoiler_handle): + hk_players = multiworld.get_game_players(cls.game) spoiler_handle.write('\n\nCharm Notches:') for player in hk_players: - name = world.get_player_name(player) + name = multiworld.get_player_name(player) spoiler_handle.write(f'\n{name}\n') - hk_world: HKWorld = world.worlds[player] + hk_world: HKWorld = multiworld.worlds[player] for charm_number, cost in enumerate(hk_world.charm_costs): spoiler_handle.write(f"\n{charm_names[charm_number]}: {cost}") spoiler_handle.write('\n\nShop Prices:') for player in hk_players: - name = world.get_player_name(player) + name = multiworld.get_player_name(player) spoiler_handle.write(f'\n{name}\n') - hk_world: HKWorld = world.worlds[player] + hk_world: HKWorld = multiworld.worlds[player] - if world.CostSanity[player].value: + if hk_world.options.CostSanity: for loc in sorted( ( - loc for loc in itertools.chain(*(region.locations for region in world.get_regions(player))) + loc for loc in itertools.chain(*(region.locations for region in multiworld.get_regions(player))) if loc.costs ), key=operator.attrgetter('name') ): @@ -602,15 +604,15 @@ class HKWorld(World): 'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests', 'RandomizeRancidEggs' ): - if getattr(self.multiworld, group): + if getattr(self.options, group): fillers.extend(item for item in hollow_knight_randomize_options[group].items if item not in exclusions) self.cached_filler_items[self.player] = fillers - return self.multiworld.random.choice(self.cached_filler_items[self.player]) + return self.random.choice(self.cached_filler_items[self.player]) -def create_region(world: MultiWorld, player: int, name: str, location_names=None) -> Region: - ret = Region(name, player, world) +def create_region(multiworld: MultiWorld, player: int, name: str, location_names=None) -> Region: + ret = Region(name, player, multiworld) if location_names: for location in location_names: loc_id = HKWorld.location_name_to_id.get(location, None) @@ -683,42 +685,7 @@ class HKLogicMixin(LogicMixin): return sum(self.multiworld.worlds[player].charm_costs[notch] for notch in notches) def _hk_option(self, player: int, option_name: str) -> int: - return getattr(self.multiworld, option_name)[player].value + return getattr(self.multiworld.worlds[player].options, option_name).value def _hk_start(self, player, start_location: str) -> bool: - return self.multiworld.StartLocation[player] == start_location - - def _hk_nail_combat(self, player: int) -> bool: - return self.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player) - - def _hk_can_beat_thk(self, player: int) -> bool: - return ( - self.has('Opened_Black_Egg_Temple', player) - and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1 - and self._hk_nail_combat(player) - and ( - self.has_any({'LEFTDASH', 'RIGHTDASH'}, player) - or self._hk_option(player, 'ProficientCombat') - ) - and self.has('FOCUS', player) - ) - - def _hk_siblings_ending(self, player: int) -> bool: - return self._hk_can_beat_thk(player) and self.has('WHITEFRAGMENT', player, 3) - - def _hk_can_beat_radiance(self, player: int) -> bool: - return ( - self.has('Opened_Black_Egg_Temple', player) - and self._hk_nail_combat(player) - and self.has('WHITEFRAGMENT', player, 3) - and self.has('DREAMNAIL', player) - and ( - (self.has('LEFTCLAW', player) and self.has('RIGHTCLAW', player)) - or self.has('WINGS', player) - ) - and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1 - and ( - (self.has('LEFTDASH', player, 2) and self.has('RIGHTDASH', player, 2)) # Both Shade Cloaks - or (self._hk_option(player, 'ProficientCombat') and self.has('QUAKE', player)) # or Dive - ) - ) + return self.multiworld.worlds[player].options.StartLocation == start_location diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 3b67617873..9853be73fa 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -9,7 +9,7 @@ from worlds.AutoWorld import WebWorld, World from .datatypes import Room, RoomEntrance from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, TRAP_ITEMS, LingoItem from .locations import ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP -from .options import LingoOptions, lingo_option_groups +from .options import LingoOptions, lingo_option_groups, SunwarpAccess, VictoryCondition from .player_logic import LingoPlayerLogic from .regions import create_regions @@ -54,14 +54,17 @@ class LingoWorld(World): player_logic: LingoPlayerLogic def generate_early(self): - if not (self.options.shuffle_doors or self.options.shuffle_colors or self.options.shuffle_sunwarps): + if not (self.options.shuffle_doors or self.options.shuffle_colors or + (self.options.sunwarp_access >= SunwarpAccess.option_unlock and + self.options.victory_condition == VictoryCondition.option_pilgrimage)): if self.multiworld.players == 1: - warning(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any progression" - f" items. Please turn on Door Shuffle, Color Shuffle, or Sunwarp Shuffle if that doesn't seem" - f" right.") + warning(f"{self.player_name}'s Lingo world doesn't have any progression items. Please turn on Door" + f" Shuffle or Color Shuffle, or use item-blocked sunwarps with the Pilgrimage victory condition" + f" if that doesn't seem right.") else: - raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any" - f" progression items. Please turn on Door Shuffle, Color Shuffle or Sunwarp Shuffle.") + raise OptionError(f"{self.player_name}'s Lingo world doesn't have any progression items. Please turn on" + f" Door Shuffle or Color Shuffle, or use item-blocked sunwarps with the Pilgrimage" + f" victory condition.") self.player_logic = LingoPlayerLogic(self) @@ -167,7 +170,8 @@ class LingoWorld(World): slot_options = [ "death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels", "enable_pilgrimage", "sunwarp_access", "mastery_achievements", "level_2_requirement", "location_checks", - "early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps" + "early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps", + "group_doors" ] slot_data = { diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 3035446ef7..950fd32674 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -1,6 +1,13 @@ --- # This file is an associative array where the keys are region names. Rooms - # have four properties: entrances, panels, doors, and paintings. + # have a number of properties: + # - entrances + # - panels + # - doors + # - panel_doors + # - paintings + # - progression + # - sunwarps # # entrances is an array of regions from which this room can be accessed. The # key of each entry is the room that can access this one. The value is a list @@ -13,7 +20,7 @@ # room that the door is in. The room name may be omitted if the door is # located in the current room. # - # panels is an array of panels in the room. The key of the array is an + # panels is a named array of panels in the room. The key of the array is an # arbitrary name for the panel. Panels can have the following fields: # - id: The internal ID of the panel in the LINGO map # - required_room: In addition to having access to this room, the player must @@ -45,7 +52,7 @@ # - hunt: If True, the tracker will show this panel even when it is # not a check. Used for hunts like the Number Hunt. # - # doors is an array of doors associated with this room. When door + # doors is a named array of doors associated with this room. When door # randomization is enabled, each of these is an item. The key is a name that # will be displayed as part of the item's name. Doors can have the following # fields: @@ -78,6 +85,18 @@ # - event: Denotes that the door is event only. This is similar to # setting both skip_location and skip_item. # + # panel_doors is a named array of "panel doors" associated with this room. + # When panel door shuffle is enabled, each of these becomes an item, and those + # items block access to the listed panels. The key is a name for internal + # reference only. Panel doors can have the following fields: + # - panels: Required. This is the set of panels that are blocked by this + # panel door. + # - item_name: Overrides the name of the item generated for this panel + # door. If not specified, the item name will be generated from + # the room name and the name(s) of the panel(s). + # - panel_group: When region grouping is enabled, all panel doors with the + # same group will be covered by a single item. + # # paintings is an array of paintings in the room. This is used for painting # shuffling. # - id: The internal painting ID from the LINGO map. @@ -105,6 +124,14 @@ # fine in door shuffle mode. # - move: Denotes that the painting is able to move. # + # progression is a named array of items that define an ordered set of items. + # progression items do not have any true connection to the rooms that they + # are defined in, but it is best to place them in a thematically appropriate + # room. The key for a progression entry is the name of the item that will be + # created. A progression entry is a dictionary with one or both of a "doors" + # key and a "panel_doors" key. These fields should be lists of doors or + # panel doors that will be contained in this progressive item. + # # sunwarps is an array of sunwarps in the room. This is used for sunwarp # shuffling. # - dots: The number of dots on this sunwarp. @@ -193,6 +220,10 @@ panel: RACECAR (Black) - room: The Tenacious panel: SOLOS (Black) + panel_doors: + HIDDEN: + panels: + - HIDDEN paintings: - id: arrows_painting exit_only: True @@ -303,6 +334,10 @@ panel: SOLOS (Black) - room: Hub Room panel: RAT + panel_doors: + OPEN: + panels: + - OPEN paintings: - id: owl_painting orientation: north @@ -317,7 +352,13 @@ panels: Achievement: id: Countdown Panels/Panel_seeker_seeker - required_room: Hidden Room + # The Seeker uniquely has the property that 1) it can be entered (through the Pilgrim Room) without opening the + # front door in panels mode door shuffle, and 2) the front door panel is part of the CDP. This necessitates this + # required_panel clause, because the entrance panel needs to be solvable for the achievement even if an + # alternate entrance to the room is used. + required_panel: + room: Hidden Room + panel: OPEN tag: forbid check: True achievement: The Seeker @@ -537,6 +578,23 @@ item_group: Achievement Room Entrances panels: - OPEN + panel_doors: + ORDER: + panels: + - ORDER + SLAUGHTER: + panel_group: Tenacious Entrance Panels + panels: + - SLAUGHTER + TRACE: + panels: + - TRACE + RAT: + panels: + - RAT + OPEN: + panels: + - OPEN paintings: - id: maze_painting orientation: west @@ -608,12 +666,13 @@ item_name: "6 Sunwarp" progression: Progressive Pilgrimage: - - 1 Sunwarp - - 2 Sunwarp - - 3 Sunwarp - - 4 Sunwarp - - 5 Sunwarp - - 6 Sunwarp + doors: + - 1 Sunwarp + - 2 Sunwarp + - 3 Sunwarp + - 4 Sunwarp + - 5 Sunwarp + - 6 Sunwarp Pilgrim Antechamber: # The entrances to this room are special. When pilgrimage is enabled, we use a special access rule to determine # whether a pilgrimage can succeed. When pilgrimage is disabled, the sun painting will be added to the pool. @@ -881,6 +940,24 @@ panel: READS + RUST - room: Ending Area panel: THE END + panel_doors: + DECAY: + panel_group: Tenacious Entrance Panels + panels: + - DECAY + NOPE: + panels: + - NOPE + WE ROT: + panels: + - WE ROT + WORDS SWORD: + panels: + - WORDS + - SWORD + BEND HI: + panels: + - BEND HI paintings: - id: eye_painting disable: True @@ -895,6 +972,14 @@ direction: exit entrance_indicator_pos: [ -17, 2.5, -41.01 ] orientation: north + progression: + Progressive Suits Area: + panel_doors: + - WORDS SWORD + - room: Lost Area + panel_door: LOST + - room: Amen Name Area + panel_door: AMEN NAME Lost Area: entrances: Outside The Agreeable: @@ -920,6 +1005,11 @@ panels: - LOST (1) - LOST (2) + panel_doors: + LOST: + panels: + - LOST (1) + - LOST (2) Amen Name Area: entrances: Crossroads: @@ -953,6 +1043,11 @@ panels: - AMEN - NAME + panel_doors: + AMEN NAME: + panels: + - AMEN + - NAME Suits Area: entrances: Amen Name Area: @@ -1056,6 +1151,13 @@ - LEVEL (White) - RACECAR (White) - SOLOS (White) + panel_doors: + Black Palindromes: + item_name: The Tenacious - Black Palindromes (Panels) + panels: + - LEVEL (Black) + - RACECAR (Black) + - SOLOS (Black) Near Far Area: entrances: Hub Room: True @@ -1081,6 +1183,21 @@ panels: - NEAR - FAR + panel_doors: + NEAR FAR: + item_name: Symmetry Room - NEAR, FAR (Panels) + panel_group: Symmetry Room Panels + panels: + - NEAR + - FAR + progression: + Progressive Symmetry Room: + panel_doors: + - NEAR FAR + - room: Warts Straw Area + panel_door: WARTS STRAW + - room: Leaf Feel Area + panel_door: LEAF FEEL Warts Straw Area: entrances: Near Far Area: @@ -1108,6 +1225,13 @@ panels: - WARTS - STRAW + panel_doors: + WARTS STRAW: + item_name: Symmetry Room - WARTS, STRAW (Panels) + panel_group: Symmetry Room Panels + panels: + - WARTS + - STRAW Leaf Feel Area: entrances: Warts Straw Area: @@ -1135,6 +1259,13 @@ panels: - LEAF - FEEL + panel_doors: + LEAF FEEL: + item_name: Symmetry Room - LEAF, FEEL (Panels) + panel_group: Symmetry Room Panels + panels: + - LEAF + - FEEL Outside The Agreeable: entrances: Crossroads: @@ -1243,6 +1374,20 @@ panels: - room: Color Hunt panel: PURPLE + panel_doors: + MASSACRED: + panel_group: Tenacious Entrance Panels + panels: + - MASSACRED + BLACK: + panels: + - BLACK + CLOSE: + panels: + - CLOSE + RIGHT: + panels: + - RIGHT paintings: - id: eyes_yellow_painting orientation: east @@ -1294,6 +1439,14 @@ - WINTER - DIAMONDS - FIRE + panel_doors: + Lookout: + item_name: Compass Room Panels + panels: + - NORTH + - WINTER + - DIAMONDS + - FIRE paintings: - id: pencil_painting7 orientation: north @@ -1403,6 +1556,8 @@ room: Owl Hallway door: Shortcut to Hedge Maze Roof: True + The Incomparable: + door: Observant Entrance panels: DOWN: id: Maze Room/Panel_down_up @@ -1510,6 +1665,10 @@ - HIDE (3) - room: Outside The Agreeable panel: HIDE + panel_doors: + DOWN: + panels: + - DOWN The Perceptive: entrances: Starting Room: @@ -1531,6 +1690,10 @@ check: True exclude_reduce: True tag: botwhite + panel_doors: + GAZE: + panels: + - GAZE paintings: - id: garden_painting_tower orientation: north @@ -1572,9 +1735,10 @@ - EAT progression: Progressive Fearless: - - Second Floor - - room: The Fearless (Second Floor) - door: Third Floor + doors: + - Second Floor + - room: The Fearless (Second Floor) + door: Third Floor The Fearless (Second Floor): entrances: The Fearless (First Floor): @@ -1669,6 +1833,10 @@ tag: forbid required_door: door: Stairs + required_panel: + - panel: FOUR (1) + - panel: FOUR (2) + - panel: SIX achievement: The Observant FOUR (1): id: Look Room/Panel_four_back @@ -1782,6 +1950,16 @@ door_group: Observant Doors panels: - SIX + panel_doors: + BACKSIDE: + item_name: The Observant - Backside Entrance Panels + panel_group: Backside Entrance Panels + panels: + - FOUR (1) + - FOUR (2) + STAIRS: + panels: + - SIX The Incomparable: entrances: The Observant: @@ -1791,6 +1969,9 @@ door: Eight Door Orange Tower Sixth Floor: painting: True + Hedge Maze: + room: Hedge Maze + door: Observant Entrance panels: Achievement: id: Countdown Panels/Panel_incomparable_incomparable @@ -1798,9 +1979,12 @@ check: True tag: forbid required_room: - - Elements Area - - Courtyard - Eight Room + required_panel: + - room: Courtyard + panel: I + - room: Elements Area + panel: A achievement: The Incomparable A (One): id: Strand Room/Panel_blank_a @@ -1865,6 +2049,15 @@ panel: I - room: Elements Area panel: A + panel_doors: + Giant Sevens: + item_name: Giant Seven Panels + panels: + - I (Seven) + - room: Courtyard + panel: I + - room: Elements Area + panel: A paintings: - id: crown_painting orientation: east @@ -1972,14 +2165,31 @@ panel: DRAWL + RUNS - room: Owl Hallway panel: READS + RUST + panel_doors: + Access: + item_name: Orange Tower Panels + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + - room: Orange Tower Fourth Floor + panel: LEARNS + UNSEW + - room: Orange Tower Fifth Floor + panel: DRAWL + RUNS + - room: Owl Hallway + panel: READS + RUST progression: Progressive Orange Tower: - - Second Floor - - Third Floor - - Fourth Floor - - Fifth Floor - - Sixth Floor - - Seventh Floor + doors: + - Second Floor + - Third Floor + - Fourth Floor + - Fifth Floor + - Sixth Floor + - Seventh Floor Orange Tower First Floor: entrances: Hub Room: @@ -2022,6 +2232,10 @@ - SALT - room: Directional Gallery panel: PEPPER + panel_doors: + SECRET: + panels: + - SECRET sunwarps: - dots: 4 direction: enter @@ -2174,6 +2388,10 @@ id: Shuffle Room Area Doors/Door_hotcrust_shortcuts panels: - HOT CRUSTS + panel_doors: + HOT CRUSTS: + panels: + - HOT CRUSTS sunwarps: - dots: 5 direction: enter @@ -2288,6 +2506,12 @@ panels: - SIZE (Small) - SIZE (Big) + panel_doors: + SIZE: + item_name: Orange Tower Fifth Floor - SIZE Panels + panels: + - SIZE (Small) + - SIZE (Big) paintings: - id: hi_solved_painting3 orientation: south @@ -2631,6 +2855,15 @@ - SECOND - THIRD - FOURTH + panel_doors: + FIRST SECOND THIRD FOURTH: + item_name: Courtyard - Ordinal Panels + panel_group: Backside Entrance Panels + panels: + - FIRST + - SECOND + - THIRD + - FOURTH The Colorful (White): entrances: Courtyard: True @@ -2648,6 +2881,12 @@ location_name: The Colorful - White panels: - BEGIN + panel_doors: + BEGIN: + item_name: The Colorful - BEGIN (Panel) + panel_group: Colorful Panels + panels: + - BEGIN The Colorful (Black): entrances: The Colorful (White): @@ -2668,6 +2907,12 @@ door_group: Colorful Doors panels: - FOUND + panel_doors: + FOUND: + item_name: The Colorful - FOUND (Panel) + panel_group: Colorful Panels + panels: + - FOUND The Colorful (Red): entrances: The Colorful (Black): @@ -2688,6 +2933,12 @@ door_group: Colorful Doors panels: - LOAF + panel_doors: + LOAF: + item_name: The Colorful - LOAF (Panel) + panel_group: Colorful Panels + panels: + - LOAF The Colorful (Yellow): entrances: The Colorful (Red): @@ -2708,6 +2959,12 @@ door_group: Colorful Doors panels: - CREAM + panel_doors: + CREAM: + item_name: The Colorful - CREAM (Panel) + panel_group: Colorful Panels + panels: + - CREAM The Colorful (Blue): entrances: The Colorful (Yellow): @@ -2728,6 +2985,12 @@ door_group: Colorful Doors panels: - SUN + panel_doors: + SUN: + item_name: The Colorful - SUN (Panel) + panel_group: Colorful Panels + panels: + - SUN The Colorful (Purple): entrances: The Colorful (Blue): @@ -2748,6 +3011,12 @@ door_group: Colorful Doors panels: - SPOON + panel_doors: + SPOON: + item_name: The Colorful - SPOON (Panel) + panel_group: Colorful Panels + panels: + - SPOON The Colorful (Orange): entrances: The Colorful (Purple): @@ -2768,6 +3037,12 @@ door_group: Colorful Doors panels: - LETTERS + panel_doors: + LETTERS: + item_name: The Colorful - LETTERS (Panel) + panel_group: Colorful Panels + panels: + - LETTERS The Colorful (Green): entrances: The Colorful (Orange): @@ -2788,6 +3063,12 @@ door_group: Colorful Doors panels: - WALLS + panel_doors: + WALLS: + item_name: The Colorful - WALLS (Panel) + panel_group: Colorful Panels + panels: + - WALLS The Colorful (Brown): entrances: The Colorful (Green): @@ -2808,6 +3089,12 @@ door_group: Colorful Doors panels: - IRON + panel_doors: + IRON: + item_name: The Colorful - IRON (Panel) + panel_group: Colorful Panels + panels: + - IRON The Colorful (Gray): entrances: The Colorful (Brown): @@ -2828,6 +3115,12 @@ door_group: Colorful Doors panels: - OBSTACLE + panel_doors: + OBSTACLE: + item_name: The Colorful - OBSTACLE (Panel) + panel_group: Colorful Panels + panels: + - OBSTACLE The Colorful: entrances: The Colorful (Gray): @@ -2866,26 +3159,48 @@ orientation: north progression: Progressive Colorful: - - room: The Colorful (White) - door: Progress Door - - room: The Colorful (Black) - door: Progress Door - - room: The Colorful (Red) - door: Progress Door - - room: The Colorful (Yellow) - door: Progress Door - - room: The Colorful (Blue) - door: Progress Door - - room: The Colorful (Purple) - door: Progress Door - - room: The Colorful (Orange) - door: Progress Door - - room: The Colorful (Green) - door: Progress Door - - room: The Colorful (Brown) - door: Progress Door - - room: The Colorful (Gray) - door: Progress Door + doors: + - room: The Colorful (White) + door: Progress Door + - room: The Colorful (Black) + door: Progress Door + - room: The Colorful (Red) + door: Progress Door + - room: The Colorful (Yellow) + door: Progress Door + - room: The Colorful (Blue) + door: Progress Door + - room: The Colorful (Purple) + door: Progress Door + - room: The Colorful (Orange) + door: Progress Door + - room: The Colorful (Green) + door: Progress Door + - room: The Colorful (Brown) + door: Progress Door + - room: The Colorful (Gray) + door: Progress Door + panel_doors: + - room: The Colorful (White) + panel_door: BEGIN + - room: The Colorful (Black) + panel_door: FOUND + - room: The Colorful (Red) + panel_door: LOAF + - room: The Colorful (Yellow) + panel_door: CREAM + - room: The Colorful (Blue) + panel_door: SUN + - room: The Colorful (Purple) + panel_door: SPOON + - room: The Colorful (Orange) + panel_door: LETTERS + - room: The Colorful (Green) + panel_door: WALLS + - room: The Colorful (Brown) + panel_door: IRON + - room: The Colorful (Gray) + panel_door: OBSTACLE Welcome Back Area: entrances: Starting Room: @@ -2958,6 +3273,10 @@ door_group: Hedge Maze Doors panels: - STRAYS + panel_doors: + STRAYS: + panels: + - STRAYS paintings: - id: arrows_painting_8 orientation: south @@ -3155,6 +3474,13 @@ panel: I - room: Elements Area panel: A + panel_doors: + UNCOVER: + panels: + - UNCOVER + OXEN: + panels: + - OXEN paintings: - id: clock_painting_5 orientation: east @@ -3524,6 +3850,13 @@ - RISE (Sunrise) - ZEN - SON + panel_doors: + UNOPEN: + panels: + - UNOPEN + BEGIN: + panels: + - BEGIN paintings: - id: pencil_painting2 orientation: west @@ -3819,6 +4152,34 @@ item_group: Achievement Room Entrances panels: - ZERO + panel_doors: + ZERO: + panels: + - ZERO + PEN: + panels: + - PEN + TWO: + item_name: Two Panels + panels: + - TWO (1) + - TWO (2) + THREE: + item_name: Three Panels + panels: + - THREE (1) + - THREE (2) + - THREE (3) + FOUR: + item_name: Four Panels + panels: + - FOUR + - room: Hub Room + panel: FOUR + - room: Dead End Area + panel: FOUR + - room: The Traveled + panel: FOUR paintings: - id: maze_painting_3 enter_only: True @@ -3994,6 +4355,10 @@ panel: FIVE (1) - room: Directional Gallery panel: FIVE (2) + First Six: + event: True + panels: + - SIX Sevens: id: - Count Up Room Area Doors/Door_seven_hider @@ -4102,12 +4467,109 @@ panel: NINE - room: Elements Area panel: NINE + panel_doors: + FIVE: + item_name: Five Panels + panels: + - FIVE + - room: Outside The Agreeable + panel: FIVE (1) + - room: Outside The Agreeable + panel: FIVE (2) + - room: Directional Gallery + panel: FIVE (1) + - room: Directional Gallery + panel: FIVE (2) + SIX: + item_name: Six Panels + panels: + - SIX + - room: Outside The Bold + panel: SIX + - room: Directional Gallery + panel: SIX (1) + - room: Directional Gallery + panel: SIX (2) + - room: The Bearer (East) + panel: SIX + - room: The Bearer (South) + panel: SIX + SEVEN: + item_name: Seven Panels + panels: + - SEVEN + - room: Directional Gallery + panel: SEVEN + - room: Knight Night Exit + panel: SEVEN (1) + - room: Knight Night Exit + panel: SEVEN (2) + - room: Knight Night Exit + panel: SEVEN (3) + - room: Outside The Initiated + panel: SEVEN (1) + - room: Outside The Initiated + panel: SEVEN (2) + EIGHT: + item_name: Eight Panels + panels: + - EIGHT + - room: Directional Gallery + panel: EIGHT + - room: The Eyes They See + panel: EIGHT + - room: Dead End Area + panel: EIGHT + - room: Crossroads + panel: EIGHT + - room: Hot Crusts Area + panel: EIGHT + - room: Art Gallery + panel: EIGHT + - room: Outside The Initiated + panel: EIGHT + NINE: + item_name: Nine Panels + panels: + - NINE + - room: Directional Gallery + panel: NINE + - room: Amen Name Area + panel: NINE + - room: Yellow Backside Area + panel: NINE + - room: Outside The Initiated + panel: NINE + - room: Outside The Bold + panel: NINE + - room: Rhyme Room (Cross) + panel: NINE + - room: Orange Tower Fifth Floor + panel: NINE + - room: Elements Area + panel: NINE paintings: - id: smile_painting_5 enter_only: True orientation: east required_door: door: Eights + progression: + Progressive Number Hunt: + panel_doors: + - room: Outside The Undeterred + panel_door: TWO + - room: Outside The Undeterred + panel_door: THREE + - room: Outside The Undeterred + panel_door: FOUR + - FIVE + - SIX + - SEVEN + - EIGHT + - NINE + - room: Outside The Undeterred + panel_door: ZERO Directional Gallery: entrances: Outside The Agreeable: @@ -4195,7 +4657,7 @@ tag: midorange required_door: room: Number Hunt - door: Sixes + door: First Six PARANOID: id: Backside Room/Panel_paranoid_paranoid tag: midwhite @@ -4203,7 +4665,7 @@ exclude_reduce: True required_door: room: Number Hunt - door: Sixes + door: First Six YELLOW: id: Color Arrow Room/Panel_yellow_afar tag: midwhite @@ -4266,6 +4728,11 @@ panels: - room: Color Hunt panel: YELLOW + panel_doors: + TURN LEARN: + panels: + - TURN + - LEARN paintings: - id: smile_painting_7 orientation: south @@ -4277,7 +4744,7 @@ move: True required_door: room: Number Hunt - door: Sixes + door: First Six - id: boxes_painting orientation: south - id: cherry_painting @@ -4344,6 +4811,34 @@ id: Rock Room Doors/Door_hint panels: - EXIT + panel_doors: + EXIT: + panels: + - EXIT + RED: + panel_group: Color Hunt Panels + panels: + - RED + BLUE: + panel_group: Color Hunt Panels + panels: + - BLUE + YELLOW: + panel_group: Color Hunt Panels + panels: + - YELLOW + ORANGE: + panel_group: Color Hunt Panels + panels: + - ORANGE + PURPLE: + panel_group: Color Hunt Panels + panels: + - PURPLE + GREEN: + panel_group: Color Hunt Panels + panels: + - GREEN paintings: - id: arrows_painting_7 orientation: east @@ -4481,6 +4976,14 @@ event: True panels: - HEART + panel_doors: + FARTHER: + panel_group: Backside Entrance Panels + panels: + - FARTHER + MIDDLE: + panels: + - MIDDLE The Bearer (East): entrances: Cross Tower (East): True @@ -5333,6 +5836,11 @@ item_name: Knight Night Room - Exit panels: - TRUSTED + panel_doors: + TRUSTED: + item_name: Knight Night Room - TRUSTED (Panel) + panels: + - TRUSTED Knight Night Exit: entrances: Knight Night (Outer Ring): @@ -6017,6 +6525,10 @@ item_group: Achievement Room Entrances panels: - SHRINK + panel_doors: + SHRINK: + panels: + - SHRINK The Wondrous (Doorknob): entrances: Outside The Wondrous: @@ -6228,18 +6740,36 @@ - KEEP - BAILEY - TOWER + panel_doors: + CASTLE: + item_name: Hallway Room - First Room Panels + panel_group: Hallway Room Panels + panels: + - WALL + - KEEP + - BAILEY + - TOWER paintings: - id: panda_painting orientation: south progression: Progressive Hallway Room: - - Exit - - room: Hallway Room (2) - door: Exit - - room: Hallway Room (3) - door: Exit - - room: Hallway Room (4) - door: Exit + doors: + - Exit + - room: Hallway Room (2) + door: Exit + - room: Hallway Room (3) + door: Exit + - room: Hallway Room (4) + door: Exit + panel_doors: + - CASTLE + - room: Hallway Room (2) + panel_door: COUNTERCLOCKWISE + - room: Hallway Room (3) + panel_door: TRANSFORMATION + - room: Hallway Room (4) + panel_door: WHEELBARROW Hallway Room (2): entrances: Hallway Room (1): @@ -6278,6 +6808,15 @@ - CLOCK - ER - COUNT + panel_doors: + COUNTERCLOCKWISE: + item_name: Hallway Room - Second Room Panels + panel_group: Hallway Room Panels + panels: + - WISE + - CLOCK + - ER + - COUNT Hallway Room (3): entrances: Hallway Room (2): @@ -6316,6 +6855,15 @@ - FORM - A - SHUN + panel_doors: + TRANSFORMATION: + item_name: Hallway Room - Third Room Panels + panel_group: Hallway Room Panels + panels: + - TRANCE + - FORM + - A + - SHUN Hallway Room (4): entrances: Hallway Room (3): @@ -6338,6 +6886,12 @@ panels: - WHEEL include_reduce: True + panel_doors: + WHEELBARROW: + item_name: Hallway Room - WHEEL + panel_group: Hallway Room Panels + panels: + - WHEEL Elements Area: entrances: Roof: True @@ -6412,6 +6966,10 @@ panels: - room: The Wanderer panel: Achievement + panel_doors: + WANDERLUST: + panels: + - WANDERLUST The Wanderer: entrances: Outside The Wanderer: @@ -6553,6 +7111,10 @@ item_group: Achievement Room Entrances panels: - ORDER + panel_doors: + ORDER: + panels: + - ORDER paintings: - id: smile_painting_3 orientation: west @@ -6566,10 +7128,11 @@ orientation: south progression: Progressive Art Gallery: - - Second Floor - - Third Floor - - Fourth Floor - - Fifth Floor + doors: + - Second Floor + - Third Floor + - Fourth Floor + - Fifth Floor Art Gallery (Second Floor): entrances: Art Gallery: @@ -7091,6 +7654,8 @@ LEAP: id: Double Room/Panel_leap_leap tag: midwhite + required_door: + door: Door to Cross doors: Door to Cross: id: Double Room Area Doors/Door_room_4a @@ -7281,8 +7846,8 @@ id: Panel Room/Panel_broomed_bedroom colors: yellow tag: midyellow - required_door: - door: Excavation + required_panel: + panel: WALL (1) LAYS: id: Panel Room/Panel_lays_maze colors: purple @@ -7309,13 +7874,24 @@ Excavation: event: True panels: - - WALL (1) + - STAIRS Cellar Exit: id: - Tower Room Area Doors/Door_panel_basement - Tower Room Area Doors/Door_panel_basement2 panels: - BASE + panel_doors: + STAIRS: + panel_group: Room Room Panels + panels: + - STAIRS + Colors: + panel_group: Room Room Panels + panels: + - BROOMED + - LAYS + - BASE Cellar: entrances: Room Room: @@ -7354,6 +7930,11 @@ panels: - KITTEN - CAT + panel_doors: + KITTEN CAT: + panels: + - KITTEN + - CAT paintings: - id: arrows_painting_2 orientation: east @@ -7608,6 +8189,10 @@ item_group: Achievement Room Entrances panels: - OPEN + panel_doors: + OPEN: + panels: + - OPEN The Scientific: entrances: Outside The Scientific: diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 4a751b25ec..9a49d3d9d4 100644 Binary files a/worlds/lingo/data/generated.dat and b/worlds/lingo/data/generated.dat differ diff --git a/worlds/lingo/data/ids.yaml b/worlds/lingo/data/ids.yaml index c49a8df363..b46f1d36ec 100644 --- a/worlds/lingo/data/ids.yaml +++ b/worlds/lingo/data/ids.yaml @@ -1478,3 +1478,145 @@ progression: Progressive Art Gallery: 444563 Progressive Colorful: 444580 Progressive Pilgrimage: 444583 + Progressive Suits Area: 444602 + Progressive Symmetry Room: 444608 + Progressive Number Hunt: 444654 +panel_doors: + Starting Room: + HIDDEN: 444589 + Hidden Room: + OPEN: 444590 + Hub Room: + ORDER: 444591 + SLAUGHTER: 444592 + TRACE: 444594 + RAT: 444595 + OPEN: 444596 + Crossroads: + DECAY: 444597 + NOPE: 444598 + WE ROT: 444599 + WORDS SWORD: 444600 + BEND HI: 444601 + Lost Area: + LOST: 444603 + Amen Name Area: + AMEN NAME: 444604 + The Tenacious: + Black Palindromes: 444605 + Near Far Area: + NEAR FAR: 444606 + Warts Straw Area: + WARTS STRAW: 444609 + Leaf Feel Area: + LEAF FEEL: 444610 + Outside The Agreeable: + MASSACRED: 444611 + BLACK: 444612 + CLOSE: 444613 + RIGHT: 444614 + Compass Room: + Lookout: 444615 + Hedge Maze: + DOWN: 444617 + The Perceptive: + GAZE: 444618 + The Observant: + BACKSIDE: 444619 + STAIRS: 444621 + The Incomparable: + Giant Sevens: 444622 + Orange Tower: + Access: 444623 + Orange Tower First Floor: + SECRET: 444624 + Orange Tower Fourth Floor: + HOT CRUSTS: 444625 + Orange Tower Fifth Floor: + SIZE: 444626 + First Second Third Fourth: + FIRST SECOND THIRD FOURTH: 444627 + The Colorful (White): + BEGIN: 444628 + The Colorful (Black): + FOUND: 444630 + The Colorful (Red): + LOAF: 444631 + The Colorful (Yellow): + CREAM: 444632 + The Colorful (Blue): + SUN: 444633 + The Colorful (Purple): + SPOON: 444634 + The Colorful (Orange): + LETTERS: 444635 + The Colorful (Green): + WALLS: 444636 + The Colorful (Brown): + IRON: 444637 + The Colorful (Gray): + OBSTACLE: 444638 + Owl Hallway: + STRAYS: 444639 + Outside The Initiated: + UNCOVER: 444640 + OXEN: 444641 + Outside The Bold: + UNOPEN: 444642 + BEGIN: 444643 + Outside The Undeterred: + ZERO: 444644 + PEN: 444645 + TWO: 444646 + THREE: 444647 + FOUR: 444648 + Number Hunt: + FIVE: 444649 + SIX: 444650 + SEVEN: 444651 + EIGHT: 444652 + NINE: 444653 + Color Hunt: + EXIT: 444655 + RED: 444656 + BLUE: 444658 + YELLOW: 444659 + ORANGE: 444660 + PURPLE: 444661 + GREEN: 444662 + The Bearer: + FARTHER: 444663 + MIDDLE: 444664 + Knight Night (Final): + TRUSTED: 444665 + Outside The Wondrous: + SHRINK: 444666 + Hallway Room (1): + CASTLE: 444667 + Hallway Room (2): + COUNTERCLOCKWISE: 444669 + Hallway Room (3): + TRANSFORMATION: 444670 + Hallway Room (4): + WHEELBARROW: 444671 + Outside The Wanderer: + WANDERLUST: 444672 + Art Gallery: + ORDER: 444673 + Room Room: + STAIRS: 444674 + Colors: 444676 + Outside The Wise: + KITTEN CAT: 444677 + Outside The Scientific: + OPEN: 444678 + Directional Gallery: + TURN LEARN: 444679 +panel_groups: + Tenacious Entrance Panels: 444593 + Symmetry Room Panels: 444607 + Backside Entrance Panels: 444620 + Colorful Panels: 444629 + Color Hunt Panels: 444657 + Hallway Room Panels: 444668 + Room Room Panels: 444675 diff --git a/worlds/lingo/datatypes.py b/worlds/lingo/datatypes.py index 36141daa41..9521422ab1 100644 --- a/worlds/lingo/datatypes.py +++ b/worlds/lingo/datatypes.py @@ -12,6 +12,11 @@ class RoomAndPanel(NamedTuple): panel: str +class RoomAndPanelDoor(NamedTuple): + room: Optional[str] + panel_door: str + + class EntranceType(Flag): NORMAL = auto() PAINTING = auto() @@ -63,9 +68,15 @@ class Panel(NamedTuple): exclude_reduce: bool achievement: bool non_counting: bool + panel_door: Optional[RoomAndPanelDoor] # This will always be fully specified. location_name: Optional[str] +class PanelDoor(NamedTuple): + item_name: str + panel_group: Optional[str] + + class Painting(NamedTuple): id: str room: str diff --git a/worlds/lingo/items.py b/worlds/lingo/items.py index 67eaceab10..78b288e7c2 100644 --- a/worlds/lingo/items.py +++ b/worlds/lingo/items.py @@ -3,7 +3,7 @@ from typing import Dict, List, NamedTuple, Set from BaseClasses import Item, ItemClassification from .static_logic import DOORS_BY_ROOM, PROGRESSIVE_ITEMS, get_door_group_item_id, get_door_item_id, \ - get_progressive_item_id, get_special_item_id + get_progressive_item_id, get_special_item_id, PANEL_DOORS_BY_ROOM, get_panel_door_item_id, get_panel_group_item_id class ItemType(Enum): @@ -65,6 +65,21 @@ def load_item_data(): ItemClassification.progression, ItemType.NORMAL, True, []) ITEMS_BY_GROUP.setdefault("Doors", []).append(group) + panel_groups: Set[str] = set() + for room_name, panel_doors in PANEL_DOORS_BY_ROOM.items(): + for panel_door_name, panel_door in panel_doors.items(): + if panel_door.panel_group is not None: + panel_groups.add(panel_door.panel_group) + + ALL_ITEM_TABLE[panel_door.item_name] = ItemData(get_panel_door_item_id(room_name, panel_door_name), + ItemClassification.progression, ItemType.NORMAL, False, []) + ITEMS_BY_GROUP.setdefault("Panels", []).append(panel_door.item_name) + + for group in panel_groups: + ALL_ITEM_TABLE[group] = ItemData(get_panel_group_item_id(group), ItemClassification.progression, + ItemType.NORMAL, False, []) + ITEMS_BY_GROUP.setdefault("Panels", []).append(group) + special_items: Dict[str, ItemClassification] = { ":)": ItemClassification.filler, "The Feeling of Being Lost": ItemClassification.filler, diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index 5a076e527d..2fd57ff5ed 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -8,21 +8,31 @@ from .items import TRAP_ITEMS class ShuffleDoors(Choice): - """If on, opening doors will require their respective "keys". + """This option specifies how doors open. - - **Simple:** Doors are sorted into logical groups, which are all opened by - receiving an item. - - **Complex:** The items are much more granular, and will usually only open - a single door each. + - **None:** Doors in the game will open the way they do in vanilla. + - **Panels:** Doors still open as in vanilla, but the panels that open the + doors will be locked, and an item will be required to unlock the panels. + - **Doors:** the doors themselves are locked behind items, and will open + automatically without needing to solve a panel once the key is obtained. """ display_name = "Shuffle Doors" option_none = 0 - option_simple = 1 - option_complex = 2 + option_panels = 1 + option_doors = 2 + alias_simple = 2 + alias_complex = 2 + + +class GroupDoors(Toggle): + """By default, door shuffle in either panels or doors mode will create individual keys for every panel or door to be locked. + + When group doors is on, some panels and doors are sorted into logical groups, which are opened together by receiving an item.""" + display_name = "Group Doors" class ProgressiveOrangeTower(DefaultOnToggle): - """When "Shuffle Doors" is on, this setting governs the manner in which the Orange Tower floors open up. + """When "Shuffle Doors" is on doors mode, this setting governs the manner in which the Orange Tower floors open up. - **Off:** There is an item for each floor of the tower, and each floor's item is the only one needed to access that floor. @@ -33,7 +43,7 @@ class ProgressiveOrangeTower(DefaultOnToggle): class ProgressiveColorful(DefaultOnToggle): - """When "Shuffle Doors" is on "complex", this setting governs the manner in which The Colorful opens up. + """When "Shuffle Doors" is on either panels or doors mode and "Group Doors" is off, this setting governs the manner in which The Colorful opens up. - **Off:** There is an item for each room of The Colorful, meaning that random rooms in the middle of the sequence can open up without giving you @@ -253,6 +263,7 @@ lingo_option_groups = [ @dataclass class LingoOptions(PerGameCommonOptions): shuffle_doors: ShuffleDoors + group_doors: GroupDoors progressive_orange_tower: ProgressiveOrangeTower progressive_colorful: ProgressiveColorful location_checks: LocationChecks diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 35080acf3a..b21735c1f5 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -7,8 +7,8 @@ from .items import ALL_ITEM_TABLE, ItemType from .locations import ALL_LOCATION_TABLE, LocationClassification from .options import LocationChecks, ShuffleDoors, SunwarpAccess, VictoryCondition from .static_logic import DOORS_BY_ROOM, PAINTINGS, PAINTING_ENTRANCES, PAINTING_EXITS, \ - PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, \ - SUNWARP_ENTRANCES, SUNWARP_EXITS + PANELS_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, PROGRESSIVE_DOORS_BY_ROOM, \ + PANEL_DOORS_BY_ROOM, PROGRESSIVE_PANELS_BY_ROOM, SUNWARP_ENTRANCES, SUNWARP_EXITS if TYPE_CHECKING: from . import LingoWorld @@ -18,6 +18,8 @@ class AccessRequirements: rooms: Set[str] doors: Set[RoomAndDoor] colors: Set[str] + items: Set[str] + progression: Dict[str, int] the_master: bool postgame: bool @@ -25,6 +27,8 @@ class AccessRequirements: self.rooms = set() self.doors = set() self.colors = set() + self.items = set() + self.progression = dict() self.the_master = False self.postgame = False @@ -32,12 +36,17 @@ class AccessRequirements: self.rooms |= other.rooms self.doors |= other.doors self.colors |= other.colors + self.items |= other.items self.the_master |= other.the_master self.postgame |= other.postgame + for progression, index in other.progression.items(): + if progression not in self.progression or index > self.progression[progression]: + self.progression[progression] = index + def __str__(self): - return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors}," \ - f" the_master={self.the_master}, postgame={self.postgame})" + return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors}, items={self.items}," \ + f" progression={self.progression}), the_master={self.the_master}, postgame={self.postgame}" class PlayerLocation(NamedTuple): @@ -117,15 +126,15 @@ class LingoPlayerLogic: self.item_by_door.setdefault(room, {})[door] = item def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"): - if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]: - progression_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name + if room_name in PROGRESSIVE_DOORS_BY_ROOM and door_data.name in PROGRESSIVE_DOORS_BY_ROOM[room_name]: + progression_name = PROGRESSIVE_DOORS_BY_ROOM[room_name][door_data.name].item_name progression_handling = should_split_progression(progression_name, world) if progression_handling == ProgressiveItemBehavior.SPLIT: self.set_door_item(room_name, door_data.name, door_data.item_name) self.real_items.append(door_data.item_name) elif progression_handling == ProgressiveItemBehavior.PROGRESSIVE: - progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name + progressive_item_name = PROGRESSIVE_DOORS_BY_ROOM[room_name][door_data.name].item_name self.set_door_item(room_name, door_data.name, progressive_item_name) self.real_items.append(progressive_item_name) else: @@ -156,17 +165,31 @@ class LingoPlayerLogic: victory_condition = world.options.victory_condition early_color_hallways = world.options.early_color_hallways - if location_checks == LocationChecks.option_reduced and door_shuffle != ShuffleDoors.option_none: - raise OptionError("You cannot have reduced location checks when door shuffle is on, because there would not" - " be enough locations for all of the door items.") + if location_checks == LocationChecks.option_reduced: + if door_shuffle == ShuffleDoors.option_doors: + raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks when door shuffle" + f" is on, because there would not be enough locations for all of the door items.") + if door_shuffle == ShuffleDoors.option_panels: + if not world.options.group_doors: + raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks when ungrouped" + f" panels mode door shuffle is on, because there would not be enough locations for" + f" all of the panel items.") + if color_shuffle: + raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks with both" + f" panels mode door shuffle and color shuffle because there would not be enough" + f" locations for all of the items.") + if world.options.sunwarp_access >= SunwarpAccess.option_individual: + raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks with both" + f" panels mode door shuffle and individual or progressive sunwarp access because" + f" there would not be enough locations for all of the items.") # Create door items, where needed. door_groups: Set[str] = set() for room_name, room_data in DOORS_BY_ROOM.items(): for door_name, door_data in room_data.items(): if door_data.skip_item is False and door_data.event is False: - if door_data.type == DoorType.NORMAL and door_shuffle != ShuffleDoors.option_none: - if door_data.door_group is not None and door_shuffle == ShuffleDoors.option_simple: + if door_data.type == DoorType.NORMAL and door_shuffle == ShuffleDoors.option_doors: + if door_data.door_group is not None and world.options.group_doors: # Grouped doors are handled differently if shuffle doors is on simple. self.set_door_item(room_name, door_name, door_data.door_group) door_groups.add(door_data.door_group) @@ -188,7 +211,29 @@ class LingoPlayerLogic: self.real_items.append(door_data.item_name) self.real_items += door_groups - + + # Create panel items, where needed. + if world.options.shuffle_doors == ShuffleDoors.option_panels: + panel_groups: Set[str] = set() + + for room_name, room_data in PANEL_DOORS_BY_ROOM.items(): + for panel_door_name, panel_door_data in room_data.items(): + if panel_door_data.panel_group is not None and world.options.group_doors: + panel_groups.add(panel_door_data.panel_group) + elif room_name in PROGRESSIVE_PANELS_BY_ROOM \ + and panel_door_name in PROGRESSIVE_PANELS_BY_ROOM[room_name]: + progression_obj = PROGRESSIVE_PANELS_BY_ROOM[room_name][panel_door_name] + progression_handling = should_split_progression(progression_obj.item_name, world) + + if progression_handling == ProgressiveItemBehavior.SPLIT: + self.real_items.append(panel_door_data.item_name) + elif progression_handling == ProgressiveItemBehavior.PROGRESSIVE: + self.real_items.append(progression_obj.item_name) + else: + self.real_items.append(panel_door_data.item_name) + + self.real_items += panel_groups + # Create color items, if needed. if color_shuffle: self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR] @@ -244,7 +289,7 @@ class LingoPlayerLogic: elif location_checks == LocationChecks.option_insanity: location_classification = LocationClassification.insanity - if door_shuffle != ShuffleDoors.option_none and not early_color_hallways: + if door_shuffle == ShuffleDoors.option_doors and not early_color_hallways: location_classification |= LocationClassification.small_sphere_one for location_name, location_data in ALL_LOCATION_TABLE.items(): @@ -286,7 +331,7 @@ class LingoPlayerLogic: "iterations. This is very unlikely to happen on its own, and probably indicates some " "kind of logic error.") - if door_shuffle != ShuffleDoors.option_none and location_checks != LocationChecks.option_insanity \ + if door_shuffle == ShuffleDoors.option_doors and location_checks != LocationChecks.option_insanity \ and not early_color_hallways and world.multiworld.players > 1: # Under the combination of door shuffle, normal location checks, and no early color hallways, sphere 1 is # only three checks. In a multiplayer situation, this can be frustrating for the player because they are @@ -301,19 +346,19 @@ class LingoPlayerLogic: # Starting Room - Exit Door gives access to OPEN and TRACE. good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"] - if not color_shuffle and not world.options.enable_pilgrimage: - # HOT CRUST and THIS. - good_item_options.append("Pilgrim Room - Sun Painting") - if not color_shuffle: - if door_shuffle == ShuffleDoors.option_simple: + if not world.options.enable_pilgrimage: + # HOT CRUST and THIS. + good_item_options.append("Pilgrim Room - Sun Painting") + + if world.options.group_doors: # WELCOME BACK, CLOCKWISE, and DRAWL + RUNS. good_item_options.append("Welcome Back Doors") else: # WELCOME BACK and CLOCKWISE. good_item_options.append("Welcome Back Area - Shortcut to Starting Room") - if door_shuffle == ShuffleDoors.option_simple: + if world.options.group_doors: # Color hallways access (NOTE: reconsider when sunwarp shuffling exists). good_item_options.append("Rhyme Room Doors") @@ -359,13 +404,11 @@ class LingoPlayerLogic: def randomize_paintings(self, world: "LingoWorld") -> bool: self.painting_mapping.clear() - door_shuffle = world.options.shuffle_doors - # First, assign mappings to the required-exit paintings. We ensure that req-blocked paintings do not lead to # required paintings. req_exits = [] required_painting_rooms = REQUIRED_PAINTING_ROOMS - if door_shuffle == ShuffleDoors.option_none: + if world.options.shuffle_doors != ShuffleDoors.option_doors: required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS req_exits = [painting_id for painting_id, painting in PAINTINGS.items() if painting.required_when_no_doors] @@ -432,7 +475,7 @@ class LingoPlayerLogic: for painting_id, painting in PAINTINGS.items(): if painting_id not in self.painting_mapping.values() \ and (painting.required or (painting.required_when_no_doors and - door_shuffle == ShuffleDoors.option_none)): + world.options.shuffle_doors != ShuffleDoors.option_doors)): return False return True @@ -447,12 +490,31 @@ class LingoPlayerLogic: access_reqs = AccessRequirements() panel_object = PANELS_BY_ROOM[room][panel] + if world.options.shuffle_doors == ShuffleDoors.option_panels and panel_object.panel_door is not None: + panel_door_room = panel_object.panel_door.room + panel_door_name = panel_object.panel_door.panel_door + panel_door = PANEL_DOORS_BY_ROOM[panel_door_room][panel_door_name] + + if panel_door.panel_group is not None and world.options.group_doors: + access_reqs.items.add(panel_door.panel_group) + elif panel_door_room in PROGRESSIVE_PANELS_BY_ROOM\ + and panel_door_name in PROGRESSIVE_PANELS_BY_ROOM[panel_door_room]: + progression_obj = PROGRESSIVE_PANELS_BY_ROOM[panel_door_room][panel_door_name] + progression_handling = should_split_progression(progression_obj.item_name, world) + + if progression_handling == ProgressiveItemBehavior.SPLIT: + access_reqs.items.add(panel_door.item_name) + elif progression_handling == ProgressiveItemBehavior.PROGRESSIVE: + access_reqs.progression[progression_obj.item_name] = progression_obj.index + else: + access_reqs.items.add(panel_door.item_name) + for req_room in panel_object.required_rooms: access_reqs.rooms.add(req_room) for req_door in panel_object.required_doors: door_object = DOORS_BY_ROOM[room if req_door.room is None else req_door.room][req_door.door] - if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none: + if door_object.event or world.options.shuffle_doors != ShuffleDoors.option_doors: sub_access_reqs = self.calculate_door_requirements( room if req_door.room is None else req_door.room, req_door.door, world) access_reqs.merge(sub_access_reqs) @@ -522,11 +584,14 @@ class LingoPlayerLogic: continue # We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will - # only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. THE MASTER has - # special access rules and is handled separately. + # only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. Panel door locked + # puzzles will be separate if panels mode is on. THE MASTER has special access rules and is handled + # separately. if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\ or len(panel_data.required_rooms) > 0\ or (world.options.shuffle_colors and len(panel_data.colors) > 1)\ + or (world.options.shuffle_doors == ShuffleDoors.option_panels + and panel_data.panel_door is not None)\ or panel_name == "THE MASTER": self.counting_panel_reqs.setdefault(room_name, []).append( (self.calculate_panel_requirements(room_name, panel_name, world), 1)) diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index ed84c56e28..e0bb08fa1f 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING from BaseClasses import CollectionState from .datatypes import RoomAndDoor from .player_logic import AccessRequirements, PlayerLocation -from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS +from .static_logic import PROGRESSIVE_DOORS_BY_ROOM, PROGRESSIVE_ITEMS if TYPE_CHECKING: from . import LingoWorld @@ -59,6 +59,12 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir if not state.has(color.capitalize(), world.player): return False + if not all(state.has(item, world.player) for item in access.items): + return False + + if not all(state.has(item, world.player, index) for item, index in access.progression.items()): + return False + if access.the_master and not lingo_can_use_mastery_location(state, world): return False @@ -77,7 +83,7 @@ def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "L item_name = world.player_logic.item_by_door[room][door] if item_name in PROGRESSIVE_ITEMS: - progression = PROGRESSION_BY_ROOM[room][door] + progression = PROGRESSIVE_DOORS_BY_ROOM[room][door] return state.has(item_name, world.player, progression.index) return state.has(item_name, world.player) diff --git a/worlds/lingo/static_logic.py b/worlds/lingo/static_logic.py index ff820dd0cb..74eea449f2 100644 --- a/worlds/lingo/static_logic.py +++ b/worlds/lingo/static_logic.py @@ -4,15 +4,17 @@ import pickle from io import BytesIO from typing import Dict, List, Set -from .datatypes import Door, Painting, Panel, Progression, Room +from .datatypes import Door, Painting, Panel, PanelDoor, Progression, Room ALL_ROOMS: List[Room] = [] DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {} PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {} +PANEL_DOORS_BY_ROOM: Dict[str, Dict[str, PanelDoor]] = {} PAINTINGS: Dict[str, Painting] = {} -PROGRESSIVE_ITEMS: List[str] = [] -PROGRESSION_BY_ROOM: Dict[str, Dict[str, Progression]] = {} +PROGRESSIVE_ITEMS: Set[str] = set() +PROGRESSIVE_DOORS_BY_ROOM: Dict[str, Dict[str, Progression]] = {} +PROGRESSIVE_PANELS_BY_ROOM: Dict[str, Dict[str, Progression]] = {} PAINTING_ENTRANCES: int = 0 PAINTING_EXIT_ROOMS: Set[str] = set() @@ -28,6 +30,8 @@ PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {} DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {} DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {} DOOR_GROUP_ITEM_IDS: Dict[str, int] = {} +PANEL_DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {} +PANEL_GROUP_ITEM_IDS: Dict[str, int] = {} PROGRESSIVE_ITEM_IDS: Dict[str, int] = {} HASHES: Dict[str, str] = {} @@ -68,6 +72,20 @@ def get_door_group_item_id(name: str): return DOOR_GROUP_ITEM_IDS[name] +def get_panel_door_item_id(room: str, name: str): + if room not in PANEL_DOOR_ITEM_IDS or name not in PANEL_DOOR_ITEM_IDS[room]: + raise Exception(f"Item ID for panel door {room} - {name} not found in ids.yaml.") + + return PANEL_DOOR_ITEM_IDS[room][name] + + +def get_panel_group_item_id(name: str): + if name not in PANEL_GROUP_ITEM_IDS: + raise Exception(f"Item ID for panel group {name} not found in ids.yaml.") + + return PANEL_GROUP_ITEM_IDS[name] + + def get_progressive_item_id(name: str): if name not in PROGRESSIVE_ITEM_IDS: raise Exception(f"Item ID for progressive item {name} not found in ids.yaml.") @@ -97,8 +115,10 @@ def load_static_data_from_file(): ALL_ROOMS.extend(pickdata["ALL_ROOMS"]) DOORS_BY_ROOM.update(pickdata["DOORS_BY_ROOM"]) PANELS_BY_ROOM.update(pickdata["PANELS_BY_ROOM"]) - PROGRESSIVE_ITEMS.extend(pickdata["PROGRESSIVE_ITEMS"]) - PROGRESSION_BY_ROOM.update(pickdata["PROGRESSION_BY_ROOM"]) + PANEL_DOORS_BY_ROOM.update(pickdata["PANEL_DOORS_BY_ROOM"]) + PROGRESSIVE_ITEMS.update(pickdata["PROGRESSIVE_ITEMS"]) + PROGRESSIVE_DOORS_BY_ROOM.update(pickdata["PROGRESSIVE_DOORS_BY_ROOM"]) + PROGRESSIVE_PANELS_BY_ROOM.update(pickdata["PROGRESSIVE_PANELS_BY_ROOM"]) PAINTING_ENTRANCES = pickdata["PAINTING_ENTRANCES"] PAINTING_EXIT_ROOMS.update(pickdata["PAINTING_EXIT_ROOMS"]) PAINTING_EXITS = pickdata["PAINTING_EXITS"] @@ -111,6 +131,8 @@ def load_static_data_from_file(): DOOR_LOCATION_IDS.update(pickdata["DOOR_LOCATION_IDS"]) DOOR_ITEM_IDS.update(pickdata["DOOR_ITEM_IDS"]) DOOR_GROUP_ITEM_IDS.update(pickdata["DOOR_GROUP_ITEM_IDS"]) + PANEL_DOOR_ITEM_IDS.update(pickdata["PANEL_DOOR_ITEM_IDS"]) + PANEL_GROUP_ITEM_IDS.update(pickdata["PANEL_GROUP_ITEM_IDS"]) PROGRESSIVE_ITEM_IDS.update(pickdata["PROGRESSIVE_ITEM_IDS"]) diff --git a/worlds/lingo/test/TestDoors.py b/worlds/lingo/test/TestDoors.py index f496c5f578..cfbd7f3027 100644 --- a/worlds/lingo/test/TestDoors.py +++ b/worlds/lingo/test/TestDoors.py @@ -3,7 +3,7 @@ from . import LingoTestBase class TestRequiredRoomLogic(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "shuffle_colors": "false", } @@ -50,7 +50,7 @@ class TestRequiredRoomLogic(LingoTestBase): class TestRequiredDoorLogic(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "shuffle_colors": "false", } @@ -78,7 +78,8 @@ class TestRequiredDoorLogic(LingoTestBase): class TestSimpleDoors(LingoTestBase): options = { - "shuffle_doors": "simple", + "shuffle_doors": "doors", + "group_doors": "true", "shuffle_colors": "false", } @@ -90,3 +91,52 @@ class TestSimpleDoors(LingoTestBase): self.assertTrue(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + +class TestPanels(LingoTestBase): + options = { + "shuffle_doors": "panels" + } + + def test_requirement(self): + self.assertFalse(self.can_reach_location("Starting Room - HIDDEN")) + self.assertFalse(self.can_reach_location("Hidden Room - OPEN")) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Starting Room - HIDDEN (Panel)") + self.assertTrue(self.can_reach_location("Starting Room - HIDDEN")) + self.assertFalse(self.can_reach_location("Hidden Room - OPEN")) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Hidden Room - OPEN (Panel)") + self.assertTrue(self.can_reach_location("Starting Room - HIDDEN")) + self.assertTrue(self.can_reach_location("Hidden Room - OPEN")) + self.assertTrue(self.can_reach_location("The Seeker - Achievement")) + + +class TestGroupedPanels(LingoTestBase): + options = { + "shuffle_doors": "panels", + "group_doors": "true", + "shuffle_colors": "false", + } + + def test_requirement(self): + self.assertFalse(self.can_reach_location("Hub Room - SLAUGHTER")) + self.assertFalse(self.can_reach_location("Dread Hallway - DREAD")) + self.assertFalse(self.can_reach_location("The Tenacious - Achievement")) + + self.collect_by_name("Tenacious Entrance Panels") + self.assertTrue(self.can_reach_location("Hub Room - SLAUGHTER")) + self.assertFalse(self.can_reach_location("Dread Hallway - DREAD")) + self.assertFalse(self.can_reach_location("The Tenacious - Achievement")) + + self.collect_by_name("Outside The Agreeable - BLACK (Panel)") + self.assertTrue(self.can_reach_location("Hub Room - SLAUGHTER")) + self.assertTrue(self.can_reach_location("Dread Hallway - DREAD")) + self.assertFalse(self.can_reach_location("The Tenacious - Achievement")) + + self.collect_by_name("The Tenacious - Black Palindromes (Panels)") + self.assertTrue(self.can_reach_location("Hub Room - SLAUGHTER")) + self.assertTrue(self.can_reach_location("Dread Hallway - DREAD")) + self.assertTrue(self.can_reach_location("The Tenacious - Achievement")) + diff --git a/worlds/lingo/test/TestOptions.py b/worlds/lingo/test/TestOptions.py index fce0743116..bd8ed81d7a 100644 --- a/worlds/lingo/test/TestOptions.py +++ b/worlds/lingo/test/TestOptions.py @@ -3,7 +3,7 @@ from . import LingoTestBase class TestMultiShuffleOptions(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "progressive_orange_tower": "true", "shuffle_colors": "true", "shuffle_paintings": "true", @@ -13,7 +13,7 @@ class TestMultiShuffleOptions(LingoTestBase): class TestPanelsanity(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "progressive_orange_tower": "true", "location_checks": "insanity", "shuffle_colors": "true" @@ -22,7 +22,18 @@ class TestPanelsanity(LingoTestBase): class TestAllPanelHunt(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", + "progressive_orange_tower": "true", + "shuffle_colors": "true", + "victory_condition": "level_2", + "level_2_requirement": "800", + "early_color_hallways": "true" + } + + +class TestAllPanelHuntPanelsMode(LingoTestBase): + options = { + "shuffle_doors": "panels", "progressive_orange_tower": "true", "shuffle_colors": "true", "victory_condition": "level_2", diff --git a/worlds/lingo/test/TestOrangeTower.py b/worlds/lingo/test/TestOrangeTower.py index 7b0c3bb525..444264a589 100644 --- a/worlds/lingo/test/TestOrangeTower.py +++ b/worlds/lingo/test/TestOrangeTower.py @@ -3,7 +3,7 @@ from . import LingoTestBase class TestProgressiveOrangeTower(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "progressive_orange_tower": "true" } diff --git a/worlds/lingo/test/TestPanelsanity.py b/worlds/lingo/test/TestPanelsanity.py index 34c1b3815a..f8330ae782 100644 --- a/worlds/lingo/test/TestPanelsanity.py +++ b/worlds/lingo/test/TestPanelsanity.py @@ -3,7 +3,7 @@ from . import LingoTestBase class TestPanelHunt(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "location_checks": "insanity", "victory_condition": "level_2", "level_2_requirement": "15" diff --git a/worlds/lingo/test/TestPilgrimage.py b/worlds/lingo/test/TestPilgrimage.py index 4c5e259cd5..328156da2d 100644 --- a/worlds/lingo/test/TestPilgrimage.py +++ b/worlds/lingo/test/TestPilgrimage.py @@ -18,7 +18,7 @@ class TestPilgrimageWithRoofAndPaintings(LingoTestBase): options = { "enable_pilgrimage": "true", "shuffle_colors": "false", - "shuffle_doors": "complex", + "shuffle_doors": "doors", "pilgrimage_allows_roof_access": "true", "pilgrimage_allows_paintings": "true", "early_color_hallways": "false" @@ -39,7 +39,7 @@ class TestPilgrimageNoRoofYesPaintings(LingoTestBase): options = { "enable_pilgrimage": "true", "shuffle_colors": "false", - "shuffle_doors": "complex", + "shuffle_doors": "doors", "pilgrimage_allows_roof_access": "false", "pilgrimage_allows_paintings": "true", "early_color_hallways": "false" @@ -62,7 +62,7 @@ class TestPilgrimageNoRoofNoPaintings(LingoTestBase): options = { "enable_pilgrimage": "true", "shuffle_colors": "false", - "shuffle_doors": "complex", + "shuffle_doors": "doors", "pilgrimage_allows_roof_access": "false", "pilgrimage_allows_paintings": "false", "early_color_hallways": "false" @@ -117,7 +117,7 @@ class TestPilgrimageYesRoofNoPaintings(LingoTestBase): options = { "enable_pilgrimage": "true", "shuffle_colors": "false", - "shuffle_doors": "complex", + "shuffle_doors": "doors", "pilgrimage_allows_roof_access": "true", "pilgrimage_allows_paintings": "false", "early_color_hallways": "false" diff --git a/worlds/lingo/test/TestProgressive.py b/worlds/lingo/test/TestProgressive.py index e79fd6bc90..2c837f53f3 100644 --- a/worlds/lingo/test/TestProgressive.py +++ b/worlds/lingo/test/TestProgressive.py @@ -3,7 +3,7 @@ from . import LingoTestBase class TestComplexProgressiveHallwayRoom(LingoTestBase): options = { - "shuffle_doors": "complex" + "shuffle_doors": "doors" } def test_item(self): @@ -54,7 +54,8 @@ class TestComplexProgressiveHallwayRoom(LingoTestBase): class TestSimpleHallwayRoom(LingoTestBase): options = { - "shuffle_doors": "simple" + "shuffle_doors": "doors", + "group_doors": "true", } def test_item(self): @@ -81,7 +82,7 @@ class TestSimpleHallwayRoom(LingoTestBase): class TestProgressiveArtGallery(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "shuffle_colors": "false", } diff --git a/worlds/lingo/test/TestSunwarps.py b/worlds/lingo/test/TestSunwarps.py index e8e913c4f4..66ba3afd6e 100644 --- a/worlds/lingo/test/TestSunwarps.py +++ b/worlds/lingo/test/TestSunwarps.py @@ -19,7 +19,8 @@ class TestVanillaDoorsNormalSunwarps(LingoTestBase): class TestSimpleDoorsNormalSunwarps(LingoTestBase): options = { - "shuffle_doors": "simple", + "shuffle_doors": "doors", + "group_doors": "true", "sunwarp_access": "normal" } @@ -37,7 +38,8 @@ class TestSimpleDoorsNormalSunwarps(LingoTestBase): class TestSimpleDoorsDisabledSunwarps(LingoTestBase): options = { - "shuffle_doors": "simple", + "shuffle_doors": "doors", + "group_doors": "true", "sunwarp_access": "disabled" } @@ -56,7 +58,8 @@ class TestSimpleDoorsDisabledSunwarps(LingoTestBase): class TestSimpleDoorsUnlockSunwarps(LingoTestBase): options = { - "shuffle_doors": "simple", + "shuffle_doors": "doors", + "group_doors": "true", "sunwarp_access": "unlock" } @@ -78,7 +81,8 @@ class TestSimpleDoorsUnlockSunwarps(LingoTestBase): class TestComplexDoorsNormalSunwarps(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", + "group_doors": "false", "sunwarp_access": "normal" } @@ -96,7 +100,8 @@ class TestComplexDoorsNormalSunwarps(LingoTestBase): class TestComplexDoorsDisabledSunwarps(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", + "group_doors": "false", "sunwarp_access": "disabled" } @@ -115,7 +120,8 @@ class TestComplexDoorsDisabledSunwarps(LingoTestBase): class TestComplexDoorsIndividualSunwarps(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", + "group_doors": "false", "sunwarp_access": "individual" } @@ -142,7 +148,8 @@ class TestComplexDoorsIndividualSunwarps(LingoTestBase): class TestComplexDoorsProgressiveSunwarps(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", + "group_doors": "false", "sunwarp_access": "progressive" } diff --git a/worlds/lingo/utils/assign_ids.rb b/worlds/lingo/utils/assign_ids.rb index 9e1ce67bd2..f7de3d03f5 100644 --- a/worlds/lingo/utils/assign_ids.rb +++ b/worlds/lingo/utils/assign_ids.rb @@ -73,6 +73,22 @@ if old_generated.include? "door_groups" then end end end +if old_generated.include? "panel_doors" then + old_generated["panel_doors"].each do |room, panel_doors| + panel_doors.each do |name, id| + if id >= next_item_id then + next_item_id = id + 1 + end + end + end +end +if old_generated.include? "panel_groups" then + old_generated["panel_groups"].each do |name, id| + if id >= next_item_id then + next_item_id = id + 1 + end + end +end if old_generated.include? "progression" then old_generated["progression"].each do |name, id| if id >= next_item_id then @@ -82,6 +98,7 @@ if old_generated.include? "progression" then end door_groups = Set[] +panel_groups = Set[] config = YAML.load_file(configpath) config.each do |room_name, room_data| @@ -163,6 +180,29 @@ config.each do |room_name, room_data| end end + if room_data.include? "panel_doors" + room_data["panel_doors"].each do |panel_door_name, panel_door| + unless old_generated.include? "panel_doors" and old_generated["panel_doors"].include? room_name and old_generated["panel_doors"][room_name].include? panel_door_name then + old_generated["panel_doors"] ||= {} + old_generated["panel_doors"][room_name] ||= {} + old_generated["panel_doors"][room_name][panel_door_name] = next_item_id + + next_item_id += 1 + end + + if panel_door.include? "panel_group" and not panel_groups.include? panel_door["panel_group"] then + panel_groups.add(panel_door["panel_group"]) + + unless old_generated.include? "panel_groups" and old_generated["panel_groups"].include? panel_door["panel_group"] then + old_generated["panel_groups"] ||= {} + old_generated["panel_groups"][panel_door["panel_group"]] = next_item_id + + next_item_id += 1 + end + end + end + end + if room_data.include? "progression" room_data["progression"].each do |progression_name, pdata| unless old_generated.include? "progression" and old_generated["progression"].include? progression_name then diff --git a/worlds/lingo/utils/pickle_static_data.py b/worlds/lingo/utils/pickle_static_data.py index e40c21ce3e..92bcb7a859 100644 --- a/worlds/lingo/utils/pickle_static_data.py +++ b/worlds/lingo/utils/pickle_static_data.py @@ -6,8 +6,8 @@ import sys sys.path.append(os.path.join("worlds", "lingo")) sys.path.append(".") sys.path.append("..") -from datatypes import Door, DoorType, EntranceType, Painting, Panel, Progression, Room, RoomAndDoor, RoomAndPanel,\ - RoomEntrance +from datatypes import Door, DoorType, EntranceType, Painting, Panel, PanelDoor, Progression, Room, RoomAndDoor,\ + RoomAndPanel, RoomAndPanelDoor, RoomEntrance import hashlib import pickle @@ -18,10 +18,12 @@ import Utils ALL_ROOMS: List[Room] = [] DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {} PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {} +PANEL_DOORS_BY_ROOM: Dict[str, Dict[str, PanelDoor]] = {} PAINTINGS: Dict[str, Painting] = {} -PROGRESSIVE_ITEMS: List[str] = [] -PROGRESSION_BY_ROOM: Dict[str, Dict[str, Progression]] = {} +PROGRESSIVE_ITEMS: Set[str] = set() +PROGRESSIVE_DOORS_BY_ROOM: Dict[str, Dict[str, Progression]] = {} +PROGRESSIVE_PANELS_BY_ROOM: Dict[str, Dict[str, Progression]] = {} PAINTING_ENTRANCES: int = 0 PAINTING_EXIT_ROOMS: Set[str] = set() @@ -37,8 +39,13 @@ PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {} DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {} DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {} DOOR_GROUP_ITEM_IDS: Dict[str, int] = {} +PANEL_DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {} +PANEL_GROUP_ITEM_IDS: Dict[str, int] = {} PROGRESSIVE_ITEM_IDS: Dict[str, int] = {} +# This doesn't need to be stored in the datafile. +PANEL_DOOR_BY_PANEL_BY_ROOM: Dict[str, Dict[str, str]] = {} + def hash_file(path): md5 = hashlib.md5() @@ -53,7 +60,7 @@ def hash_file(path): def load_static_data(ll1_path, ids_path): global PAINTING_EXITS, SPECIAL_ITEM_IDS, PANEL_LOCATION_IDS, DOOR_LOCATION_IDS, DOOR_ITEM_IDS, \ - DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS + DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS, PANEL_DOOR_ITEM_IDS, PANEL_GROUP_ITEM_IDS # Load in all item and location IDs. These are broken up into groups based on the type of item/location. with open(ids_path, "r") as file: @@ -86,6 +93,17 @@ def load_static_data(ll1_path, ids_path): for item_name, item_id in config["door_groups"].items(): DOOR_GROUP_ITEM_IDS[item_name] = item_id + if "panel_doors" in config: + for room_name, panel_doors in config["panel_doors"].items(): + PANEL_DOOR_ITEM_IDS[room_name] = {} + + for panel_door, item_id in panel_doors.items(): + PANEL_DOOR_ITEM_IDS[room_name][panel_door] = item_id + + if "panel_groups" in config: + for item_name, item_id in config["panel_groups"].items(): + PANEL_GROUP_ITEM_IDS[item_name] = item_id + if "progression" in config: for item_name, item_id in config["progression"].items(): PROGRESSIVE_ITEM_IDS[item_name] = item_id @@ -147,6 +165,46 @@ def process_entrance(source_room, doors, room_obj): room_obj.entrances.append(RoomEntrance(source_room, door, entrance_type)) +def process_panel_door(room_name, panel_door_name, panel_door_data): + global PANEL_DOORS_BY_ROOM, PANEL_DOOR_BY_PANEL_BY_ROOM + + panels: List[RoomAndPanel] = list() + for panel in panel_door_data["panels"]: + if isinstance(panel, dict): + panels.append(RoomAndPanel(panel["room"], panel["panel"])) + else: + panels.append(RoomAndPanel(room_name, panel)) + + for panel in panels: + PANEL_DOOR_BY_PANEL_BY_ROOM.setdefault(panel.room, {})[panel.panel] = RoomAndPanelDoor(room_name, + panel_door_name) + + if "item_name" in panel_door_data: + item_name = panel_door_data["item_name"] + else: + panel_per_room = dict() + for panel in panels: + panel_room_name = room_name if panel.room is None else panel.room + panel_per_room.setdefault(panel_room_name, []).append(panel.panel) + + room_strs = list() + for door_room_str, door_panels_str in panel_per_room.items(): + room_strs.append(door_room_str + " - " + ", ".join(door_panels_str)) + + if len(panels) == 1: + item_name = f"{room_strs[0]} (Panel)" + else: + item_name = " and ".join(room_strs) + " (Panels)" + + if "panel_group" in panel_door_data: + panel_group = panel_door_data["panel_group"] + else: + panel_group = None + + panel_door_obj = PanelDoor(item_name, panel_group) + PANEL_DOORS_BY_ROOM[room_name][panel_door_name] = panel_door_obj + + def process_panel(room_name, panel_name, panel_data): global PANELS_BY_ROOM @@ -227,13 +285,18 @@ def process_panel(room_name, panel_name, panel_data): else: non_counting = False + if room_name in PANEL_DOOR_BY_PANEL_BY_ROOM and panel_name in PANEL_DOOR_BY_PANEL_BY_ROOM[room_name]: + panel_door = PANEL_DOOR_BY_PANEL_BY_ROOM[room_name][panel_name] + else: + panel_door = None + if "location_name" in panel_data: location_name = panel_data["location_name"] else: location_name = None panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, exclude_reduce, - achievement, non_counting, location_name) + achievement, non_counting, panel_door, location_name) PANELS_BY_ROOM[room_name][panel_name] = panel_obj @@ -325,7 +388,7 @@ def process_door(room_name, door_name, door_data): painting_ids = [] door_type = DoorType.NORMAL - if door_name.endswith(" Sunwarp"): + if room_name == "Sunwarps": door_type = DoorType.SUNWARP elif room_name == "Pilgrim Antechamber" and door_name == "Sun Painting": door_type = DoorType.SUN_PAINTING @@ -404,11 +467,11 @@ def process_sunwarp(room_name, sunwarp_data): SUNWARP_EXITS[sunwarp_data["dots"] - 1] = room_name -def process_progression(room_name, progression_name, progression_doors): - global PROGRESSIVE_ITEMS, PROGRESSION_BY_ROOM +def process_progressive_door(room_name, progression_name, progression_doors): + global PROGRESSIVE_ITEMS, PROGRESSIVE_DOORS_BY_ROOM # Progressive items are configured as a list of doors. - PROGRESSIVE_ITEMS.append(progression_name) + PROGRESSIVE_ITEMS.add(progression_name) progression_index = 1 for door in progression_doors: @@ -419,11 +482,31 @@ def process_progression(room_name, progression_name, progression_doors): door_room = room_name door_door = door - room_progressions = PROGRESSION_BY_ROOM.setdefault(door_room, {}) + room_progressions = PROGRESSIVE_DOORS_BY_ROOM.setdefault(door_room, {}) room_progressions[door_door] = Progression(progression_name, progression_index) progression_index += 1 +def process_progressive_panel(room_name, progression_name, progression_panel_doors): + global PROGRESSIVE_ITEMS, PROGRESSIVE_PANELS_BY_ROOM + + # Progressive items are configured as a list of panel doors. + PROGRESSIVE_ITEMS.add(progression_name) + + progression_index = 1 + for panel_door in progression_panel_doors: + if isinstance(panel_door, Dict): + panel_door_room = panel_door["room"] + panel_door_door = panel_door["panel_door"] + else: + panel_door_room = room_name + panel_door_door = panel_door + + room_progressions = PROGRESSIVE_PANELS_BY_ROOM.setdefault(panel_door_room, {}) + room_progressions[panel_door_door] = Progression(progression_name, progression_index) + progression_index += 1 + + def process_room(room_name, room_data): global ALL_ROOMS @@ -433,6 +516,12 @@ def process_room(room_name, room_data): for source_room, doors in room_data["entrances"].items(): process_entrance(source_room, doors, room_obj) + if "panel_doors" in room_data: + PANEL_DOORS_BY_ROOM[room_name] = dict() + + for panel_door_name, panel_door_data in room_data["panel_doors"].items(): + process_panel_door(room_name, panel_door_name, panel_door_data) + if "panels" in room_data: PANELS_BY_ROOM[room_name] = dict() @@ -454,8 +543,11 @@ def process_room(room_name, room_data): process_sunwarp(room_name, sunwarp_data) if "progression" in room_data: - for progression_name, progression_doors in room_data["progression"].items(): - process_progression(room_name, progression_name, progression_doors) + for progression_name, pdata in room_data["progression"].items(): + if "doors" in pdata: + process_progressive_door(room_name, progression_name, pdata["doors"]) + if "panel_doors" in pdata: + process_progressive_panel(room_name, progression_name, pdata["panel_doors"]) ALL_ROOMS.append(room_obj) @@ -492,8 +584,10 @@ if __name__ == '__main__': "ALL_ROOMS": ALL_ROOMS, "DOORS_BY_ROOM": DOORS_BY_ROOM, "PANELS_BY_ROOM": PANELS_BY_ROOM, + "PANEL_DOORS_BY_ROOM": PANEL_DOORS_BY_ROOM, "PROGRESSIVE_ITEMS": PROGRESSIVE_ITEMS, - "PROGRESSION_BY_ROOM": PROGRESSION_BY_ROOM, + "PROGRESSIVE_DOORS_BY_ROOM": PROGRESSIVE_DOORS_BY_ROOM, + "PROGRESSIVE_PANELS_BY_ROOM": PROGRESSIVE_PANELS_BY_ROOM, "PAINTING_ENTRANCES": PAINTING_ENTRANCES, "PAINTING_EXIT_ROOMS": PAINTING_EXIT_ROOMS, "PAINTING_EXITS": PAINTING_EXITS, @@ -506,6 +600,8 @@ if __name__ == '__main__': "DOOR_LOCATION_IDS": DOOR_LOCATION_IDS, "DOOR_ITEM_IDS": DOOR_ITEM_IDS, "DOOR_GROUP_ITEM_IDS": DOOR_GROUP_ITEM_IDS, + "PANEL_DOOR_ITEM_IDS": PANEL_DOOR_ITEM_IDS, + "PANEL_GROUP_ITEM_IDS": PANEL_GROUP_ITEM_IDS, "PROGRESSIVE_ITEM_IDS": PROGRESSIVE_ITEM_IDS, } diff --git a/worlds/lingo/utils/validate_config.rb b/worlds/lingo/utils/validate_config.rb index 498980bb71..70f7fc2cf6 100644 --- a/worlds/lingo/utils/validate_config.rb +++ b/worlds/lingo/utils/validate_config.rb @@ -33,19 +33,23 @@ end configured_rooms = Set["Menu"] configured_doors = Set[] configured_panels = Set[] +configured_panel_doors = Set[] mentioned_rooms = Set[] mentioned_doors = Set[] mentioned_panels = Set[] +mentioned_panel_doors = Set[] mentioned_sunwarp_entrances = Set[] mentioned_sunwarp_exits = Set[] mentioned_paintings = Set[] door_groups = {} +panel_groups = {} -directives = Set["entrances", "panels", "doors", "paintings", "sunwarps", "progression"] +directives = Set["entrances", "panels", "doors", "panel_doors", "paintings", "sunwarps", "progression"] panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt", "location_name"] door_directives = Set["id", "painting_id", "panels", "item_name", "item_group", "location_name", "skip_location", "skip_item", "door_group", "include_reduce", "event", "warp_id"] +panel_door_directives = Set["panels", "item_name", "panel_group"] painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"] non_counting = 0 @@ -253,6 +257,43 @@ config.each do |room_name, room| end end + (room["panel_doors"] || {}).each do |panel_door_name, panel_door| + configured_panel_doors.add("#{room_name} - #{panel_door_name}") + + if panel_door.include?("panels") + panel_door["panels"].each do |panel| + if panel.kind_of? Hash then + other_room = panel.include?("room") ? panel["room"] : room_name + mentioned_panels.add("#{other_room} - #{panel["panel"]}") + else + other_room = panel.include?("room") ? panel["room"] : room_name + mentioned_panels.add("#{room_name} - #{panel}") + end + end + else + puts "#{room_name} - #{panel_door_name} :::: Missing panels field" + end + + if panel_door.include?("panel_group") + panel_groups[panel_door["panel_group"]] ||= 0 + panel_groups[panel_door["panel_group"]] += 1 + end + + bad_subdirectives = [] + panel_door.keys.each do |key| + unless panel_door_directives.include?(key) then + bad_subdirectives << key + end + end + unless bad_subdirectives.empty? then + puts "#{room_name} - #{panel_door_name} :::: Panel door has the following invalid subdirectives: #{bad_subdirectives.join(", ")}" + end + + unless ids.include?("panel_doors") and ids["panel_doors"].include?(room_name) and ids["panel_doors"][room_name].include?(panel_door_name) + puts "#{room_name} - #{panel_door_name} :::: Panel door is missing an item ID" + end + end + (room["paintings"] || []).each do |painting| if painting.include?("id") and painting["id"].kind_of? String then unless paintings.include? painting["id"] then @@ -327,12 +368,24 @@ config.each do |room_name, room| end end - (room["progression"] || {}).each do |progression_name, door_list| - door_list.each do |door| - if door.kind_of? Hash then - mentioned_doors.add("#{door["room"]} - #{door["door"]}") - else - mentioned_doors.add("#{room_name} - #{door}") + (room["progression"] || {}).each do |progression_name, pdata| + if pdata.include? "doors" then + pdata["doors"].each do |door| + if door.kind_of? Hash then + mentioned_doors.add("#{door["room"]} - #{door["door"]}") + else + mentioned_doors.add("#{room_name} - #{door}") + end + end + end + + if pdata.include? "panel_doors" then + pdata["panel_doors"].each do |panel_door| + if panel_door.kind_of? Hash then + mentioned_panel_doors.add("#{panel_door["room"]} - #{panel_door["panel_door"]}") + else + mentioned_panel_doors.add("#{room_name} - #{panel_door}") + end end end @@ -344,17 +397,22 @@ end errored_rooms = mentioned_rooms - configured_rooms unless errored_rooms.empty? then - puts "The folloring rooms are mentioned but do not exist: " + errored_rooms.to_s + puts "The following rooms are mentioned but do not exist: " + errored_rooms.to_s end errored_panels = mentioned_panels - configured_panels unless errored_panels.empty? then - puts "The folloring panels are mentioned but do not exist: " + errored_panels.to_s + puts "The following panels are mentioned but do not exist: " + errored_panels.to_s end errored_doors = mentioned_doors - configured_doors unless errored_doors.empty? then - puts "The folloring doors are mentioned but do not exist: " + errored_doors.to_s + puts "The following doors are mentioned but do not exist: " + errored_doors.to_s +end + +errored_panel_doors = mentioned_panel_doors - configured_panel_doors +unless errored_panel_doors.empty? then + puts "The following panel doors are mentioned but do not exist: " + errored_panel_doors.to_s end door_groups.each do |group,num| @@ -367,6 +425,16 @@ door_groups.each do |group,num| end end +panel_groups.each do |group,num| + if num == 1 then + puts "Panel group \"#{group}\" only has one panel in it" + end + + unless ids.include?("panel_groups") and ids["panel_groups"].include?(group) + puts "#{group} :::: Panel group is missing an item ID" + end +end + slashed_rooms = configured_rooms.select do |room| room.include? "/" end diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index aa4f6ccf75..abdee26f57 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -52,8 +52,17 @@ class PokemonEmeraldWebWorld(WebWorld): "setup/es", ["nachocua"] ) + + setup_sv = Tutorial( + "Multivärld Installations Guide", + "En guide för att kunna spela Pokémon Emerald med Archipelago.", + "Svenska", + "setup_sv.md", + "setup/sv", + ["Tsukino"] + ) - tutorials = [setup_en, setup_es] + tutorials = [setup_en, setup_es, setup_sv] class PokemonEmeraldSettings(settings.Group): diff --git a/worlds/pokemon_emerald/docs/setup_sv.md b/worlds/pokemon_emerald/docs/setup_sv.md new file mode 100644 index 0000000000..88b1d38409 --- /dev/null +++ b/worlds/pokemon_emerald/docs/setup_sv.md @@ -0,0 +1,78 @@ +# Pokémon Emerald Installationsguide + +## Programvara som behövs + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- Ett engelskt Pokémon Emerald ROM, Archipelago kan inte hjälpa dig med detta. +- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 eller senare + +### Konfigurera BizHawk + +När du har installerat BizHawk, öppna `EmuHawk.exe` och ändra följande inställningar: + +- Om du använder BizHawk 2.7 eller 2.8, gå till `Config > Customize`. På "Advanced Tab", byt Lua core från +`NLua+KopiLua` till `Lua+LuaInterface`, starta om EmuHawk efteråt. (Använder du BizHawk 2.9, kan du skippa detta steg.) +- Gå till `Config > Customize`. Markera "Run in background" inställningen för att förhindra bortkoppling från +klienten om du alt-tabbar bort från EmuHawk. +- Öppna en `.gba` fil i EmuHawk och gå till `Config > Controllers…` för att konfigurera dina inputs. +Om du inte hittar `Controllers…`, starta ett valfritt `.gba` ROM först. +- Överväg att rensa keybinds i `Config > Hotkeys…` som du inte tänkt använda. Välj en keybind och tryck på ESC +för att rensa bort den. + +## Extra programvara + +- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), +används tillsammans med +[PopTracker](https://github.com/black-sliver/PopTracker/releases) + +## Generera och patcha ett spel + +1. Skapa din konfigurationsfil (YAML). Du kan göra en via att använda +[Pokémon Emerald options hemsida](../../../games/Pokemon%20Emerald/player-options). +2. Följ de allmänna Archipelago instruktionerna för att +[Generera ett spel](../../Archipelago/setup/en#generating-a-game). +Detta kommer generera en fil för dig. Din patchfil kommer ha `.apemerald` som sitt filnamnstillägg. +3. Öppna `ArchipelagoLauncher.exe` +4. Välj "Open Patch" på vänstra sidan, och välj din patchfil. +5. Om detta är första gången du patchar, så kommer du behöva välja var ditt ursprungliga ROM är. +6. En patchad `.gba` fil kommer skapas på samma plats som patchfilen. +7. Första gången du öppnar en patch med BizHawk-klienten, kommer du också behöva bekräfta var `EmuHawk.exe` filen är +installerad i din BizHawk-mapp. + +Om du bara tänkt spela själv och du inte bryr dig om automatisk spårning eller ledtrådar, så kan du stanna här, stänga +av klienten, och starta ditt patchade ROM med valfri emulator. Dock, för multvärldsfunktionen eller andra +Archipelago-funktioner, fortsätt nedanför med BizHawk. + +## Anslut till en server + +Om du vanligtsvis öppnar en patchad fil så görs steg 1-5 automatiskt åt dig. Även om det är så, kom ihåg dessa steg +ifall du till exempel behöver stänga ner och starta om något medans du spelar. + +1. Pokemon Emerald använder Archipelagos BizHawk-klient. Om klienten inte startat efter att du patchat ditt spel, +så kan du bara öppna den igen från launchern. +2. Dubbelkolla att EmuHawk faktiskt startat med den patchade ROM-filen. +3. I EmuHawk, gå till `Tools > Lua Console`. Luakonsolen måste vara igång medans du spelar. +4. I Luakonsolen, Tryck på `Script > Open Script…`. +5. Leta reda på din Archipelago-mapp och i den öppna `data/lua/connector_bizhawk_generic.lua`. +6. Emulatorn och klienten kommer så småningom ansluta till varandra. I BizHawk-klienten kommer du kunna see om allt är +anslutet och att Pokemon Emerald är igenkänt. +7. För att ansluta klienten till en server, skriv in din lobbyadress och port i textfältet t.ex. +`archipelago.gg:38281` +längst upp i din klient och tryck sen på "Connect". + +Du borde nu kunna ta emot och skicka föremål. Du behöver göra dom här stegen varje gång du vill ansluta igen. Det är +helt okej att göra saker offline utan att behöva oroa sig; allt kommer att synkronisera när du ansluter till servern +igen. + +## Automatisk Spårning + +Pokémon Emerald har en fullt fungerande spårare med stöd för automatisk spårning. + +1. Ladda ner [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest) +och +[PopTracker](https://github.com/black-sliver/PopTracker/releases). +2. Placera tracker pack zip-filen i packs/ där du har PopTracker installerat. +3. Öppna PopTracker, och välj Pokemon Emerald. +4. För att automatiskt spåra, tryck på "AP" symbolen längst upp. +5. Skriv in Archipelago-serverns uppgifter (Samma som du använde för att ansluta med klienten), "Slot"-namn samt +lösenord. diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index 251beb59cc..6aee25df26 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -427,7 +427,7 @@ location_data = [ LocationData("Seafoam Islands B3F", "Hidden Item Rock", "Max Elixir", rom_addresses['Hidden_Item_Seafoam_Islands_B3F'], Hidden(50), inclusion=hidden_items), LocationData("Vermilion City", "Hidden Item In Water Near Fan Club", "Max Ether", rom_addresses['Hidden_Item_Vermilion_City'], Hidden(51), inclusion=hidden_items), LocationData("Cerulean City-Badge House Backyard", "Hidden Item Gym Badge Guy's Backyard", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_City'], Hidden(52), inclusion=hidden_items), - LocationData("Route 4-E", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items), + LocationData("Route 4-C", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items), LocationData("Oak's Lab", "Oak's Parcel Reward", "Pokedex", rom_addresses["Event_Pokedex"], EventFlag(0x38)), diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 6056a171d3..d78c9f7d82 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -215,7 +215,6 @@ class SMZ3World(World): niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World) junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World) - allJunkItems = niceItems + junkItems self.junkItemsNames = [item.Type.name for item in junkItems] if (self.smz3World.Config.Keysanity): @@ -228,7 +227,8 @@ class SMZ3World(World): self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item)) itemPool = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in progressionItems] + \ - [SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in allJunkItems] + [SMZ3Item(item.Type.name, ItemClassification.useful, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in niceItems] + \ + [SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in junkItems] self.smz3DungeonItems = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in self.dungeon] self.multiworld.itempool += itemPool diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 1aba9af7ab..f9df8c292e 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -1,4 +1,5 @@ import logging +from random import Random from typing import Dict, Any, Iterable, Optional, Union, List, TextIO from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState @@ -27,15 +28,20 @@ from .strings.goal_names import Goal as GoalName from .strings.metal_names import Ore from .strings.region_names import Region as RegionName, LogicRegion +logger = logging.getLogger(__name__) + +STARDEW_VALLEY = "Stardew Valley" +UNIVERSAL_TRACKER_SEED_PROPERTY = "ut_seed" + client_version = 0 class StardewLocation(Location): - game: str = "Stardew Valley" + game: str = STARDEW_VALLEY class StardewItem(Item): - game: str = "Stardew Valley" + game: str = STARDEW_VALLEY class StardewWebWorld(WebWorld): @@ -60,7 +66,7 @@ class StardewValleyWorld(World): Stardew Valley is an open-ended country-life RPG. You can farm, fish, mine, fight, complete quests, befriend villagers, and uncover dark secrets. """ - game = "Stardew Valley" + game = STARDEW_VALLEY topology_present = False item_name_to_id = {name: data.code for name, data in item_table.items()} @@ -95,6 +101,17 @@ class StardewValleyWorld(World): self.total_progression_items = 0 # self.all_progression_items = dict() + # Taking the seed specified in slot data for UT, otherwise just generating the seed. + self.seed = getattr(multiworld, "re_gen_passthrough", {}).get(STARDEW_VALLEY, self.random.getrandbits(64)) + self.random = Random(self.seed) + + def interpret_slot_data(self, slot_data: Dict[str, Any]) -> Optional[int]: + # If the seed is not specified in the slot data, this mean the world was generated before Universal Tracker support. + seed = slot_data.get(UNIVERSAL_TRACKER_SEED_PROPERTY) + if seed is None: + logger.warning(f"World was generated before Universal Tracker support. Tracker might not be accurate.") + return seed + def generate_early(self): self.force_change_options_if_incompatible() self.content = create_content(self.options) @@ -108,12 +125,12 @@ class StardewValleyWorld(World): self.options.exclude_ginger_island.value = ExcludeGingerIsland.option_false goal_name = self.options.goal.current_key player_name = self.multiworld.player_name[self.player] - logging.warning( + logger.warning( f"Goal '{goal_name}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({player_name})") if exclude_ginger_island and self.options.walnutsanity != Walnutsanity.preset_none: self.options.walnutsanity.value = Walnutsanity.preset_none player_name = self.multiworld.player_name[self.player] - logging.warning( + logger.warning( f"Walnutsanity requires Ginger Island. Ginger Island was excluded from {self.player} ({player_name})'s world, so walnutsanity was force disabled") def create_regions(self): @@ -413,6 +430,7 @@ class StardewValleyWorld(World): included_option_names: List[str] = [option_name for option_name in self.options_dataclass.type_hints if option_name not in excluded_option_names] slot_data = self.options.as_dict(*included_option_names) slot_data.update({ + UNIVERSAL_TRACKER_SEED_PROPERTY: self.seed, "seed": self.random.randrange(1000000000), # Seed should be max 9 digits "randomized_entrances": self.randomized_entrances, "modified_bundles": bundles, diff --git a/worlds/stardew_valley/content/mods/alecto.py b/worlds/stardew_valley/content/mods/alecto.py new file mode 100644 index 0000000000..c05c936de3 --- /dev/null +++ b/worlds/stardew_valley/content/mods/alecto.py @@ -0,0 +1,33 @@ +from ..game_content import ContentPack, StardewContent +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data +from ...data.harvest import ForagingSource +from ...data.requirement import QuestRequirement +from ...mods.mod_data import ModNames +from ...strings.quest_names import ModQuest +from ...strings.region_names import Region +from ...strings.seed_names import DistantLandsSeed + + +class AlectoContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + if ModNames.distant_lands in content.registered_packs: + content.game_items.pop(DistantLandsSeed.void_mint) + content.game_items.pop(DistantLandsSeed.vile_ancient_fruit) + content.source_item(DistantLandsSeed.void_mint, + ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.WitchOrder),)),), + content.source_item(DistantLandsSeed.vile_ancient_fruit, + ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.WitchOrder),)), ), + + +register_mod_content_pack(ContentPack( + ModNames.alecto, + weak_dependencies=( + ModNames.distant_lands, # For Witch's order + ), + villagers=( + villagers_data.alecto, + ) + +)) diff --git a/worlds/stardew_valley/content/mods/archeology.py b/worlds/stardew_valley/content/mods/archeology.py index 97d38085d3..5eb8af4cfc 100644 --- a/worlds/stardew_valley/content/mods/archeology.py +++ b/worlds/stardew_valley/content/mods/archeology.py @@ -1,20 +1,34 @@ -from ..game_content import ContentPack +from ..game_content import ContentPack, StardewContent from ..mod_registry import register_mod_content_pack -from ...data.game_item import ItemTag, Tag -from ...data.shop import ShopSource +from ...data.artisan import MachineSource from ...data.skill import Skill from ...mods.mod_data import ModNames -from ...strings.book_names import ModBook -from ...strings.region_names import LogicRegion +from ...strings.craftable_names import ModMachine +from ...strings.fish_names import ModTrash +from ...strings.metal_names import all_artifacts, all_fossils from ...strings.skill_names import ModSkill -register_mod_content_pack(ContentPack( + +class ArchaeologyContentPack(ContentPack): + def artisan_good_hook(self, content: StardewContent): + # Done as honestly there are too many display items to put into the initial registration traditionally. + display_items = all_artifacts + all_fossils + for item in display_items: + self.source_display_items(item, content) + content.source_item(ModTrash.rusty_scrap, *(MachineSource(item=artifact, machine=ModMachine.grinder) for artifact in all_artifacts)) + + def source_display_items(self, item: str, content: StardewContent): + wood_display = f"Wooden Display: {item}" + hardwood_display = f"Hardwood Display: {item}" + if item == "Trilobite": + wood_display = f"Wooden Display: Trilobite Fossil" + hardwood_display = f"Hardwood Display: Trilobite Fossil" + content.source_item(wood_display, MachineSource(item=str(item), machine=ModMachine.preservation_chamber)) + content.source_item(hardwood_display, MachineSource(item=str(item), machine=ModMachine.hardwood_preservation_chamber)) + + +register_mod_content_pack(ArchaeologyContentPack( ModNames.archaeology, - shop_sources={ - ModBook.digging_like_worms: ( - Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), - ShopSource(money_price=500, shop_region=LogicRegion.bookseller_1),), - }, skills=(Skill(name=ModSkill.archaeology, has_mastery=False),), )) diff --git a/worlds/stardew_valley/content/mods/distant_lands.py b/worlds/stardew_valley/content/mods/distant_lands.py index 19380d4ff5..c5614d1302 100644 --- a/worlds/stardew_valley/content/mods/distant_lands.py +++ b/worlds/stardew_valley/content/mods/distant_lands.py @@ -1,9 +1,26 @@ -from ..game_content import ContentPack +from ..game_content import ContentPack, StardewContent from ..mod_registry import register_mod_content_pack from ...data import villagers_data, fish_data +from ...data.game_item import ItemTag, Tag +from ...data.harvest import ForagingSource, HarvestCropSource +from ...data.requirement import QuestRequirement from ...mods.mod_data import ModNames +from ...strings.crop_names import DistantLandsCrop +from ...strings.forageable_names import DistantLandsForageable +from ...strings.quest_names import ModQuest +from ...strings.region_names import Region +from ...strings.season_names import Season +from ...strings.seed_names import DistantLandsSeed -register_mod_content_pack(ContentPack( + +class DistantLandsContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + content.untag_item(DistantLandsSeed.void_mint, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(DistantLandsSeed.vile_ancient_fruit, tag=ItemTag.CROPSANITY_SEED) + + +register_mod_content_pack(DistantLandsContentPack( ModNames.distant_lands, fishes=( fish_data.void_minnow, @@ -13,5 +30,13 @@ register_mod_content_pack(ContentPack( ), villagers=( villagers_data.zic, - ) + ), + harvest_sources={ + DistantLandsForageable.swamp_herb: (ForagingSource(regions=(Region.witch_swamp,)),), + DistantLandsForageable.brown_amanita: (ForagingSource(regions=(Region.witch_swamp,)),), + DistantLandsSeed.void_mint: (ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.CorruptedCropsTask),)),), + DistantLandsCrop.void_mint: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=DistantLandsSeed.void_mint, seasons=(Season.spring, Season.summer, Season.fall)),), + DistantLandsSeed.vile_ancient_fruit: (ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.CorruptedCropsTask),)),), + DistantLandsCrop.vile_ancient_fruit: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=DistantLandsSeed.vile_ancient_fruit, seasons=(Season.spring, Season.summer, Season.fall)),) + } )) diff --git a/worlds/stardew_valley/content/mods/npc_mods.py b/worlds/stardew_valley/content/mods/npc_mods.py index 3172a55dbf..52d97d5c52 100644 --- a/worlds/stardew_valley/content/mods/npc_mods.py +++ b/worlds/stardew_valley/content/mods/npc_mods.py @@ -73,13 +73,6 @@ register_mod_content_pack(ContentPack( ) )) -register_mod_content_pack(ContentPack( - ModNames.alecto, - villagers=( - villagers_data.alecto, - ) -)) - register_mod_content_pack(ContentPack( ModNames.lacey, villagers=( diff --git a/worlds/stardew_valley/content/mods/sve.py b/worlds/stardew_valley/content/mods/sve.py index f74b80948c..a68d4ae9c0 100644 --- a/worlds/stardew_valley/content/mods/sve.py +++ b/worlds/stardew_valley/content/mods/sve.py @@ -3,15 +3,27 @@ from ..mod_registry import register_mod_content_pack from ..override import override from ..vanilla.ginger_island import ginger_island_content_pack as ginger_island_content_pack from ...data import villagers_data, fish_data -from ...data.harvest import ForagingSource -from ...data.requirement import YearRequirement +from ...data.game_item import ItemTag, Tag +from ...data.harvest import ForagingSource, HarvestCropSource +from ...data.requirement import YearRequirement, CombatRequirement, RelationshipRequirement, ToolRequirement, SkillRequirement, FishingRequirement +from ...data.shop import ShopSource from ...mods.mod_data import ModNames -from ...strings.crop_names import Fruit -from ...strings.fish_names import WaterItem +from ...strings.craftable_names import ModEdible +from ...strings.crop_names import Fruit, SVEVegetable, SVEFruit +from ...strings.fish_names import WaterItem, SVEFish, SVEWaterItem from ...strings.flower_names import Flower -from ...strings.forageable_names import Mushroom, Forageable -from ...strings.region_names import Region, SVERegion +from ...strings.food_names import SVEMeal, SVEBeverage +from ...strings.forageable_names import Mushroom, Forageable, SVEForage +from ...strings.gift_names import SVEGift +from ...strings.metal_names import Ore +from ...strings.monster_drop_names import ModLoot, Loot +from ...strings.performance_names import Performance +from ...strings.region_names import Region, SVERegion, LogicRegion from ...strings.season_names import Season +from ...strings.seed_names import SVESeed +from ...strings.skill_names import Skill +from ...strings.tool_names import Tool, ToolMaterial +from ...strings.villager_names import ModNPC class SVEContentPack(ContentPack): @@ -38,6 +50,24 @@ class SVEContentPack(ContentPack): # Remove Lance if Ginger Island is not in content since he is first encountered in Volcano Forge content.villagers.pop(villagers_data.lance.name) + def harvest_source_hook(self, content: StardewContent): + content.untag_item(SVESeed.shrub, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.fungus, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.slime, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.stalk, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.void, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.ancient_fern, tag=ItemTag.CROPSANITY_SEED) + if ginger_island_content_pack.name not in content.registered_packs: + # Remove Highlands seeds as these are behind Lance existing. + content.game_items.pop(SVESeed.void) + content.game_items.pop(SVEVegetable.void_root) + content.game_items.pop(SVESeed.stalk) + content.game_items.pop(SVEFruit.monster_fruit) + content.game_items.pop(SVESeed.fungus) + content.game_items.pop(SVEVegetable.monster_mushroom) + content.game_items.pop(SVESeed.slime) + content.game_items.pop(SVEFruit.slime_berry) + register_mod_content_pack(SVEContentPack( ModNames.sve, @@ -45,12 +75,24 @@ register_mod_content_pack(SVEContentPack( ginger_island_content_pack.name, ModNames.jasper, # To override Marlon and Gunther ), + shop_sources={ + SVEGift.aged_blue_moon_wine: (ShopSource(money_price=28000, shop_region=SVERegion.blue_moon_vineyard),), + SVEGift.blue_moon_wine: (ShopSource(money_price=3000, shop_region=SVERegion.blue_moon_vineyard),), + ModEdible.lightning_elixir: (ShopSource(money_price=12000, shop_region=SVERegion.galmoran_outpost),), + ModEdible.barbarian_elixir: (ShopSource(money_price=22000, shop_region=SVERegion.galmoran_outpost),), + ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),), + SVEMeal.grampleton_orange_chicken: (ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),), + ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),), + ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),), + SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),), + SVEMeal.stamina_capsule: (ShopSource(money_price=4000, shop_region=Region.hospital),), + }, harvest_sources={ Mushroom.red: ( ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.summer, Season.fall)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) ), Mushroom.purple: ( - ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave, SVERegion.junimo_woods), ) ), Mushroom.morel: ( ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) @@ -64,17 +106,59 @@ register_mod_content_pack(SVEContentPack( Flower.sunflower: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.summer,)),), Flower.fairy_rose: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.fall,)),), Fruit.ancient_fruit: ( - ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring, Season.summer, Season.fall), other_requirements=(YearRequirement(3),)), + ForagingSource(regions=(SVERegion.sprite_spring,), seasons=Season.not_winter, other_requirements=(YearRequirement(3),)), ForagingSource(regions=(SVERegion.sprite_spring_cave,)), ), Fruit.sweet_gem_berry: ( - ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring, Season.summer, Season.fall), other_requirements=(YearRequirement(3),)), + ForagingSource(regions=(SVERegion.sprite_spring,), seasons=Season.not_winter, other_requirements=(YearRequirement(3),)), ), + # New items + + ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),), + ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,), + other_requirements=(CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),), + ModLoot.swirl_stone: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.galaxy),)),), + ModLoot.void_soul: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),), + SVEForage.bearberry: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.winter,)),), + SVEForage.poison_mushroom: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.summer, Season.fall)),), + SVEForage.red_baneberry: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.summer, Season.summer)),), + SVEForage.ferngill_primrose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.spring,)),), + SVEForage.goldenrod: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.summer, Season.fall)),), + SVEForage.conch: (ForagingSource(regions=(Region.beach, SVERegion.fable_reef,)),), + SVEForage.dewdrop_berry: (ForagingSource(regions=(SVERegion.enchanted_grove,)),), + SVEForage.sand_dollar: (ForagingSource(regions=(Region.beach, SVERegion.fable_reef,), seasons=(Season.spring, Season.summer)),), + SVEForage.golden_ocean_flower: (ForagingSource(regions=(SVERegion.fable_reef,)),), + SVEForage.four_leaf_clover: (ForagingSource(regions=(Region.secret_woods, SVERegion.forest_west,), seasons=(Season.summer, Season.fall)),), + SVEForage.mushroom_colony: (ForagingSource(regions=(Region.secret_woods, SVERegion.junimo_woods, SVERegion.forest_west,), seasons=(Season.fall,)),), + SVEForage.rusty_blade: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),), + SVEForage.rafflesia: (ForagingSource(regions=(Region.secret_woods,), seasons=Season.not_winter),), + SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),), + ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),), + ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,), + other_requirements=(CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),), + SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),), + # Fable Reef WaterItem.coral: (ForagingSource(regions=(SVERegion.fable_reef,)),), Forageable.rainbow_shell: (ForagingSource(regions=(SVERegion.fable_reef,)),), WaterItem.sea_urchin: (ForagingSource(regions=(SVERegion.fable_reef,)),), + + # Crops + SVESeed.shrub: (ForagingSource(regions=(Region.secret_woods,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.salal_berry: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.shrub, seasons=(Season.spring,)),), + SVESeed.slime: (ForagingSource(regions=(SVERegion.highlands_outside,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.slime_berry: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.slime, seasons=(Season.spring,)),), + SVESeed.ancient_fern: (ForagingSource(regions=(Region.secret_woods,)),), + SVEVegetable.ancient_fiber: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.ancient_fern, seasons=(Season.summer,)),), + SVESeed.stalk: (ForagingSource(regions=(SVERegion.highlands_outside,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.monster_fruit: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.stalk, seasons=(Season.summer,)),), + SVESeed.fungus: (ForagingSource(regions=(SVERegion.highlands_pond,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEVegetable.monster_mushroom: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.fungus, seasons=(Season.fall,)),), + SVESeed.void: (ForagingSource(regions=(SVERegion.highlands_cavern,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEVegetable.void_root: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.void, seasons=(Season.winter,)),), + }, fishes=( fish_data.baby_lunaloo, # Removed when no ginger island diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 917e8cca22..220b46eae2 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -229,7 +229,7 @@ pelican_town = ContentPack( ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.mapping_cave_systems: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=Region.adventurer_guild_bedroom), + GenericSource(regions=(Region.adventurer_guild_bedroom,)), ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.monster_compendium: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), @@ -243,12 +243,12 @@ pelican_town = ContentPack( ShopSource(money_price=3000, shop_region=LogicRegion.bookseller_2),), Book.the_alleyway_buffet: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=Region.town, + GenericSource(regions=(Region.town,), other_requirements=(ToolRequirement(Tool.axe, ToolMaterial.iron), ToolRequirement(Tool.pickaxe, ToolMaterial.iron))), ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.the_art_o_crabbing: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=Region.beach, + GenericSource(regions=(Region.beach,), other_requirements=(ToolRequirement(Tool.fishing_rod, ToolMaterial.iridium), SkillRequirement(Skill.fishing, 6), SeasonRequirement(Season.winter))), diff --git a/worlds/stardew_valley/data/fish_data.py b/worlds/stardew_valley/data/fish_data.py index c6f0c30d41..26b1a0d58a 100644 --- a/worlds/stardew_valley/data/fish_data.py +++ b/worlds/stardew_valley/data/fish_data.py @@ -46,7 +46,8 @@ pirate_cove = (Region.pirate_cove,) crimson_badlands = (SVERegion.crimson_badlands,) shearwater = (SVERegion.shearwater,) -highlands = (SVERegion.highlands_outside,) +highlands_pond = (SVERegion.highlands_pond,) +highlands_cave = (SVERegion.highlands_cavern,) sprite_spring = (SVERegion.sprite_spring,) fable_reef = (SVERegion.fable_reef,) vineyard = (SVERegion.blue_moon_vineyard,) @@ -133,9 +134,9 @@ bonefish = create_fish(SVEFish.bonefish, crimson_badlands, season.all_seasons, 7 bull_trout = create_fish(SVEFish.bull_trout, forest_river, season.not_spring, 45, mod_name=ModNames.sve) butterfish = create_fish(SVEFish.butterfish, shearwater, season.not_winter, 75, mod_name=ModNames.sve) clownfish = create_fish(SVEFish.clownfish, ginger_island_ocean, season.all_seasons, 45, mod_name=ModNames.sve) -daggerfish = create_fish(SVEFish.daggerfish, highlands, season.all_seasons, 50, mod_name=ModNames.sve) +daggerfish = create_fish(SVEFish.daggerfish, highlands_pond, season.all_seasons, 50, mod_name=ModNames.sve) frog = create_fish(SVEFish.frog, mountain_lake, (season.spring, season.summer), 70, mod_name=ModNames.sve) -gemfish = create_fish(SVEFish.gemfish, highlands, season.all_seasons, 100, mod_name=ModNames.sve) +gemfish = create_fish(SVEFish.gemfish, highlands_cave, season.all_seasons, 100, mod_name=ModNames.sve) goldenfish = create_fish(SVEFish.goldenfish, sprite_spring, season.all_seasons, 60, mod_name=ModNames.sve) grass_carp = create_fish(SVEFish.grass_carp, secret_woods, (season.spring, season.summer), 85, mod_name=ModNames.sve) king_salmon = create_fish(SVEFish.king_salmon, forest_river, (season.spring, season.summer), 80, mod_name=ModNames.sve) diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index 2604ad2c46..e026090f86 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -307,7 +307,7 @@ id,name,classification,groups,mod_name 322,Phoenix Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", 323,Immunity Band,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", 324,Glowstone Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", -325,Fairy Dust Recipe,progression,, +325,Fairy Dust Recipe,progression,"GINGER_ISLAND", 326,Heavy Tapper Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND", 327,Hyper Speed-Gro Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND", 328,Deluxe Fertilizer Recipe,progression,QI_CRAFTING_RECIPE, diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 6e30d2b8c8..0d7a10f954 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2088,7 +2088,7 @@ id,region,name,tags,mod_name 3472,Farm,Craft Life Elixir,CRAFTSANITY, 3473,Farm,Craft Oil of Garlic,CRAFTSANITY, 3474,Farm,Craft Monster Musk,CRAFTSANITY, -3475,Farm,Craft Fairy Dust,CRAFTSANITY, +3475,Farm,Craft Fairy Dust,"CRAFTSANITY,GINGER_ISLAND", 3476,Farm,Craft Warp Totem: Beach,CRAFTSANITY, 3477,Farm,Craft Warp Totem: Mountains,CRAFTSANITY, 3478,Farm,Craft Warp Totem: Farm,CRAFTSANITY, @@ -2900,7 +2900,6 @@ id,region,name,tags,mod_name 7055,Abandoned Mines - 3,Abandoned Treasure - Floor 3,MANDATORY,Boarding House and Bus Stop Extension 7056,Abandoned Mines - 4,Abandoned Treasure - Floor 4,MANDATORY,Boarding House and Bus Stop Extension 7057,Abandoned Mines - 5,Abandoned Treasure - Floor 5,MANDATORY,Boarding House and Bus Stop Extension -7351,Farm,Read Digging Like Worms,"BOOKSANITY,BOOKSANITY_SKILL",Archaeology 7401,Farm,Cook Magic Elixir,COOKSANITY,Magic 7402,Farm,Craft Travel Core,CRAFTSANITY,Magic 7403,Farm,Craft Haste Elixir,CRAFTSANITY,Stardew Valley Expanded @@ -3280,10 +3279,10 @@ id,region,name,tags,mod_name 8237,Shipping,Shipsanity: Pterodactyl R Wing Bone,SHIPSANITY,Boarding House and Bus Stop Extension 8238,Shipping,Shipsanity: Scrap Rust,SHIPSANITY,Archaeology 8239,Shipping,Shipsanity: Rusty Path,SHIPSANITY,Archaeology -8240,Shipping,Shipsanity: Digging Like Worms,SHIPSANITY,Archaeology 8241,Shipping,Shipsanity: Digger's Delight,SHIPSANITY,Archaeology 8242,Shipping,Shipsanity: Rocky Root Coffee,SHIPSANITY,Archaeology 8243,Shipping,Shipsanity: Ancient Jello,SHIPSANITY,Archaeology 8244,Shipping,Shipsanity: Bone Fence,SHIPSANITY,Archaeology 8245,Shipping,Shipsanity: Grilled Cheese,SHIPSANITY,Binning Skill 8246,Shipping,Shipsanity: Fish Casserole,SHIPSANITY,Binning Skill +8247,Shipping,Shipsanity: Snatcher Worm,SHIPSANITY,Stardew Valley Expanded diff --git a/worlds/stardew_valley/data/recipe_data.py b/worlds/stardew_valley/data/recipe_data.py index b482468762..3123bb9243 100644 --- a/worlds/stardew_valley/data/recipe_data.py +++ b/worlds/stardew_valley/data/recipe_data.py @@ -5,7 +5,7 @@ from ..strings.animal_product_names import AnimalProduct from ..strings.artisan_good_names import ArtisanGood from ..strings.craftable_names import ModEdible, Edible from ..strings.crop_names import Fruit, Vegetable, SVEFruit, DistantLandsCrop -from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish +from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish, SVEWaterItem from ..strings.flower_names import Flower from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom from ..strings.ingredient_names import Ingredient @@ -195,7 +195,7 @@ mixed_berry_pie = shop_recipe(SVEMeal.mixed_berry_pie, Region.saloon, 3500, {Fru ModNames.sve) mushroom_berry_rice = friendship_and_shop_recipe(SVEMeal.mushroom_berry_rice, ModNPC.marlon, 6, Region.adventurer_guild, 1500, {SVEForage.poison_mushroom: 3, SVEForage.red_baneberry: 10, Ingredient.rice: 1, Ingredient.sugar: 2}, ModNames.sve) -seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEFish.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve) +seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEWaterItem.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve) void_delight = friendship_and_shop_recipe(SVEMeal.void_delight, NPC.krobus, 10, Region.sewer, 5000, {SVEFish.void_eel: 1, Loot.void_essence: 50, Loot.solar_essence: 20}, ModNames.sve) void_salmon_sushi = friendship_and_shop_recipe(SVEMeal.void_salmon_sushi, NPC.krobus, 10, Region.sewer, 5000, diff --git a/worlds/stardew_valley/data/requirement.py b/worlds/stardew_valley/data/requirement.py index 4744f9dffd..b2416d8d0b 100644 --- a/worlds/stardew_valley/data/requirement.py +++ b/worlds/stardew_valley/data/requirement.py @@ -31,6 +31,27 @@ class YearRequirement(Requirement): year: int +@dataclass(frozen=True) +class CombatRequirement(Requirement): + level: str + + +@dataclass(frozen=True) +class QuestRequirement(Requirement): + quest: str + + +@dataclass(frozen=True) +class RelationshipRequirement(Requirement): + npc: str + hearts: int + + +@dataclass(frozen=True) +class FishingRequirement(Requirement): + region: str + + @dataclass(frozen=True) class WalnutRequirement(Requirement): amount: int diff --git a/worlds/stardew_valley/data/shop.py b/worlds/stardew_valley/data/shop.py index ca54d35e14..f14dbac821 100644 --- a/worlds/stardew_valley/data/shop.py +++ b/worlds/stardew_valley/data/shop.py @@ -16,8 +16,8 @@ class ShopSource(ItemSource): other_requirements: Tuple[Requirement, ...] = () def __post_init__(self): - assert self.money_price or self.items_price, "At least money price or items price need to be defined." - assert self.items_price is None or all(type(p) == tuple for p in self.items_price), "Items price should be a tuple." + assert self.money_price is not None or self.items_price is not None, "At least money price or items price need to be defined." + assert self.items_price is None or all(isinstance(p, tuple) for p in self.items_price), "Items price should be a tuple." @dataclass(frozen=True, **kw_only) diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index cb61020169..31c7da5e3a 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -409,8 +409,9 @@ def create_special_quest_rewards(item_factory: StardewItemFactory, options: Star else: items.append(item_factory(Wallet.bears_knowledge, ItemClassification.useful)) # Not necessary outside of SVE items.append(item_factory(Wallet.iridium_snake_milk)) - items.append(item_factory("Fairy Dust Recipe")) items.append(item_factory("Dark Talisman")) + if options.exclude_ginger_island == ExcludeGingerIsland.option_false: + items.append(item_factory("Fairy Dust Recipe")) def create_help_wanted_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): diff --git a/worlds/stardew_valley/logic/requirement_logic.py b/worlds/stardew_valley/logic/requirement_logic.py index 9356440ac6..6a5adf4890 100644 --- a/worlds/stardew_valley/logic/requirement_logic.py +++ b/worlds/stardew_valley/logic/requirement_logic.py @@ -3,15 +3,20 @@ from typing import Union, Iterable from .base_logic import BaseLogicMixin, BaseLogic from .book_logic import BookLogicMixin +from .combat_logic import CombatLogicMixin +from .fishing_logic import FishingLogicMixin from .has_logic import HasLogicMixin +from .quest_logic import QuestLogicMixin from .received_logic import ReceivedLogicMixin +from .relationship_logic import RelationshipLogicMixin from .season_logic import SeasonLogicMixin 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, WalnutRequirement +from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, CombatRequirement, QuestRequirement, \ + RelationshipRequirement, FishingRequirement, WalnutRequirement class RequirementLogicMixin(BaseLogicMixin): @@ -21,7 +26,7 @@ class RequirementLogicMixin(BaseLogicMixin): class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin, -SeasonLogicMixin, TimeLogicMixin, WalnutLogicMixin]]): +SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, RelationshipLogicMixin, FishingLogicMixin, WalnutLogicMixin]]): def meet_all_requirements(self, requirements: Iterable[Requirement]): if not requirements: @@ -55,3 +60,21 @@ SeasonLogicMixin, TimeLogicMixin, WalnutLogicMixin]]): @meet_requirement.register def _(self, requirement: WalnutRequirement): return self.logic.walnut.has_walnut(requirement.amount) + + @meet_requirement.register + def _(self, requirement: CombatRequirement): + return self.logic.combat.can_fight_at_level(requirement.level) + + @meet_requirement.register + def _(self, requirement: QuestRequirement): + return self.logic.quest.can_complete_quest(requirement.quest) + + @meet_requirement.register + def _(self, requirement: RelationshipRequirement): + return self.logic.relationship.has_hearts(requirement.npc, requirement.hearts) + + @meet_requirement.register + def _(self, requirement: FishingRequirement): + return self.logic.fishing.can_fish_at(requirement.region) + + diff --git a/worlds/stardew_valley/mods/logic/item_logic.py b/worlds/stardew_valley/mods/logic/item_logic.py index cfafc88e83..ef5eab0134 100644 --- a/worlds/stardew_valley/mods/logic/item_logic.py +++ b/worlds/stardew_valley/mods/logic/item_logic.py @@ -23,24 +23,15 @@ from ...logic.tool_logic import ToolLogicMixin from ...options import Cropsanity from ...stardew_rule import StardewRule, True_ from ...strings.artisan_good_names import ModArtisanGood -from ...strings.craftable_names import ModCraftable, ModEdible, ModMachine -from ...strings.crop_names import SVEVegetable, SVEFruit, DistantLandsCrop -from ...strings.fish_names import ModTrash, SVEFish -from ...strings.food_names import SVEMeal, SVEBeverage -from ...strings.forageable_names import SVEForage, DistantLandsForageable -from ...strings.gift_names import SVEGift +from ...strings.craftable_names import ModCraftable, ModMachine +from ...strings.fish_names import ModTrash from ...strings.ingredient_names import Ingredient from ...strings.material_names import Material from ...strings.metal_names import all_fossils, all_artifacts, Ore, ModFossil -from ...strings.monster_drop_names import ModLoot, Loot +from ...strings.monster_drop_names import Loot from ...strings.performance_names import Performance -from ...strings.quest_names import ModQuest -from ...strings.region_names import Region, SVERegion, DeepWoodsRegion, BoardingHouseRegion -from ...strings.season_names import Season -from ...strings.seed_names import SVESeed, DistantLandsSeed -from ...strings.skill_names import Skill +from ...strings.region_names import SVERegion, DeepWoodsRegion, BoardingHouseRegion from ...strings.tool_names import Tool, ToolMaterial -from ...strings.villager_names import ModNPC display_types = [ModCraftable.wooden_display, ModCraftable.hardwood_display] display_items = all_artifacts + all_fossils @@ -58,12 +49,6 @@ FarmingLogicMixin]]): def get_modded_item_rules(self) -> Dict[str, StardewRule]: items = dict() - if ModNames.sve in self.options.mods: - items.update(self.get_sve_item_rules()) - if ModNames.archaeology in self.options.mods: - items.update(self.get_archaeology_item_rules()) - if ModNames.distant_lands in self.options.mods: - items.update(self.get_distant_lands_item_rules()) if ModNames.boarding_house in self.options.mods: items.update(self.get_boarding_house_item_rules()) return items @@ -75,61 +60,6 @@ FarmingLogicMixin]]): item_rule.update(self.get_modified_item_rules_for_deep_woods(item_rule)) return item_rule - def get_sve_item_rules(self): - return {SVEGift.aged_blue_moon_wine: self.logic.money.can_spend_at(SVERegion.sophias_house, 28000), - SVEGift.blue_moon_wine: self.logic.money.can_spend_at(SVERegion.sophias_house, 3000), - SVESeed.fungus: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, - ModLoot.green_mushroom: self.logic.region.can_reach(SVERegion.highlands_outside) & - self.logic.tool.has_tool(Tool.axe, ToolMaterial.iron) & self.logic.season.has_any_not_winter(), - SVEFruit.monster_fruit: self.logic.season.has(Season.summer) & self.logic.has(SVESeed.stalk), - SVEVegetable.monster_mushroom: self.logic.season.has(Season.fall) & self.logic.has(SVESeed.fungus), - ModLoot.ornate_treasure_chest: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_galaxy_weapon & - self.logic.tool.has_tool(Tool.axe, ToolMaterial.iron), - SVEFruit.slime_berry: self.logic.season.has(Season.spring) & self.logic.has(SVESeed.slime), - SVESeed.slime: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, - SVESeed.stalk: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, - ModLoot.swirl_stone: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - SVEVegetable.void_root: self.logic.season.has(Season.winter) & self.logic.has(SVESeed.void), - SVESeed.void: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, - ModLoot.void_soul: self.logic.region.can_reach( - SVERegion.crimson_badlands) & self.logic.combat.has_good_weapon & self.logic.cooking.can_cook(), - SVEForage.winter_star_rose: self.logic.region.can_reach(SVERegion.summit) & self.logic.season.has(Season.winter), - SVEForage.bearberry: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.winter), - SVEForage.poison_mushroom: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has_any([Season.summer, Season.fall]), - SVEForage.red_baneberry: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.summer), - SVEForage.ferngill_primrose: self.logic.region.can_reach(SVERegion.summit) & self.logic.season.has(Season.spring), - SVEForage.goldenrod: self.logic.region.can_reach(SVERegion.summit) & ( - self.logic.season.has(Season.summer) | self.logic.season.has(Season.fall)), - SVESeed.shrub: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), - SVEFruit.salal_berry: self.logic.farming.can_plant_and_grow_item((Season.spring, Season.summer)) & self.logic.has(SVESeed.shrub), - ModEdible.aegis_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 28000), - ModEdible.lightning_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 12000), - ModEdible.barbarian_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 22000), - ModEdible.gravity_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 4000), - SVESeed.ancient_fern: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), - SVEVegetable.ancient_fiber: self.logic.farming.can_plant_and_grow_item(Season.summer) & self.logic.has(SVESeed.ancient_fern), - SVEForage.conch: self.logic.region.can_reach_any((Region.beach, SVERegion.fable_reef)), - SVEForage.dewdrop_berry: self.logic.region.can_reach(SVERegion.enchanted_grove), - SVEForage.sand_dollar: self.logic.region.can_reach(SVERegion.fable_reef) | (self.logic.region.can_reach(Region.beach) & - self.logic.season.has_any([Season.summer, Season.fall])), - SVEForage.golden_ocean_flower: self.logic.region.can_reach(SVERegion.fable_reef), - SVEMeal.grampleton_orange_chicken: self.logic.money.can_spend_at(Region.saloon, 650) & self.logic.relationship.has_hearts(ModNPC.sophia, 6), - ModEdible.hero_elixir: self.logic.money.can_spend_at(SVERegion.isaac_shop, 8000), - SVEForage.four_leaf_clover: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.forest_west)) & - self.logic.season.has_any([Season.spring, Season.summer]), - SVEForage.mushroom_colony: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.junimo_woods, SVERegion.forest_west)) & - self.logic.season.has(Season.fall), - SVEForage.rusty_blade: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - SVEForage.rafflesia: self.logic.region.can_reach(Region.secret_woods), - SVEBeverage.sports_drink: self.logic.money.can_spend_at(Region.hospital, 750), - "Stamina Capsule": self.logic.money.can_spend_at(Region.hospital, 4000), - SVEForage.thistle: self.logic.region.can_reach(SVERegion.summit), - ModLoot.void_pebble: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - ModLoot.void_shard: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_galaxy_weapon & - self.logic.skill.has_level(Skill.combat, 10) & self.logic.region.can_reach(Region.saloon) & self.logic.time.has_year_three - } - # @formatter:on - def get_modified_item_rules_for_sve(self, items: Dict[str, StardewRule]): return { Loot.void_essence: items[Loot.void_essence] | self.logic.region.can_reach(SVERegion.highlands_cavern) | self.logic.region.can_reach( @@ -141,7 +71,7 @@ FarmingLogicMixin]]): self.logic.combat.can_fight_at_level(Performance.great)), Ore.iridium: items[Ore.iridium] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.crimson_badlands) & self.logic.combat.can_fight_at_level(Performance.maximum)), - SVEFish.dulse_seaweed: self.logic.fishing.can_fish_at(Region.beach) & self.logic.season.has_any([Season.spring, Season.summer, Season.winter]) + } def get_modified_item_rules_for_deep_woods(self, items: Dict[str, StardewRule]): @@ -160,36 +90,6 @@ FarmingLogicMixin]]): return options_to_update - def get_archaeology_item_rules(self): - archaeology_item_rules = {} - preservation_chamber_rule = self.logic.has(ModMachine.preservation_chamber) - hardwood_preservation_chamber_rule = self.logic.has(ModMachine.hardwood_preservation_chamber) - for item in display_items: - for display_type in display_types: - if item == "Trilobite": - location_name = f"{display_type}: Trilobite Fossil" - else: - location_name = f"{display_type}: {item}" - display_item_rule = self.logic.crafting.can_craft(all_crafting_recipes_by_name[display_type]) & self.logic.has(item) - if "Wooden" in display_type: - archaeology_item_rules[location_name] = display_item_rule & preservation_chamber_rule - else: - archaeology_item_rules[location_name] = display_item_rule & hardwood_preservation_chamber_rule - archaeology_item_rules[ModTrash.rusty_scrap] = self.logic.has(ModMachine.grinder) & self.logic.has_any(*all_artifacts) - return archaeology_item_rules - - def get_distant_lands_item_rules(self): - return { - DistantLandsForageable.swamp_herb: self.logic.region.can_reach(Region.witch_swamp), - DistantLandsForageable.brown_amanita: self.logic.region.can_reach(Region.witch_swamp), - DistantLandsSeed.vile_ancient_fruit: self.logic.quest.can_complete_quest(ModQuest.WitchOrder) | self.logic.quest.can_complete_quest( - ModQuest.CorruptedCropsTask), - DistantLandsSeed.void_mint: self.logic.quest.can_complete_quest(ModQuest.WitchOrder) | self.logic.quest.can_complete_quest( - ModQuest.CorruptedCropsTask), - DistantLandsCrop.void_mint: self.logic.season.has_any_not_winter() & self.logic.has(DistantLandsSeed.void_mint), - DistantLandsCrop.vile_ancient_fruit: self.logic.season.has_any_not_winter() & self.logic.has(DistantLandsSeed.vile_ancient_fruit), - } - def get_boarding_house_item_rules(self): return { # Mob Drops from lost valley enemies @@ -251,8 +151,3 @@ FarmingLogicMixin]]): BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level( Performance.great), } - - def has_seed_unlocked(self, seed_name: str): - if self.options.cropsanity == Cropsanity.option_disabled: - return True_() - return self.logic.received(seed_name) diff --git a/worlds/stardew_valley/mods/mod_regions.py b/worlds/stardew_valley/mods/mod_regions.py index c075bd4d10..a402ba6068 100644 --- a/worlds/stardew_valley/mods/mod_regions.py +++ b/worlds/stardew_valley/mods/mod_regions.py @@ -183,7 +183,8 @@ stardew_valley_expanded_regions = [ RegionData(SVERegion.first_slash_guild, [SVEEntrance.first_slash_guild_to_hallway], is_ginger_island=True), RegionData(SVERegion.first_slash_hallway, [SVEEntrance.first_slash_hallway_to_room], is_ginger_island=True), RegionData(SVERegion.first_slash_spare_room, is_ginger_island=True), - RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave], is_ginger_island=True), + RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave, SVEEntrance.highlands_to_pond], is_ginger_island=True), + RegionData(SVERegion.highlands_pond, is_ginger_island=True), RegionData(SVERegion.highlands_cavern, [SVEEntrance.to_dwarf_prison], is_ginger_island=True), RegionData(SVERegion.dwarf_prison, is_ginger_island=True), RegionData(SVERegion.lances_house, [SVEEntrance.lance_to_ladder], is_ginger_island=True), @@ -276,6 +277,7 @@ mandatory_sve_connections = [ ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.fish_shop_to_willy_bedroom, SVERegion.willy_bedroom, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.museum_to_gunther_bedroom, SVERegion.gunther_bedroom, flag=RandomizationFlag.BUILDINGS), + ConnectionData(SVEEntrance.highlands_to_pond, SVERegion.highlands_pond), ] alecto_regions = [ diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index 2aca2d3f4d..b0fc7fa0ea 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -137,7 +137,8 @@ vanilla_regions = [ [Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave, Entrance.island_west_to_crystals_cave, Entrance.island_west_to_shipwreck, Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk, Entrance.parrot_express_jungle_to_docks, Entrance.parrot_express_jungle_to_dig_site, Entrance.parrot_express_jungle_to_volcano, LogicEntrance.grow_spring_crops_on_island, - LogicEntrance.grow_summer_crops_on_island, LogicEntrance.grow_fall_crops_on_island, LogicEntrance.grow_winter_crops_on_island, LogicEntrance.grow_indoor_crops_on_island], + LogicEntrance.grow_summer_crops_on_island, LogicEntrance.grow_fall_crops_on_island, LogicEntrance.grow_winter_crops_on_island, + LogicEntrance.grow_indoor_crops_on_island], is_ginger_island=True), RegionData(Region.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine], is_ginger_island=True), RegionData(Region.island_shrine, is_ginger_island=True), @@ -536,7 +537,7 @@ def create_final_regions(world_options) -> List[RegionData]: def create_final_connections_and_regions(world_options) -> Tuple[Dict[str, ConnectionData], Dict[str, RegionData]]: regions_data: Dict[str, RegionData] = {region.name: region for region in create_final_regions(world_options)} connections = {connection.name: connection for connection in vanilla_connections} - connections = modify_connections_for_mods(connections, world_options.mods) + connections = modify_connections_for_mods(connections, sorted(world_options.mods.value)) include_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_false return remove_ginger_island_regions_and_connections(regions_data, connections, include_island) @@ -563,10 +564,8 @@ def remove_ginger_island_regions_and_connections(regions_by_name: Dict[str, Regi return connections, regions_by_name -def modify_connections_for_mods(connections: Dict[str, ConnectionData], mods) -> Dict[str, ConnectionData]: - if mods is None: - return connections - for mod in mods.value: +def modify_connections_for_mods(connections: Dict[str, ConnectionData], mods: Iterable) -> Dict[str, ConnectionData]: + for mod in mods: if mod not in ModDataList: continue if mod in vanilla_connections_to_remove_by_mod: diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 7c1fdbda3c..89b1cf87c3 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -1031,6 +1031,7 @@ def set_sve_ginger_island_rules(logic: StardewLogic, multiworld: MultiWorld, pla set_entrance_rule(multiworld, player, SVEEntrance.wizard_to_fable_reef, logic.received(SVEQuestItem.fable_reef_portal)) set_entrance_rule(multiworld, player, SVEEntrance.highlands_to_cave, logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron) & logic.tool.has_tool(Tool.axe, ToolMaterial.iron)) + set_entrance_rule(multiworld, player, SVEEntrance.highlands_to_pond, logic.tool.has_tool(Tool.axe, ToolMaterial.iron)) def set_boarding_house_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): diff --git a/worlds/stardew_valley/strings/book_names.py b/worlds/stardew_valley/strings/book_names.py index 3c32cd81b3..6c271f42ae 100644 --- a/worlds/stardew_valley/strings/book_names.py +++ b/worlds/stardew_valley/strings/book_names.py @@ -27,10 +27,6 @@ class Book: the_diamond_hunter = "The Diamond Hunter" -class ModBook: - digging_like_worms = "Digging Like Worms" - - ordered_lost_books = [] all_lost_books = set() diff --git a/worlds/stardew_valley/strings/entrance_names.py b/worlds/stardew_valley/strings/entrance_names.py index 9b651f4276..58a919f2a8 100644 --- a/worlds/stardew_valley/strings/entrance_names.py +++ b/worlds/stardew_valley/strings/entrance_names.py @@ -358,6 +358,7 @@ class SVEEntrance: sprite_spring_to_cave = "Sprite Spring to Sprite Spring Cave" fish_shop_to_willy_bedroom = "Willy's Fish Shop to Willy's Bedroom" museum_to_gunther_bedroom = "Museum to Gunther's Bedroom" + highlands_to_pond = "Highlands to Highlands Pond" class AlectoEntrance: diff --git a/worlds/stardew_valley/strings/fish_names.py b/worlds/stardew_valley/strings/fish_names.py index d94f9e2fd4..d4ee81430e 100644 --- a/worlds/stardew_valley/strings/fish_names.py +++ b/worlds/stardew_valley/strings/fish_names.py @@ -137,7 +137,6 @@ class SVEFish: void_eel = "Void Eel" water_grub = "Water Grub" sea_sponge = "Sea Sponge" - dulse_seaweed = "Dulse Seaweed" class DistantLandsFish: @@ -147,6 +146,10 @@ class DistantLandsFish: giant_horsehoe_crab = "Giant Horsehoe Crab" +class SVEWaterItem: + dulse_seaweed = "Dulse Seaweed" + + class ModTrash: rusty_scrap = "Scrap Rust" diff --git a/worlds/stardew_valley/strings/food_names.py b/worlds/stardew_valley/strings/food_names.py index 5555316f83..03784336d1 100644 --- a/worlds/stardew_valley/strings/food_names.py +++ b/worlds/stardew_valley/strings/food_names.py @@ -102,6 +102,7 @@ class SVEMeal: void_delight = "Void Delight" void_salmon_sushi = "Void Salmon Sushi" grampleton_orange_chicken = "Grampleton Orange Chicken" + stamina_capsule = "Stamina Capsule" class TrashyMeal: diff --git a/worlds/stardew_valley/strings/region_names.py b/worlds/stardew_valley/strings/region_names.py index 9cedb6b8ef..58763b6fcb 100644 --- a/worlds/stardew_valley/strings/region_names.py +++ b/worlds/stardew_valley/strings/region_names.py @@ -296,6 +296,7 @@ class SVERegion: sprite_spring_cave = "Sprite Spring Cave" willy_bedroom = "Willy's Bedroom" gunther_bedroom = "Gunther's Bedroom" + highlands_pond = "Highlands Pond" class AlectoRegion: diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index bee02f3c3d..d077432e24 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -441,6 +441,16 @@ def setup_multiworld(test_options: Iterable[Dict[str, int]] = None, seed=None) - for i in range(1, len(test_options) + 1): multiworld.game[i] = StardewValleyWorld.game multiworld.player_name.update({i: f"Tester{i}"}) + args = create_args(test_options) + multiworld.set_options(args) + + for step in gen_steps: + call_all(multiworld, step) + + return multiworld + + +def create_args(test_options): args = Namespace() for name, option in StardewValleyWorld.options_dataclass.type_hints.items(): options = {} @@ -449,9 +459,4 @@ def setup_multiworld(test_options: Iterable[Dict[str, int]] = None, seed=None) - value = option(player_options[name]) if name in player_options else option.from_any(option.default) options.update({i: value}) setattr(args, name, options) - multiworld.set_options(args) - - for step in gen_steps: - call_all(multiworld, step) - - return multiworld + return args diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 5e7e9d4143..97184b1338 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -14,7 +14,8 @@ class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): def test_given_single_mods_when_generate_then_basic_checks(self): for mod in options.Mods.valid_keys: - with self.solo_world_sub_test(f"Mod: {mod}", {options.Mods: mod}) as (multi_world, _): + world_options = {options.Mods: mod, options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false} + with self.solo_world_sub_test(f"Mod: {mod}", world_options) as (multi_world, _): self.assert_basic_checks(multi_world) self.assert_stray_mod_items(mod, multi_world) @@ -22,8 +23,9 @@ class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): for option in options.EntranceRandomization.options: for mod in options.Mods.valid_keys: world_options = { - options.EntranceRandomization.internal_name: options.EntranceRandomization.options[option], - options.Mods: mod + options.EntranceRandomization: options.EntranceRandomization.options[option], + options.Mods: mod, + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false } with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options) as (multi_world, _): self.assert_basic_checks(multi_world) diff --git a/worlds/stardew_valley/test/rules/TestBundles.py b/worlds/stardew_valley/test/rules/TestBundles.py index 25d4c70b2a..ab376c90d4 100644 --- a/worlds/stardew_valley/test/rules/TestBundles.py +++ b/worlds/stardew_valley/test/rules/TestBundles.py @@ -37,7 +37,7 @@ class TestRaccoonBundlesLogic(SVTestBase): options.BundlePrice: options.BundlePrice.option_normal, options.Craftsanity: options.Craftsanity.option_all, } - seed = 1234 # Magic seed that does what I want. Might need to get changed if we change the randomness behavior of raccoon bundles + seed = 2 # Magic seed that does what I want. Might need to get changed if we change the randomness behavior of raccoon bundles def test_raccoon_bundles_rely_on_previous_ones(self): # The first raccoon bundle is a fishing one diff --git a/worlds/stardew_valley/test/stability/StabilityOutputScript.py b/worlds/stardew_valley/test/stability/StabilityOutputScript.py index 4b31011d9f..c8918d6cf2 100644 --- a/worlds/stardew_valley/test/stability/StabilityOutputScript.py +++ b/worlds/stardew_valley/test/stability/StabilityOutputScript.py @@ -1,6 +1,7 @@ import argparse import json +from ...options import FarmType, EntranceRandomization from ...test import setup_solo_multiworld, allsanity_mods_6_x_x if __name__ == "__main__": @@ -10,21 +11,23 @@ if __name__ == "__main__": args = parser.parse_args() seed = args.seed - multi_world = setup_solo_multiworld( - allsanity_mods_6_x_x(), - seed=seed - ) + options = allsanity_mods_6_x_x() + options[FarmType.internal_name] = FarmType.option_standard + options[EntranceRandomization.internal_name] = EntranceRandomization.option_buildings + multi_world = setup_solo_multiworld(options, seed=seed) + world = multi_world.worlds[1] output = { "bundles": { bundle_room.name: { bundle.name: str(bundle.items) for bundle in bundle_room.bundles } - for bundle_room in multi_world.worlds[1].modified_bundles + for bundle_room in world.modified_bundles }, "items": [item.name for item in multi_world.get_items()], - "location_rules": {location.name: repr(location.access_rule) for location in multi_world.get_locations(1)} + "location_rules": {location.name: repr(location.access_rule) for location in multi_world.get_locations(1)}, + "slot_data": world.fill_slot_data() } print(json.dumps(output)) diff --git a/worlds/stardew_valley/test/stability/TestStability.py b/worlds/stardew_valley/test/stability/TestStability.py index aaa8b33184..8bb904a56e 100644 --- a/worlds/stardew_valley/test/stability/TestStability.py +++ b/worlds/stardew_valley/test/stability/TestStability.py @@ -24,8 +24,7 @@ class TestGenerationIsStable(SVTestCase): if self.skip_long_tests: raise unittest.SkipTest("Long tests disabled") - # seed = get_seed(33778671150797368040) # troubleshooting seed - seed = get_seed(74716545478307145559) + seed = get_seed() output_a = subprocess.check_output([sys.executable, '-m', 'worlds.stardew_valley.test.stability.StabilityOutputScript', '--seed', str(seed)]) output_b = subprocess.check_output([sys.executable, '-m', 'worlds.stardew_valley.test.stability.StabilityOutputScript', '--seed', str(seed)]) @@ -54,3 +53,6 @@ class TestGenerationIsStable(SVTestCase): # We check that the actual rule has the same order to make sure it is evaluated in the same order, # so performance tests are repeatable as much as possible. self.assertEqual(rule_a, rule_b, f"Location rule of {location_a} at index {i} is different between both executions. Seed={seed}") + + for key, value in result_a["slot_data"].items(): + self.assertEqual(value, result_b["slot_data"][key], f"Slot data {key} is different between both executions. Seed={seed}") diff --git a/worlds/stardew_valley/test/stability/TestUniversalTracker.py b/worlds/stardew_valley/test/stability/TestUniversalTracker.py new file mode 100644 index 0000000000..3e33409834 --- /dev/null +++ b/worlds/stardew_valley/test/stability/TestUniversalTracker.py @@ -0,0 +1,52 @@ +import unittest +from unittest.mock import Mock + +from .. import SVTestBase, create_args, allsanity_mods_6_x_x +from ... import STARDEW_VALLEY, FarmType, BundleRandomization, EntranceRandomization + + +class TestUniversalTrackerGenerationIsStable(SVTestBase): + options = allsanity_mods_6_x_x() + options.update({ + EntranceRandomization.internal_name: EntranceRandomization.option_buildings, + BundleRandomization.internal_name: BundleRandomization.option_shuffled, + FarmType.internal_name: FarmType.option_standard, # Need to choose one otherwise it's random + }) + + def test_all_locations_and_items_are_the_same_between_two_generations(self): + # This might open a kivy window temporarily, but it's the only way to test this... + if self.skip_long_tests: + raise unittest.SkipTest("Long tests disabled") + + try: + # This test only run if UT is present, so no risk of running in the CI. + from worlds.tracker.TrackerClient import TrackerGameContext # noqa + except ImportError: + raise unittest.SkipTest("UT not loaded, skipping test") + + slot_data = self.world.fill_slot_data() + ut_data = self.world.interpret_slot_data(slot_data) + + fake_context = Mock() + fake_context.re_gen_passthrough = {STARDEW_VALLEY: ut_data} + args = create_args({0: self.options}) + args.outputpath = None + args.outputname = None + args.multi = 1 + args.race = None + args.plando_options = self.multiworld.plando_options + args.plando_items = self.multiworld.plando_items + args.plando_texts = self.multiworld.plando_texts + args.plando_connections = self.multiworld.plando_connections + args.game = self.multiworld.game + args.name = self.multiworld.player_name + args.sprite = {} + args.sprite_pool = {} + args.skip_output = True + + generated_multi_world = TrackerGameContext.TMain(fake_context, args, self.multiworld.seed) + generated_slot_data = generated_multi_world.worlds[1].fill_slot_data() + + # Just checking slot data should prove that UT generates the same result as AP generation. + self.maxDiff = None + self.assertEqual(slot_data, generated_slot_data) diff --git a/worlds/subnautica/rules.py b/worlds/subnautica/rules.py index 3b6c5cd4dd..ea9ec6a805 100644 --- a/worlds/subnautica/rules.py +++ b/worlds/subnautica/rules.py @@ -150,7 +150,7 @@ def has_ultra_glide_fins(state: "CollectionState", player: int) -> bool: def get_max_swim_depth(state: "CollectionState", player: int) -> int: - swim_rule: SwimRule = state.multiworld.swim_rule[player] + swim_rule: SwimRule = state.multiworld.worlds[player].options.swim_rule depth: int = swim_rule.base_depth if swim_rule.consider_items: if has_seaglide(state, player): @@ -296,7 +296,7 @@ def set_rules(subnautica_world: "SubnauticaWorld"): set_location_rule(multiworld, player, loc) if subnautica_world.creatures_to_scan: - option = multiworld.creature_scan_logic[player] + option = multiworld.worlds[player].options.creature_scan_logic for creature_name in subnautica_world.creatures_to_scan: location = set_creature_rule(multiworld, player, creature_name) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 10c2a01cf7..4fb99c11ed 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -1,7 +1,8 @@ from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union from logging import warning -from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld -from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names +from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState +from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names, + combat_items) from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon from .er_rules import set_er_location_rules @@ -9,7 +10,8 @@ from .regions import tunic_regions from .er_scripts import create_er_regions, verify_plando_directions from .er_data import portal_mapping from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections, - LaurelsLocation, EntranceLayout) + LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage, EntranceLayout) +from .combat_logic import area_data, CombatState from worlds.AutoWorld import WebWorld, World from Options import PlandoConnection, OptionError from decimal import Decimal, ROUND_HALF_UP @@ -84,6 +86,12 @@ class TunicWorld(World): shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected def generate_early(self) -> None: + if self.options.logic_rules >= LogicRules.option_no_major_glitches: + self.options.laurels_zips.value = LaurelsZips.option_true + self.options.ice_grappling.value = IceGrappling.option_medium + if self.options.logic_rules.value == LogicRules.option_unrestricted: + self.options.ladder_storage.value = LadderStorage.option_medium + if self.options.plando_connections: for index, cxn in enumerate(self.options.plando_connections): # making shops second to simplify other things later @@ -128,6 +136,15 @@ class TunicWorld(World): def stage_generate_early(cls, multiworld: MultiWorld) -> None: tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC") for tunic in tunic_worlds: + # setting up state combat logic stuff, see has_combat_reqs for its use + # and this is magic so pycharm doesn't like it, unfortunately + if tunic.options.combat_logic: + multiworld.state.tunic_need_to_reset_combat_from_collect[tunic.player] = False + multiworld.state.tunic_need_to_reset_combat_from_remove[tunic.player] = False + multiworld.state.tunic_area_combat_state[tunic.player] = {} + for area_name in area_data.keys(): + multiworld.state.tunic_area_combat_state[tunic.player][area_name] = CombatState.unchecked + # if it's one of the options, then it isn't a custom seed group if tunic.options.entrance_rando.value in EntranceRando.options.values(): continue @@ -363,6 +380,19 @@ class TunicWorld(World): def get_filler_item_name(self) -> str: return self.random.choice(filler_items) + # cache whether you can get through combat logic areas + def collect(self, state: CollectionState, item: Item) -> bool: + change = super().collect(state, item) + if change and self.options.combat_logic and item.name in combat_items: + state.tunic_need_to_reset_combat_from_collect[self.player] = True + return change + + def remove(self, state: CollectionState, item: Item) -> bool: + change = super().remove(state, item) + if change and self.options.combat_logic and item.name in combat_items: + state.tunic_need_to_reset_combat_from_remove[self.player] = True + return change + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: if self.options.entrance_rando: hint_data.update({self.player: {}}) diff --git a/worlds/tunic/combat_logic.py b/worlds/tunic/combat_logic.py index 00a0c6681b..77f52c354d 100644 --- a/worlds/tunic/combat_logic.py +++ b/worlds/tunic/combat_logic.py @@ -1,6 +1,8 @@ from typing import Dict, List, NamedTuple, Tuple, Optional +from enum import IntEnum from BaseClasses import CollectionState from .rules import has_sword, has_melee +from worlds.AutoWorld import LogicMixin # the vanilla stats you are expected to have to get through an area, based on where they are in vanilla @@ -45,7 +47,68 @@ area_data: Dict[str, AreaStats] = { } -def has_combat_reqs(area_name: str, state: CollectionState, player: int, alt_data: Optional[AreaStats] = None) -> bool: +# these are used for caching which areas can currently be reached in state +boss_areas: List[str] = [name for name, data in area_data.items() if data.is_boss and name != "Gauntlet"] +non_boss_areas: List[str] = [name for name, data in area_data.items() if not data.is_boss] + + +class CombatState(IntEnum): + unchecked = 0 + failed = 1 + succeeded = 2 + + +def has_combat_reqs(area_name: str, state: CollectionState, player: int) -> bool: + # we're caching whether you've met the combat reqs before if the state didn't change first + # if the combat state is stale, mark each area's combat state as stale + if state.tunic_need_to_reset_combat_from_collect[player]: + state.tunic_need_to_reset_combat_from_collect[player] = 0 + for name in area_data.keys(): + if state.tunic_area_combat_state[player][name] == CombatState.failed: + state.tunic_area_combat_state[player][name] = CombatState.unchecked + + if state.tunic_need_to_reset_combat_from_remove[player]: + state.tunic_need_to_reset_combat_from_remove[player] = 0 + for name in area_data.keys(): + if state.tunic_area_combat_state[player][name] == CombatState.succeeded: + state.tunic_area_combat_state[player][name] = CombatState.unchecked + + if state.tunic_area_combat_state[player][area_name] > CombatState.unchecked: + return state.tunic_area_combat_state[player][area_name] == CombatState.succeeded + + met_combat_reqs = check_combat_reqs(area_name, state, player) + + # we want to skip the "none area" since we don't record its results + if area_name not in area_data.keys(): + return met_combat_reqs + + # loop through the lists and set the easier/harder area states accordingly + if area_name in boss_areas: + area_list = boss_areas + elif area_name in non_boss_areas: + area_list = non_boss_areas + else: + area_list = [area_name] + + if met_combat_reqs: + # set the state as true for each area until you get to the area we're looking at + for name in area_list: + state.tunic_area_combat_state[player][name] = CombatState.succeeded + if name == area_name: + break + else: + # set the state as false for the area we're looking at and each area after that + reached_name = False + for name in area_list: + if name == area_name: + reached_name = True + if reached_name: + state.tunic_area_combat_state[player][name] = CombatState.failed + + return met_combat_reqs + + +def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_data: Optional[AreaStats] = None) -> bool: data = alt_data or area_data[area_name] extra_att_needed = 0 extra_def_needed = 0 @@ -108,7 +171,7 @@ def has_combat_reqs(area_name: str, state: CollectionState, player: int, alt_dat more_modified_stats = AreaStats(data.att_level - 16, data.def_level, data.potion_level, data.hp_level, data.sp_level, data.mp_level + 4, data.potion_count, equip_list) - if has_combat_reqs("none", state, player, more_modified_stats): + if check_combat_reqs("none", state, player, more_modified_stats): return True # and we need to check if you would have the required stats if you didn't have magic @@ -116,8 +179,9 @@ def has_combat_reqs(area_name: str, state: CollectionState, player: int, alt_dat more_modified_stats = AreaStats(data.att_level + 2, data.def_level + 2, data.potion_level, data.hp_level, data.sp_level, data.mp_level - 16, data.potion_count, equip_list) - if has_combat_reqs("none", state, player, more_modified_stats): + if check_combat_reqs("none", state, player, more_modified_stats): return True + return False elif stick_bool and "Stick" in data.equipment and "Magic" in data.equipment: # we need to check if you would have the required stats if you didn't have the stick @@ -125,8 +189,9 @@ def has_combat_reqs(area_name: str, state: CollectionState, player: int, alt_dat more_modified_stats = AreaStats(data.att_level - 16, data.def_level, data.potion_level, data.hp_level, data.sp_level, data.mp_level + 4, data.potion_count, equip_list) - if has_combat_reqs("none", state, player, more_modified_stats): + if check_combat_reqs("none", state, player, more_modified_stats): return True + return False else: return False return True @@ -163,12 +228,11 @@ def has_required_stats(data: AreaStats, state: CollectionState, player: int) -> free_def = player_def - def_offerings free_sp = player_sp - sp_offerings paid_stats = data.def_level + data.sp_level - free_def - free_sp - def_to_buy = 0 sp_to_buy = 0 if paid_stats <= 0: # if you don't have to pay for any stats, you don't need money for these upgrades - pass + def_to_buy = 0 elif paid_stats <= def_offerings: # get the amount needed to buy these def offerings def_to_buy = paid_stats @@ -265,31 +329,31 @@ def has_required_stats(data: AreaStats, state: CollectionState, player: int) -> # returns a tuple of your max attack level, the number of attack offerings def get_att_level(state: CollectionState, player: int) -> Tuple[int, int]: - att_offering_count = state.count("ATT Offering", player) + att_offerings = state.count("ATT Offering", player) att_upgrades = state.count("Hero Relic - ATT", player) sword_level = state.count("Sword Upgrade", player) if sword_level >= 3: att_upgrades += min(2, sword_level - 2) # attack falls off, can just cap it at 8 for simplicity - return min(8, 1 + att_offering_count + att_upgrades), att_offering_count + return min(8, 1 + att_offerings + att_upgrades), att_offerings # returns a tuple of your max defense level, the number of defense offerings def get_def_level(state: CollectionState, player: int) -> Tuple[int, int]: - def_offering_count = state.count("DEF Offering", player) + def_offerings = state.count("DEF Offering", player) # defense falls off, can just cap it at 8 for simplicity - return (min(8, 1 + def_offering_count + return (min(8, 1 + def_offerings + state.count_from_list({"Hero Relic - DEF", "Secret Legend", "Phonomath"}, player)), - def_offering_count) + def_offerings) # returns a tuple of your max potion level, the number of potion offerings def get_potion_level(state: CollectionState, player: int) -> Tuple[int, int]: - potion_offering_count = min(2, state.count("Potion Offering", player)) + potion_offerings = min(2, state.count("Potion Offering", player)) # your third potion upgrade (from offerings) costs 1,000 money, reasonable to assume you won't do that - return (1 + potion_offering_count + return (1 + potion_offerings + state.count_from_list({"Hero Relic - POTION", "Just Some Pals", "Spring Falls", "Back To Work"}, player), - potion_offering_count) + potion_offerings) # returns a tuple of your max hp level, the number of hp offerings @@ -341,3 +405,13 @@ def get_money_count(state: CollectionState, player: int) -> int: money += money_per_break money_per_break = min(512, money_per_break * 2) return money + + +class TunicState(LogicMixin): + # the per-player need to reset the combat state when collecting a combat item + tunic_need_to_reset_combat_from_collect: Dict[int, bool] = {} + # the per-player need to reset the combat state when removing a combat item + tunic_need_to_reset_combat_from_remove: Dict[int, bool] = {} + # the per-player, per-area state of combat checking -- unchecked, failed, or succeeded + tunic_area_combat_state: Dict[int, Dict[str, int]] = {} + diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index bb54336654..55ff0fea9e 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -35,497 +35,497 @@ class Portal(NamedTuple): portal_mapping: List[Portal] = [ Portal(name="Stick House Entrance", region="Overworld", - destination="Sword Cave", tag="_", direction=Direction.north), + destination="Sword Cave", tag="_"), Portal(name="Windmill Entrance", region="Overworld", - destination="Windmill", tag="_", direction=Direction.north), + destination="Windmill", tag="_"), Portal(name="Well Ladder Entrance", region="Overworld Well Ladder", - destination="Sewer", tag="_entrance", direction=Direction.ladder_down), + destination="Sewer", tag="_entrance"), Portal(name="Entrance to Well from Well Rail", region="Overworld Well to Furnace Rail", - destination="Sewer", tag="_west_aqueduct", direction=Direction.north), + destination="Sewer", tag="_west_aqueduct"), Portal(name="Old House Door Entrance", region="Overworld Old House Door", - destination="Overworld Interiors", tag="_house", direction=Direction.east), + destination="Overworld Interiors", tag="_house"), Portal(name="Old House Waterfall Entrance", region="Overworld", - destination="Overworld Interiors", tag="_under_checkpoint", direction=Direction.east), + destination="Overworld Interiors", tag="_under_checkpoint"), Portal(name="Entrance to Furnace from Well Rail", region="Overworld Well to Furnace Rail", - destination="Furnace", tag="_gyro_upper_north", direction=Direction.south), + destination="Furnace", tag="_gyro_upper_north"), Portal(name="Entrance to Furnace under Windmill", region="Overworld", - destination="Furnace", tag="_gyro_upper_east", direction=Direction.west), + destination="Furnace", tag="_gyro_upper_east"), Portal(name="Entrance to Furnace near West Garden", region="Overworld to West Garden from Furnace", - destination="Furnace", tag="_gyro_west", direction=Direction.east), + destination="Furnace", tag="_gyro_west"), Portal(name="Entrance to Furnace from Beach", region="Overworld Tunnel Turret", - destination="Furnace", tag="_gyro_lower", direction=Direction.north), + destination="Furnace", tag="_gyro_lower"), Portal(name="Caustic Light Cave Entrance", region="Overworld Swamp Lower Entry", - destination="Overworld Cave", tag="_", direction=Direction.north), + destination="Overworld Cave", tag="_"), Portal(name="Swamp Upper Entrance", region="Overworld Swamp Upper Entry", - destination="Swamp Redux 2", tag="_wall", direction=Direction.south), + destination="Swamp Redux 2", tag="_wall"), Portal(name="Swamp Lower Entrance", region="Overworld Swamp Lower Entry", - destination="Swamp Redux 2", tag="_conduit", direction=Direction.south), + destination="Swamp Redux 2", tag="_conduit"), Portal(name="Ruined Passage Not-Door Entrance", region="After Ruined Passage", - destination="Ruins Passage", tag="_east", direction=Direction.north), + destination="Ruins Passage", tag="_east"), Portal(name="Ruined Passage Door Entrance", region="Overworld Ruined Passage Door", - destination="Ruins Passage", tag="_west", direction=Direction.east), + destination="Ruins Passage", tag="_west"), Portal(name="Atoll Upper Entrance", region="Overworld to Atoll Upper", - destination="Atoll Redux", tag="_upper", direction=Direction.south), + destination="Atoll Redux", tag="_upper"), Portal(name="Atoll Lower Entrance", region="Overworld Beach", - destination="Atoll Redux", tag="_lower", direction=Direction.south), + destination="Atoll Redux", tag="_lower"), Portal(name="Special Shop Entrance", region="Overworld Special Shop Entry", - destination="ShopSpecial", tag="_", direction=Direction.east), + destination="ShopSpecial", tag="_"), Portal(name="Maze Cave Entrance", region="Overworld Beach", - destination="Maze Room", tag="_", direction=Direction.north), + destination="Maze Room", tag="_"), Portal(name="West Garden Entrance near Belltower", region="Overworld to West Garden Upper", - destination="Archipelagos Redux", tag="_upper", direction=Direction.west), + destination="Archipelagos Redux", tag="_upper"), Portal(name="West Garden Entrance from Furnace", region="Overworld to West Garden from Furnace", - destination="Archipelagos Redux", tag="_lower", direction=Direction.west), + destination="Archipelagos Redux", tag="_lower"), Portal(name="West Garden Laurels Entrance", region="Overworld West Garden Laurels Entry", - destination="Archipelagos Redux", tag="_lowest", direction=Direction.west), + destination="Archipelagos Redux", tag="_lowest"), Portal(name="Temple Door Entrance", region="Overworld Temple Door", - destination="Temple", tag="_main", direction=Direction.north), + destination="Temple", tag="_main"), Portal(name="Temple Rafters Entrance", region="Overworld after Temple Rafters", - destination="Temple", tag="_rafters", direction=Direction.east), + destination="Temple", tag="_rafters"), Portal(name="Ruined Shop Entrance", region="Overworld", - destination="Ruined Shop", tag="_", direction=Direction.east), + destination="Ruined Shop", tag="_"), Portal(name="Patrol Cave Entrance", region="Overworld at Patrol Cave", - destination="PatrolCave", tag="_", direction=Direction.north), + destination="PatrolCave", tag="_"), Portal(name="Hourglass Cave Entrance", region="Overworld Beach", - destination="Town Basement", tag="_beach", direction=Direction.north), + destination="Town Basement", tag="_beach"), Portal(name="Changing Room Entrance", region="Overworld", - destination="Changing Room", tag="_", direction=Direction.south), + destination="Changing Room", tag="_"), Portal(name="Cube Cave Entrance", region="Overworld", - destination="CubeRoom", tag="_", direction=Direction.north), + destination="CubeRoom", tag="_"), Portal(name="Stairs from Overworld to Mountain", region="Upper Overworld", - destination="Mountain", tag="_", direction=Direction.north), + destination="Mountain", tag="_"), Portal(name="Overworld to Fortress", region="East Overworld", - destination="Fortress Courtyard", tag="_", direction=Direction.east), + destination="Fortress Courtyard", tag="_"), Portal(name="Fountain HC Door Entrance", region="Overworld Fountain Cross Door", - destination="Town_FiligreeRoom", tag="_", direction=Direction.north), + destination="Town_FiligreeRoom", tag="_"), Portal(name="Southeast HC Door Entrance", region="Overworld Southeast Cross Door", - destination="EastFiligreeCache", tag="_", direction=Direction.north), + destination="EastFiligreeCache", tag="_"), Portal(name="Overworld to Quarry Connector", region="Overworld Quarry Entry", - destination="Darkwoods Tunnel", tag="_", direction=Direction.north), + destination="Darkwoods Tunnel", tag="_"), Portal(name="Dark Tomb Main Entrance", region="Overworld", - destination="Crypt Redux", tag="_", direction=Direction.north), + destination="Crypt Redux", tag="_"), Portal(name="Overworld to Forest Belltower", region="East Overworld", - destination="Forest Belltower", tag="_", direction=Direction.east), + destination="Forest Belltower", tag="_"), Portal(name="Town to Far Shore", region="Overworld Town Portal", - destination="Transit", tag="_teleporter_town", direction=Direction.floor), + destination="Transit", tag="_teleporter_town"), Portal(name="Spawn to Far Shore", region="Overworld Spawn Portal", - destination="Transit", tag="_teleporter_starting island", direction=Direction.floor), + destination="Transit", tag="_teleporter_starting island"), Portal(name="Secret Gathering Place Entrance", region="Overworld", - destination="Waterfall", tag="_", direction=Direction.north), + destination="Waterfall", tag="_"), Portal(name="Secret Gathering Place Exit", region="Secret Gathering Place", - destination="Overworld Redux", tag="_", direction=Direction.south), + destination="Overworld Redux", tag="_"), Portal(name="Windmill Exit", region="Windmill", - destination="Overworld Redux", tag="_", direction=Direction.south), + destination="Overworld Redux", tag="_"), Portal(name="Windmill Shop", region="Windmill", - destination="Shop", tag="_", direction=Direction.north), + destination="Shop", tag="_"), Portal(name="Old House Door Exit", region="Old House Front", - destination="Overworld Redux", tag="_house", direction=Direction.west), + destination="Overworld Redux", tag="_house"), Portal(name="Old House to Glyph Tower", region="Old House Front", - destination="g_elements", tag="_", direction=Direction.south), # portal drops you on north side + destination="g_elements", tag="_"), Portal(name="Old House Waterfall Exit", region="Old House Back", - destination="Overworld Redux", tag="_under_checkpoint", direction=Direction.west), + destination="Overworld Redux", tag="_under_checkpoint"), Portal(name="Glyph Tower Exit", region="Relic Tower", - destination="Overworld Interiors", tag="_", direction=Direction.north), + destination="Overworld Interiors", tag="_"), Portal(name="Changing Room Exit", region="Changing Room", - destination="Overworld Redux", tag="_", direction=Direction.north), + destination="Overworld Redux", tag="_"), Portal(name="Fountain HC Room Exit", region="Fountain Cross Room", - destination="Overworld Redux", tag="_", direction=Direction.south), + destination="Overworld Redux", tag="_"), Portal(name="Cube Cave Exit", region="Cube Cave", - destination="Overworld Redux", tag="_", direction=Direction.south), + destination="Overworld Redux", tag="_"), Portal(name="Guard Patrol Cave Exit", region="Patrol Cave", - destination="Overworld Redux", tag="_", direction=Direction.south), + destination="Overworld Redux", tag="_"), Portal(name="Ruined Shop Exit", region="Ruined Shop", - destination="Overworld Redux", tag="_", direction=Direction.west), + destination="Overworld Redux", tag="_"), Portal(name="Furnace Exit towards Well", region="Furnace Fuse", - destination="Overworld Redux", tag="_gyro_upper_north", direction=Direction.north), + destination="Overworld Redux", tag="_gyro_upper_north"), Portal(name="Furnace Exit to Dark Tomb", region="Furnace Walking Path", - destination="Crypt Redux", tag="_", direction=Direction.east), + destination="Crypt Redux", tag="_"), Portal(name="Furnace Exit towards West Garden", region="Furnace Walking Path", - destination="Overworld Redux", tag="_gyro_west", direction=Direction.west), + destination="Overworld Redux", tag="_gyro_west"), Portal(name="Furnace Exit to Beach", region="Furnace Ladder Area", - destination="Overworld Redux", tag="_gyro_lower", direction=Direction.south), + destination="Overworld Redux", tag="_gyro_lower"), Portal(name="Furnace Exit under Windmill", region="Furnace Ladder Area", - destination="Overworld Redux", tag="_gyro_upper_east", direction=Direction.east), + destination="Overworld Redux", tag="_gyro_upper_east"), Portal(name="Stick House Exit", region="Stick House", - destination="Overworld Redux", tag="_", direction=Direction.south), + destination="Overworld Redux", tag="_"), Portal(name="Ruined Passage Not-Door Exit", region="Ruined Passage", - destination="Overworld Redux", tag="_east", direction=Direction.south), + destination="Overworld Redux", tag="_east"), Portal(name="Ruined Passage Door Exit", region="Ruined Passage", - destination="Overworld Redux", tag="_west", direction=Direction.west), + destination="Overworld Redux", tag="_west"), Portal(name="Southeast HC Room Exit", region="Southeast Cross Room", - destination="Overworld Redux", tag="_", direction=Direction.south), + destination="Overworld Redux", tag="_"), Portal(name="Caustic Light Cave Exit", region="Caustic Light Cave", - destination="Overworld Redux", tag="_", direction=Direction.south), + destination="Overworld Redux", tag="_"), Portal(name="Maze Cave Exit", region="Maze Cave", - destination="Overworld Redux", tag="_", direction=Direction.south), + destination="Overworld Redux", tag="_"), Portal(name="Hourglass Cave Exit", region="Hourglass Cave", - destination="Overworld Redux", tag="_beach", direction=Direction.south), + destination="Overworld Redux", tag="_beach"), Portal(name="Special Shop Exit", region="Special Shop", - destination="Overworld Redux", tag="_", direction=Direction.south), + destination="Overworld Redux", tag="_"), Portal(name="Temple Rafters Exit", region="Sealed Temple Rafters", - destination="Overworld Redux", tag="_rafters", direction=Direction.west), + destination="Overworld Redux", tag="_rafters"), Portal(name="Temple Door Exit", region="Sealed Temple", - destination="Overworld Redux", tag="_main", direction=Direction.south), + destination="Overworld Redux", tag="_main"), Portal(name="Well Ladder Exit", region="Beneath the Well Ladder Exit", - destination="Overworld Redux", tag="_entrance", direction=Direction.ladder_up), + destination="Overworld Redux", tag="_entrance"), Portal(name="Well to Well Boss", region="Beneath the Well Back", - destination="Sewer_Boss", tag="_", direction=Direction.east), + destination="Sewer_Boss", tag="_"), Portal(name="Well Exit towards Furnace", region="Beneath the Well Back", - destination="Overworld Redux", tag="_west_aqueduct", direction=Direction.south), + destination="Overworld Redux", tag="_west_aqueduct"), Portal(name="Well Boss to Well", region="Well Boss", - destination="Sewer", tag="_", direction=Direction.west), + destination="Sewer", tag="_"), Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint", - destination="Crypt Redux", tag="_", direction=Direction.ladder_up), + destination="Crypt Redux", tag="_"), Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point", - destination="Overworld Redux", tag="_", direction=Direction.south), + destination="Overworld Redux", tag="_"), Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit", - destination="Furnace", tag="_", direction=Direction.west), + destination="Furnace", tag="_"), Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point", - destination="Sewer_Boss", tag="_", direction=Direction.ladder_down), + destination="Sewer_Boss", tag="_"), Portal(name="West Garden Exit near Hero's Grave", region="West Garden before Terry", - destination="Overworld Redux", tag="_lower", direction=Direction.east), + destination="Overworld Redux", tag="_lower"), Portal(name="West Garden to Magic Dagger House", region="West Garden at Dagger House", - destination="archipelagos_house", tag="_", direction=Direction.east), + destination="archipelagos_house", tag="_"), Portal(name="West Garden Exit after Boss", region="West Garden after Boss", - destination="Overworld Redux", tag="_upper", direction=Direction.east), + destination="Overworld Redux", tag="_upper"), Portal(name="West Garden Shop", region="West Garden before Terry", - destination="Shop", tag="_", direction=Direction.east), + destination="Shop", tag="_"), Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit Region", - destination="Overworld Redux", tag="_lowest", direction=Direction.east), + destination="Overworld Redux", tag="_lowest"), Portal(name="West Garden Hero's Grave", region="West Garden Hero's Grave Region", - destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor), + destination="RelicVoid", tag="_teleporter_relic plinth"), Portal(name="West Garden to Far Shore", region="West Garden Portal", - destination="Transit", tag="_teleporter_archipelagos_teleporter", direction=Direction.floor), + destination="Transit", tag="_teleporter_archipelagos_teleporter"), Portal(name="Magic Dagger House Exit", region="Magic Dagger House", - destination="Archipelagos Redux", tag="_", direction=Direction.west), + destination="Archipelagos Redux", tag="_"), Portal(name="Atoll Upper Exit", region="Ruined Atoll", - destination="Overworld Redux", tag="_upper", direction=Direction.north), + destination="Overworld Redux", tag="_upper"), Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area", - destination="Overworld Redux", tag="_lower", direction=Direction.north), + destination="Overworld Redux", tag="_lower"), Portal(name="Atoll Shop", region="Ruined Atoll", - destination="Shop", tag="_", direction=Direction.north), + destination="Shop", tag="_"), Portal(name="Atoll to Far Shore", region="Ruined Atoll Portal", - destination="Transit", tag="_teleporter_atoll", direction=Direction.floor), + destination="Transit", tag="_teleporter_atoll"), Portal(name="Atoll Statue Teleporter", region="Ruined Atoll Statue", - destination="Library Exterior", tag="_", direction=Direction.floor), + destination="Library Exterior", tag="_"), Portal(name="Frog Stairs Eye Entrance", region="Ruined Atoll Frog Eye", - destination="Frog Stairs", tag="_eye", direction=Direction.south), # camera rotates, it's fine + destination="Frog Stairs", tag="_eye"), Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth", - destination="Frog Stairs", tag="_mouth", direction=Direction.east), + destination="Frog Stairs", tag="_mouth"), Portal(name="Frog Stairs Eye Exit", region="Frog Stairs Eye Exit", - destination="Atoll Redux", tag="_eye", direction=Direction.north), + destination="Atoll Redux", tag="_eye"), Portal(name="Frog Stairs Mouth Exit", region="Frog Stairs Upper", - destination="Atoll Redux", tag="_mouth", direction=Direction.west), + destination="Atoll Redux", tag="_mouth"), Portal(name="Frog Stairs to Frog's Domain's Entrance", region="Frog Stairs to Frog's Domain", - destination="frog cave main", tag="_Entrance", direction=Direction.ladder_down), + destination="frog cave main", tag="_Entrance"), Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog Stairs Lower", - destination="frog cave main", tag="_Exit", direction=Direction.east), + destination="frog cave main", tag="_Exit"), Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain Entry", - destination="Frog Stairs", tag="_Entrance", direction=Direction.ladder_up), + destination="Frog Stairs", tag="_Entrance"), Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back", - destination="Frog Stairs", tag="_Exit", direction=Direction.west), + destination="Frog Stairs", tag="_Exit"), Portal(name="Library Exterior Tree", region="Library Exterior Tree Region", - destination="Atoll Redux", tag="_", direction=Direction.floor), + destination="Atoll Redux", tag="_"), Portal(name="Library Exterior Ladder", region="Library Exterior Ladder Region", - destination="Library Hall", tag="_", direction=Direction.west), # camera rotates + destination="Library Hall", tag="_"), Portal(name="Library Hall Bookshelf Exit", region="Library Hall Bookshelf", - destination="Library Exterior", tag="_", direction=Direction.east), + destination="Library Exterior", tag="_"), Portal(name="Library Hero's Grave", region="Library Hero's Grave Region", - destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor), + destination="RelicVoid", tag="_teleporter_relic plinth"), Portal(name="Library Hall to Rotunda", region="Library Hall to Rotunda", - destination="Library Rotunda", tag="_", direction=Direction.ladder_up), + destination="Library Rotunda", tag="_"), Portal(name="Library Rotunda Lower Exit", region="Library Rotunda to Hall", - destination="Library Hall", tag="_", direction=Direction.ladder_down), + destination="Library Hall", tag="_"), Portal(name="Library Rotunda Upper Exit", region="Library Rotunda to Lab", - destination="Library Lab", tag="_", direction=Direction.ladder_up), + destination="Library Lab", tag="_"), Portal(name="Library Lab to Rotunda", region="Library Lab Lower", - destination="Library Rotunda", tag="_", direction=Direction.ladder_down), + destination="Library Rotunda", tag="_"), Portal(name="Library to Far Shore", region="Library Portal", - destination="Transit", tag="_teleporter_library teleporter", direction=Direction.floor), + destination="Transit", tag="_teleporter_library teleporter"), Portal(name="Library Lab to Librarian Arena", region="Library Lab to Librarian", - destination="Library Arena", tag="_", direction=Direction.ladder_up), + destination="Library Arena", tag="_"), Portal(name="Librarian Arena Exit", region="Library Arena", - destination="Library Lab", tag="_", direction=Direction.ladder_down), + destination="Library Lab", tag="_"), Portal(name="Forest to Belltower", region="East Forest", - destination="Forest Belltower", tag="_", direction=Direction.north), + destination="Forest Belltower", tag="_"), Portal(name="Forest Guard House 1 Lower Entrance", region="East Forest", - destination="East Forest Redux Laddercave", tag="_lower", direction=Direction.north), + destination="East Forest Redux Laddercave", tag="_lower"), Portal(name="Forest Guard House 1 Gate Entrance", region="East Forest", - destination="East Forest Redux Laddercave", tag="_gate", direction=Direction.north), + destination="East Forest Redux Laddercave", tag="_gate"), Portal(name="Forest Dance Fox Outside Doorway", region="East Forest Dance Fox Spot", - destination="East Forest Redux Laddercave", tag="_upper", direction=Direction.east), + destination="East Forest Redux Laddercave", tag="_upper"), Portal(name="Forest to Far Shore", region="East Forest Portal", - destination="Transit", tag="_teleporter_forest teleporter", direction=Direction.floor), + destination="Transit", tag="_teleporter_forest teleporter"), Portal(name="Forest Guard House 2 Lower Entrance", region="Lower Forest", - destination="East Forest Redux Interior", tag="_lower", direction=Direction.north), + destination="East Forest Redux Interior", tag="_lower"), Portal(name="Forest Guard House 2 Upper Entrance", region="East Forest", - destination="East Forest Redux Interior", tag="_upper", direction=Direction.east), + destination="East Forest Redux Interior", tag="_upper"), Portal(name="Forest Grave Path Lower Entrance", region="East Forest", - destination="Sword Access", tag="_lower", direction=Direction.east), + destination="Sword Access", tag="_lower"), Portal(name="Forest Grave Path Upper Entrance", region="East Forest", - destination="Sword Access", tag="_upper", direction=Direction.east), + destination="Sword Access", tag="_upper"), Portal(name="Guard House 1 Dance Fox Exit", region="Guard House 1 West", - destination="East Forest Redux", tag="_upper", direction=Direction.west), + destination="East Forest Redux", tag="_upper"), Portal(name="Guard House 1 Lower Exit", region="Guard House 1 West", - destination="East Forest Redux", tag="_lower", direction=Direction.south), + destination="East Forest Redux", tag="_lower"), Portal(name="Guard House 1 Upper Forest Exit", region="Guard House 1 East", - destination="East Forest Redux", tag="_gate", direction=Direction.south), + destination="East Forest Redux", tag="_gate"), Portal(name="Guard House 1 to Guard Captain Room", region="Guard House 1 East", - destination="Forest Boss Room", tag="_", direction=Direction.north), + destination="Forest Boss Room", tag="_"), Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper", - destination="East Forest Redux", tag="_upper", direction=Direction.west), + destination="East Forest Redux", tag="_upper"), Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main", - destination="East Forest Redux", tag="_lower", direction=Direction.west), + destination="East Forest Redux", tag="_lower"), Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave", - destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor), + destination="RelicVoid", tag="_teleporter_relic plinth"), Portal(name="Guard House 2 Lower Exit", region="Guard House 2 Lower", - destination="East Forest Redux", tag="_lower", direction=Direction.south), + destination="East Forest Redux", tag="_lower"), Portal(name="Guard House 2 Upper Exit", region="Guard House 2 Upper", - destination="East Forest Redux", tag="_upper", direction=Direction.west), + destination="East Forest Redux", tag="_upper"), Portal(name="Guard Captain Room Non-Gate Exit", region="Forest Boss Room", - destination="East Forest Redux Laddercave", tag="_", direction=Direction.south), + destination="East Forest Redux Laddercave", tag="_"), Portal(name="Guard Captain Room Gate Exit", region="Forest Boss Room", - destination="Forest Belltower", tag="_", direction=Direction.north), + destination="Forest Belltower", tag="_"), Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main", - destination="Fortress Courtyard", tag="_", direction=Direction.north), + destination="Fortress Courtyard", tag="_"), Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower", - destination="East Forest Redux", tag="_", direction=Direction.south), + destination="East Forest Redux", tag="_"), Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main", - destination="Overworld Redux", tag="_", direction=Direction.west), + destination="Overworld Redux", tag="_"), Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper", - destination="Forest Boss Room", tag="_", direction=Direction.south), + destination="Forest Boss Room", tag="_"), Portal(name="Fortress Courtyard to Fortress Grave Path Lower", region="Fortress Courtyard", - destination="Fortress Reliquary", tag="_Lower", direction=Direction.east), + destination="Fortress Reliquary", tag="_Lower"), Portal(name="Fortress Courtyard to Fortress Grave Path Upper", region="Fortress Courtyard Upper", - destination="Fortress Reliquary", tag="_Upper", direction=Direction.east), + destination="Fortress Reliquary", tag="_Upper"), Portal(name="Fortress Courtyard to Fortress Interior", region="Fortress Courtyard", - destination="Fortress Main", tag="_Big Door", direction=Direction.north), + destination="Fortress Main", tag="_Big Door"), Portal(name="Fortress Courtyard to East Fortress", region="Fortress Courtyard Upper", - destination="Fortress East", tag="_", direction=Direction.north), + destination="Fortress East", tag="_"), Portal(name="Fortress Courtyard to Beneath the Vault", region="Beneath the Vault Entry", - destination="Fortress Basement", tag="_", direction=Direction.ladder_down), + destination="Fortress Basement", tag="_"), Portal(name="Fortress Courtyard to Forest Belltower", region="Fortress Exterior from East Forest", - destination="Forest Belltower", tag="_", direction=Direction.south), + destination="Forest Belltower", tag="_"), Portal(name="Fortress Courtyard to Overworld", region="Fortress Exterior from Overworld", - destination="Overworld Redux", tag="_", direction=Direction.west), + destination="Overworld Redux", tag="_"), Portal(name="Fortress Courtyard Shop", region="Fortress Exterior near cave", - destination="Shop", tag="_", direction=Direction.north), + destination="Shop", tag="_"), Portal(name="Beneath the Vault to Fortress Interior", region="Beneath the Vault Back", - destination="Fortress Main", tag="_", direction=Direction.east), + destination="Fortress Main", tag="_"), Portal(name="Beneath the Vault to Fortress Courtyard", region="Beneath the Vault Ladder Exit", - destination="Fortress Courtyard", tag="_", direction=Direction.ladder_up), + destination="Fortress Courtyard", tag="_"), Portal(name="Fortress Interior Main Exit", region="Eastern Vault Fortress", - destination="Fortress Courtyard", tag="_Big Door", direction=Direction.south), + destination="Fortress Courtyard", tag="_Big Door"), Portal(name="Fortress Interior to Beneath the Earth", region="Eastern Vault Fortress", - destination="Fortress Basement", tag="_", direction=Direction.west), + destination="Fortress Basement", tag="_"), Portal(name="Fortress Interior to Siege Engine Arena", region="Eastern Vault Fortress Gold Door", - destination="Fortress Arena", tag="_", direction=Direction.north), + destination="Fortress Arena", tag="_"), Portal(name="Fortress Interior Shop", region="Eastern Vault Fortress", - destination="Shop", tag="_", direction=Direction.north), + destination="Shop", tag="_"), Portal(name="Fortress Interior to East Fortress Upper", region="Eastern Vault Fortress", - destination="Fortress East", tag="_upper", direction=Direction.east), + destination="Fortress East", tag="_upper"), Portal(name="Fortress Interior to East Fortress Lower", region="Eastern Vault Fortress", - destination="Fortress East", tag="_lower", direction=Direction.east), + destination="Fortress East", tag="_lower"), Portal(name="East Fortress to Interior Lower", region="Fortress East Shortcut Lower", - destination="Fortress Main", tag="_lower", direction=Direction.west), + destination="Fortress Main", tag="_lower"), Portal(name="East Fortress to Courtyard", region="Fortress East Shortcut Upper", - destination="Fortress Courtyard", tag="_", direction=Direction.south), + destination="Fortress Courtyard", tag="_"), Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper", - destination="Fortress Main", tag="_upper", direction=Direction.west), + destination="Fortress Main", tag="_upper"), Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path Entry", - destination="Fortress Courtyard", tag="_Lower", direction=Direction.west), + destination="Fortress Courtyard", tag="_Lower"), Portal(name="Fortress Hero's Grave", region="Fortress Hero's Grave Region", - destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor), + destination="RelicVoid", tag="_teleporter_relic plinth"), Portal(name="Fortress Grave Path Upper Exit", region="Fortress Grave Path Upper", - destination="Fortress Courtyard", tag="_Upper", direction=Direction.west), + destination="Fortress Courtyard", tag="_Upper"), Portal(name="Fortress Grave Path Dusty Entrance", region="Fortress Grave Path Dusty Entrance Region", - destination="Dusty", tag="_", direction=Direction.north), + destination="Dusty", tag="_"), Portal(name="Dusty Exit", region="Fortress Leaf Piles", - destination="Fortress Reliquary", tag="_", direction=Direction.south), + destination="Fortress Reliquary", tag="_"), Portal(name="Siege Engine Arena to Fortress", region="Fortress Arena", - destination="Fortress Main", tag="_", direction=Direction.south), + destination="Fortress Main", tag="_"), Portal(name="Fortress to Far Shore", region="Fortress Arena Portal", - destination="Transit", tag="_teleporter_spidertank", direction=Direction.floor), + destination="Transit", tag="_teleporter_spidertank"), Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs", - destination="Mountaintop", tag="_", direction=Direction.north), + destination="Mountaintop", tag="_"), Portal(name="Mountain to Quarry", region="Lower Mountain", - destination="Quarry Redux", tag="_", direction=Direction.south), # connecting is north + destination="Quarry Redux", tag="_"), Portal(name="Mountain to Overworld", region="Lower Mountain", - destination="Overworld Redux", tag="_", direction=Direction.south), + destination="Overworld Redux", tag="_"), Portal(name="Top of the Mountain Exit", region="Top of the Mountain", - destination="Mountain", tag="_", direction=Direction.south), + destination="Mountain", tag="_"), Portal(name="Quarry Connector to Overworld", region="Quarry Connector", - destination="Overworld Redux", tag="_", direction=Direction.south), + destination="Overworld Redux", tag="_"), Portal(name="Quarry Connector to Quarry", region="Quarry Connector", - destination="Quarry Redux", tag="_", direction=Direction.north), # rotates, it's fine + destination="Quarry Redux", tag="_"), Portal(name="Quarry to Overworld Exit", region="Quarry Entry", - destination="Darkwoods Tunnel", tag="_", direction=Direction.south), # rotates, it's fine + destination="Darkwoods Tunnel", tag="_"), Portal(name="Quarry Shop", region="Quarry Entry", - destination="Shop", tag="_", direction=Direction.north), + destination="Shop", tag="_"), Portal(name="Quarry to Monastery Front", region="Quarry Monastery Entry", - destination="Monastery", tag="_front", direction=Direction.north), + destination="Monastery", tag="_front"), Portal(name="Quarry to Monastery Back", region="Monastery Rope", - destination="Monastery", tag="_back", direction=Direction.east), + destination="Monastery", tag="_back"), Portal(name="Quarry to Mountain", region="Quarry Back", - destination="Mountain", tag="_", direction=Direction.north), + destination="Mountain", tag="_"), Portal(name="Quarry to Ziggurat", region="Lower Quarry Zig Door", - destination="ziggurat2020_0", tag="_", direction=Direction.north), + destination="ziggurat2020_0", tag="_"), Portal(name="Quarry to Far Shore", region="Quarry Portal", - destination="Transit", tag="_teleporter_quarry teleporter", direction=Direction.floor), + destination="Transit", tag="_teleporter_quarry teleporter"), Portal(name="Monastery Rear Exit", region="Monastery Back", - destination="Quarry Redux", tag="_back", direction=Direction.west), + destination="Quarry Redux", tag="_back"), Portal(name="Monastery Front Exit", region="Monastery Front", - destination="Quarry Redux", tag="_front", direction=Direction.south), + destination="Quarry Redux", tag="_front"), Portal(name="Monastery Hero's Grave", region="Monastery Hero's Grave Region", - destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor), + destination="RelicVoid", tag="_teleporter_relic plinth"), Portal(name="Ziggurat Entry Hallway to Ziggurat Upper", region="Rooted Ziggurat Entry", - destination="ziggurat2020_1", tag="_", direction=Direction.north), + destination="ziggurat2020_1", tag="_"), Portal(name="Ziggurat Entry Hallway to Quarry", region="Rooted Ziggurat Entry", - destination="Quarry Redux", tag="_", direction=Direction.south), + destination="Quarry Redux", tag="_"), Portal(name="Ziggurat Upper to Ziggurat Entry Hallway", region="Rooted Ziggurat Upper Entry", - destination="ziggurat2020_0", tag="_", direction=Direction.south), + destination="ziggurat2020_0", tag="_"), Portal(name="Ziggurat Upper to Ziggurat Tower", region="Rooted Ziggurat Upper Back", - destination="ziggurat2020_2", tag="_", direction=Direction.north), # connecting is south + destination="ziggurat2020_2", tag="_"), Portal(name="Ziggurat Tower to Ziggurat Upper", region="Rooted Ziggurat Middle Top", - destination="ziggurat2020_1", tag="_", direction=Direction.south), + destination="ziggurat2020_1", tag="_"), Portal(name="Ziggurat Tower to Ziggurat Lower", region="Rooted Ziggurat Middle Bottom", - destination="ziggurat2020_3", tag="_", direction=Direction.south), + destination="ziggurat2020_3", tag="_"), Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Entry", - destination="ziggurat2020_2", tag="_", direction=Direction.north), + destination="ziggurat2020_2", tag="_"), Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance", - destination="ziggurat2020_FTRoom", tag="_", direction=Direction.north), + destination="ziggurat2020_FTRoom", tag="_"), # only if fixed shop is on, removed otherwise Portal(name="Ziggurat Lower Falling Entrance", region="Zig Skip Exit", - destination="ziggurat2020_1", tag="_zig2_skip", direction=Direction.none), + destination="ziggurat2020_1", tag="_zig2_skip"), Portal(name="Ziggurat Portal Room Exit", region="Rooted Ziggurat Portal Room Exit", - destination="ziggurat2020_3", tag="_", direction=Direction.south), + destination="ziggurat2020_3", tag="_"), Portal(name="Ziggurat to Far Shore", region="Rooted Ziggurat Portal", - destination="Transit", tag="_teleporter_ziggurat teleporter", direction=Direction.floor), + destination="Transit", tag="_teleporter_ziggurat teleporter"), Portal(name="Swamp Lower Exit", region="Swamp Front", - destination="Overworld Redux", tag="_conduit", direction=Direction.north), + destination="Overworld Redux", tag="_conduit"), Portal(name="Swamp to Cathedral Main Entrance", region="Swamp to Cathedral Main Entrance Region", - destination="Cathedral Redux", tag="_main", direction=Direction.north), + destination="Cathedral Redux", tag="_main"), Portal(name="Swamp to Cathedral Secret Legend Room Entrance", region="Swamp to Cathedral Treasure Room", - destination="Cathedral Redux", tag="_secret", direction=Direction.south), # feels a little weird + destination="Cathedral Redux", tag="_secret"), Portal(name="Swamp to Gauntlet", region="Back of Swamp", - destination="Cathedral Arena", tag="_", direction=Direction.north), + destination="Cathedral Arena", tag="_"), Portal(name="Swamp Shop", region="Swamp Front", - destination="Shop", tag="_", direction=Direction.north), + destination="Shop", tag="_"), Portal(name="Swamp Upper Exit", region="Back of Swamp Laurels Area", - destination="Overworld Redux", tag="_wall", direction=Direction.north), + destination="Overworld Redux", tag="_wall"), Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave Region", - destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor), + destination="RelicVoid", tag="_teleporter_relic plinth"), Portal(name="Cathedral Main Exit", region="Cathedral Entry", - destination="Swamp Redux 2", tag="_main", direction=Direction.south), + destination="Swamp Redux 2", tag="_main"), Portal(name="Cathedral Elevator", region="Cathedral to Gauntlet", - destination="Cathedral Arena", tag="_", direction=Direction.ladder_down), # elevators are ladders, right? + destination="Cathedral Arena", tag="_"), Portal(name="Cathedral Secret Legend Room Exit", region="Cathedral Secret Legend Room", - destination="Swamp Redux 2", tag="_secret", direction=Direction.north), + destination="Swamp Redux 2", tag="_secret"), Portal(name="Gauntlet to Swamp", region="Cathedral Gauntlet Exit", - destination="Swamp Redux 2", tag="_", direction=Direction.south), + destination="Swamp Redux 2", tag="_"), Portal(name="Gauntlet Elevator", region="Cathedral Gauntlet Checkpoint", - destination="Cathedral Redux", tag="_", direction=Direction.ladder_up), + destination="Cathedral Redux", tag="_"), Portal(name="Gauntlet Shop", region="Cathedral Gauntlet Checkpoint", - destination="Shop", tag="_", direction=Direction.east), + destination="Shop", tag="_"), Portal(name="Hero's Grave to Fortress", region="Hero Relic - Fortress", - destination="Fortress Reliquary", tag="_teleporter_relic plinth", direction=Direction.floor), + destination="Fortress Reliquary", tag="_teleporter_relic plinth"), Portal(name="Hero's Grave to Monastery", region="Hero Relic - Quarry", - destination="Monastery", tag="_teleporter_relic plinth", direction=Direction.floor), + destination="Monastery", tag="_teleporter_relic plinth"), Portal(name="Hero's Grave to West Garden", region="Hero Relic - West Garden", - destination="Archipelagos Redux", tag="_teleporter_relic plinth", direction=Direction.floor), + destination="Archipelagos Redux", tag="_teleporter_relic plinth"), Portal(name="Hero's Grave to East Forest", region="Hero Relic - East Forest", - destination="Sword Access", tag="_teleporter_relic plinth", direction=Direction.floor), + destination="Sword Access", tag="_teleporter_relic plinth"), Portal(name="Hero's Grave to Library", region="Hero Relic - Library", - destination="Library Hall", tag="_teleporter_relic plinth", direction=Direction.floor), + destination="Library Hall", tag="_teleporter_relic plinth"), Portal(name="Hero's Grave to Swamp", region="Hero Relic - Swamp", - destination="Swamp Redux 2", tag="_teleporter_relic plinth", direction=Direction.floor), + destination="Swamp Redux 2", tag="_teleporter_relic plinth"), Portal(name="Far Shore to West Garden", region="Far Shore to West Garden Region", - destination="Archipelagos Redux", tag="_teleporter_archipelagos_teleporter", direction=Direction.floor), + destination="Archipelagos Redux", tag="_teleporter_archipelagos_teleporter"), Portal(name="Far Shore to Library", region="Far Shore to Library Region", - destination="Library Lab", tag="_teleporter_library teleporter", direction=Direction.floor), + destination="Library Lab", tag="_teleporter_library teleporter"), Portal(name="Far Shore to Quarry", region="Far Shore to Quarry Region", - destination="Quarry Redux", tag="_teleporter_quarry teleporter", direction=Direction.floor), + destination="Quarry Redux", tag="_teleporter_quarry teleporter"), Portal(name="Far Shore to East Forest", region="Far Shore to East Forest Region", - destination="East Forest Redux", tag="_teleporter_forest teleporter", direction=Direction.floor), + destination="East Forest Redux", tag="_teleporter_forest teleporter"), Portal(name="Far Shore to Fortress", region="Far Shore to Fortress Region", - destination="Fortress Arena", tag="_teleporter_spidertank", direction=Direction.floor), + destination="Fortress Arena", tag="_teleporter_spidertank"), Portal(name="Far Shore to Atoll", region="Far Shore", - destination="Atoll Redux", tag="_teleporter_atoll", direction=Direction.floor), + destination="Atoll Redux", tag="_teleporter_atoll"), Portal(name="Far Shore to Ziggurat", region="Far Shore", - destination="ziggurat2020_FTRoom", tag="_teleporter_ziggurat teleporter", direction=Direction.floor), + destination="ziggurat2020_FTRoom", tag="_teleporter_ziggurat teleporter"), Portal(name="Far Shore to Heir", region="Far Shore", - destination="Spirit Arena", tag="_teleporter_spirit arena", direction=Direction.floor), + destination="Spirit Arena", tag="_teleporter_spirit arena"), Portal(name="Far Shore to Town", region="Far Shore", - destination="Overworld Redux", tag="_teleporter_town", direction=Direction.floor), + destination="Overworld Redux", tag="_teleporter_town"), Portal(name="Far Shore to Spawn", region="Far Shore to Spawn Region", - destination="Overworld Redux", tag="_teleporter_starting island", direction=Direction.floor), + destination="Overworld Redux", tag="_teleporter_starting island"), Portal(name="Heir Arena Exit", region="Spirit Arena", - destination="Transit", tag="_teleporter_spirit arena", direction=Direction.floor), + destination="Transit", tag="_teleporter_spirit arena"), Portal(name="Purgatory Bottom Exit", region="Purgatory", - destination="Purgatory", tag="_bottom", direction=Direction.south), + destination="Purgatory", tag="_bottom"), Portal(name="Purgatory Top Exit", region="Purgatory", - destination="Purgatory", tag="_top", direction=Direction.north), + destination="Purgatory", tag="_top"), ] diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index c98f8e5ef4..731bbf4758 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -925,7 +925,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Rooted Ziggurat Upper Entry"].connect( connecting_region=regions["Rooted Ziggurat Upper Front"]) - regions["Rooted Ziggurat Upper Front"].connect( + zig_upper_front_back = regions["Rooted Ziggurat Upper Front"].connect( connecting_region=regions["Rooted Ziggurat Upper Back"], rule=lambda state: state.has(laurels, player) or has_sword(state, player)) regions["Rooted Ziggurat Upper Back"].connect( @@ -1328,6 +1328,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ set_rule(lower_quarry_empty_to_combat, lambda state: has_combat_reqs("Quarry", state, player)) + set_rule(zig_upper_front_back, + lambda state: state.has(laurels, player) + or has_combat_reqs("Rooted Ziggurat", state, player)) set_rule(zig_low_entry_to_front, lambda state: has_combat_reqs("Rooted Ziggurat", state, player)) set_rule(zig_low_mid_to_front, @@ -1651,17 +1654,14 @@ def set_er_location_rules(world: "TunicWorld") -> None: # laurel means you can dodge the enemies freely with the laurels if set_instead: set_rule(multiworld.get_location(loc_name, player), - # someome tell me if you actually need to do the p=player and c=combat_req_area, lambdas scary - lambda state, p=player, c=combat_req_area, d=dagger, la=laurel: - has_combat_reqs(c, state, p) - or (state.has(ice_dagger, player) if d else False) - or (state.has(laurels, player) if la else False)) + lambda state: has_combat_reqs(combat_req_area, state, player) + or (dagger and state.has(ice_dagger, player)) + or (laurel and state.has(laurels, player))) else: add_rule(multiworld.get_location(loc_name, player), - lambda state, p=player, c=combat_req_area, d=dagger, la=laurel: - has_combat_reqs(c, state, p) - or (state.has(ice_dagger, player) if d else True) - or (state.has(laurels, player) if la else False)) + lambda state: has_combat_reqs(combat_req_area, state, player) + or (dagger and state.has(ice_dagger, player)) + or (laurel and state.has(laurels, player))) if world.options.combat_logic >= CombatLogic.option_bosses_only: # garden knight is in the regions part above diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 28bb5a84ee..edfd6d1d8e 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -157,7 +157,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal laurels_zips = world.options.laurels_zips.value ice_grappling = world.options.ice_grappling.value ladder_storage = world.options.ladder_storage.value - entrance_layout = world.options.entrance_layout + fixed_shop = world.options.fixed_shop laurels_location = world.options.laurels_location traversal_reqs = deepcopy(traversal_requirements) has_laurels = True @@ -169,7 +169,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal laurels_zips = seed_group["laurels_zips"] ice_grappling = seed_group["ice_grappling"] ladder_storage = seed_group["ladder_storage"] - entrance_layout = seed_group["entrance_layout"] + fixed_shop = seed_group["fixed_shop"] laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False logic_tricks: Tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage) @@ -181,7 +181,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal # need to keep track of which scenes have shops, since you shouldn't have multiple shops connected to the same scene shop_scenes: Set[str] = set() shop_count = 6 - if entrance_layout == EntranceLayout.option_fixed_shop: + if fixed_shop: shop_count = 0 shop_scenes.add("Overworld Redux") else: @@ -190,9 +190,6 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal if portal.region == "Zig Skip Exit": portal_map.remove(portal) break - # need 8 shops with direction pairs or there won't be a valid set of pairs - if entrance_layout == EntranceLayout.option_direction_pairs: - shop_count = 8 # If using Universal Tracker, restore portal_map. Could be cleaner, but it does not matter for UT even a little bit if hasattr(world.multiworld, "re_gen_passthrough"): @@ -219,7 +216,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal else: dead_ends.append(portal) if portal.region == "Zig Skip Exit": - if entrance_layout == EntranceLayout.option_fixed_shop: + if fixed_shop: two_plus.append(portal) else: dead_ends.append(portal) @@ -266,7 +263,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal # secret gathering place and zig skip get weird, special handling elif region_info.dead_end == DeadEnd.special: if (region_name == "Secret Gathering Place" and laurels_location == "10_fairies") \ - or (region_name == "Zig Skip Exit" and entrance_layout == EntranceLayout.option_fixed_shop): + or (region_name == "Zig Skip Exit" and fixed_shop): non_dead_end_regions.add(region_name) if plando_connections: @@ -322,12 +319,8 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal break # if it's not a dead end, it might be a shop if p_exit == "Shop Portal": - # 6 of the shops have south exits, 2 of them have west exits - shop_dir = Direction.south - if world.shop_num > 6: - shop_dir = Direction.west portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", - destination="Previous Region", tag="_", direction=shop_dir) + destination="Previous Region", tag="_") create_shop_region(world, regions) shop_count -= 1 # need to maintain an even number of portals total @@ -374,15 +367,15 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal # if we have plando connections, our connected regions may change somewhat connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) - if entrance_layout == EntranceLayout.option_fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"): + if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"): portal1 = None for portal in two_plus: if portal.scene_destination() == "Overworld Redux, Windmill_": portal1 = portal break if not portal1: - raise Exception(f"Failed to do Fixed Shop option for Entrance Layout. " - f"Did {player_name} plando the Windmill Shop entrance?") + raise Exception(f"Failed to do Fixed Shop option. " + f"Did {player_name} plando connection the Windmill Shop entrance?") portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", destination="Previous Region", tag="_") @@ -461,7 +454,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal if "TUNIC" in world.multiworld.re_gen_passthrough: shop_count = 0 - for _ in range(shop_count): + for i in range(shop_count): portal1 = None for portal in two_plus: if portal.scene() not in shop_scenes: @@ -471,12 +464,8 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal break if portal1 is None: raise Exception("Too many shops in the pool, or something else went wrong.") - # 6 of the shops have south exits, 2 of them have west exits - shop_dir = Direction.south - if world.shop_num > 6: - shop_dir = Direction.west portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", - destination="Previous Region", tag="_", direction=shop_dir) + destination="Previous Region", tag="_") create_shop_region(world, regions) portal_pairs[portal1] = portal2 @@ -557,53 +546,3 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic) return connected_regions - - -# which directions are opposites -direction_pairs: Dict[int, int] = { - Direction.north: Direction.south, - Direction.south: Direction.north, - Direction.east: Direction.west, - Direction.west: Direction.east, - Direction.ladder_up: Direction.ladder_down, - Direction.ladder_down: Direction.ladder_up, - Direction.floor: Direction.floor, -} - - -# verify that two portals are in compatible directions -def verify_direction_pair(portal1: Portal, portal2: Portal) -> bool: - if portal1.direction == direction_pairs[portal2.direction]: - return True - elif portal1.name.startswith("Shop"): - if portal2.direction in [Direction.north, Direction.east]: - return True - elif portal2.name.startswith("Shop"): - if portal1.direction in [Direction.north, Direction.east]: - return True - else: - return False - - -# verify that two plando'd portals are in compatible directions -def verify_plando_directions(connection: PlandoConnection) -> bool: - entrance_portal = None - exit_portal = None - for portal in portal_mapping: - if connection.entrance == portal.name: - entrance_portal = portal - if connection.exit == portal.name: - exit_portal = portal - if entrance_portal and exit_portal: - if entrance_portal.direction == direction_pairs[exit_portal.direction]: - return True - # this is two shop portals, they can never pair directions - elif not entrance_portal and not exit_portal: - return False - # if one of them is none, it's a shop, which has two possible directions - elif not entrance_portal: - if exit_portal.direction in [Direction.north, Direction.east]: - return True - elif not exit_portal: - if entrance_portal.direction in [Direction.north, Direction.east]: - return True diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index 6e05998c80..5c269595bf 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -208,6 +208,10 @@ slot_data_item_names = [ "Gold Questagon", ] +combat_items: List[str] = [name for name, data in item_table.items() + if data.combat_ic and IC.progression in data.combat_ic] +combat_items.extend(["Stick", "Sword", "Sword Upgrade", "Magic Wand", "Hero's Laurels"]) + item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()} filler_items: List[str] = [name for name, data in item_table.items() if data.classification == IC.filler] diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 069201b3fe..ec0dc06933 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -263,8 +263,8 @@ class LadderStorage(Choice): class LadderStorageWithoutItems(Toggle): """ - If disabled, you logically require Stick, Sword, or Magic Orb to Ladder Storage. - If enabled, you will be expected to Ladder Storage without progression items. + If disabled, you logically require Stick, Sword, or Magic Orb to perform Ladder Storage. + If enabled, you will be expected to perform Ladder Storage without progression items. This can be done with the plushie code, a Golden Coin, Prayer, and many other options. This option has no effect if you do not have Ladder Storage Logic enabled @@ -273,6 +273,24 @@ class LadderStorageWithoutItems(Toggle): display_name = "Ladder Storage without Items" +class LogicRules(Choice): + """ + This option has been superseded by the individual trick options. + If set to nmg, it will set Ice Grappling to medium and Laurels Zips on. + If set to ur, it will do nmg as well as set Ladder Storage to medium. + It is here to avoid breaking old yamls, and will be removed at a later date. + """ + visibility = Visibility.none + internal_name = "logic_rules" + display_name = "Logic Rules" + option_restricted = 0 + option_no_major_glitches = 1 + alias_nmg = 1 + option_unrestricted = 2 + alias_ur = 2 + default = 0 + + @dataclass class TunicOptions(PerGameCommonOptions): start_inventory_from_pool: StartInventoryPool @@ -297,6 +315,7 @@ class TunicOptions(PerGameCommonOptions): ice_grappling: IceGrappling ladder_storage: LadderStorage ladder_storage_without_items: LadderStorageWithoutItems + plando_connections: TunicPlandoConnections fixed_shop: FixedShop logic_rules: Removed # fully removed in the direction pairs update