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/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/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/hk/__init__.py b/worlds/hk/__init__.py index 78287305df..fbc6461f6a 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -554,7 +554,8 @@ 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 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..5d10f07747 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 @@ -1510,6 +1663,10 @@ - HIDE (3) - room: Outside The Agreeable panel: HIDE + panel_doors: + DOWN: + panels: + - DOWN The Perceptive: entrances: Starting Room: @@ -1531,6 +1688,10 @@ check: True exclude_reduce: True tag: botwhite + panel_doors: + GAZE: + panels: + - GAZE paintings: - id: garden_painting_tower orientation: north @@ -1572,9 +1733,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 +1831,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 +1948,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: @@ -1798,9 +1974,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 +2044,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 +2160,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 +2227,10 @@ - SALT - room: Directional Gallery panel: PEPPER + panel_doors: + SECRET: + panels: + - SECRET sunwarps: - dots: 4 direction: enter @@ -2174,6 +2383,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 +2501,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 +2850,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 +2876,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 +2902,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 +2928,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 +2954,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 +2980,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 +3006,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 +3032,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 +3058,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 +3084,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 +3110,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 +3154,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 +3268,10 @@ door_group: Hedge Maze Doors panels: - STRAYS + panel_doors: + STRAYS: + panels: + - STRAYS paintings: - id: arrows_painting_8 orientation: south @@ -3155,6 +3469,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 +3845,13 @@ - RISE (Sunrise) - ZEN - SON + panel_doors: + UNOPEN: + panels: + - UNOPEN + BEGIN: + panels: + - BEGIN paintings: - id: pencil_painting2 orientation: west @@ -3819,6 +4147,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 +4350,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 +4462,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 +4652,7 @@ tag: midorange required_door: room: Number Hunt - door: Sixes + door: First Six PARANOID: id: Backside Room/Panel_paranoid_paranoid tag: midwhite @@ -4203,7 +4660,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 +4723,11 @@ panels: - room: Color Hunt panel: YELLOW + panel_doors: + TURN LEARN: + panels: + - TURN + - LEARN paintings: - id: smile_painting_7 orientation: south @@ -4277,7 +4739,7 @@ move: True required_door: room: Number Hunt - door: Sixes + door: First Six - id: boxes_painting orientation: south - id: cherry_painting @@ -4344,6 +4806,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 +4971,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 +5831,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 +6520,10 @@ item_group: Achievement Room Entrances panels: - SHRINK + panel_doors: + SHRINK: + panels: + - SHRINK The Wondrous (Doorknob): entrances: Outside The Wondrous: @@ -6228,18 +6735,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 +6803,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 +6850,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 +6881,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 +6961,10 @@ panels: - room: The Wanderer panel: Achievement + panel_doors: + WANDERLUST: + panels: + - WANDERLUST The Wanderer: entrances: Outside The Wanderer: @@ -6553,6 +7106,10 @@ item_group: Achievement Room Entrances panels: - ORDER + panel_doors: + ORDER: + panels: + - ORDER paintings: - id: smile_painting_3 orientation: west @@ -6566,10 +7123,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 +7649,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 +7841,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 +7869,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 +7925,11 @@ panels: - KITTEN - CAT + panel_doors: + KITTEN CAT: + panels: + - KITTEN + - CAT paintings: - id: arrows_painting_2 orientation: east @@ -7608,6 +8184,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..ad412fc352 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 1882dc26ce..249270103c 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -132,7 +132,7 @@ class TunicWorld(World): ladder_storage=tunic.options.ladder_storage.value, laurels_at_10_fairies=tunic.options.laurels_location == LaurelsLocation.option_10_fairies, fixed_shop=bool(tunic.options.fixed_shop), - plando=multiworld.plando_connections[tunic.player]) + plando=tunic.options.plando_connections) continue # off is more restrictive @@ -151,9 +151,9 @@ class TunicWorld(World): if tunic.options.fixed_shop: cls.seed_groups[group]["fixed_shop"] = True - if multiworld.plando_connections[tunic.player]: + if tunic.options.plando_connections: # loop through the connections in the player's yaml - for cxn in multiworld.plando_connections[tunic.player]: + for cxn in tunic.options.plando_connections: new_cxn = True for group_cxn in cls.seed_groups[group]["plando"]: # if neither entrance nor exit match anything in the group, add to group