diff --git a/BaseClasses.py b/BaseClasses.py index 45190ac7b98..1b6677dd194 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -226,25 +226,24 @@ def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optio range(1, self.players + 1)} def set_options(self, args: Namespace) -> None: - for option_key in Options.common_options: - setattr(self, option_key, getattr(args, option_key, {})) - for option_key in Options.per_game_common_options: - setattr(self, option_key, getattr(args, option_key, {})) - for player in self.player_ids: self.custom_data[player] = {} world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] - for option_key in world_type.option_definitions: - setattr(self, option_key, getattr(args, option_key, {})) - self.worlds[player] = world_type(self, player) self.worlds[player].random = self.per_slot_randoms[player] + for option_key in world_type.options_dataclass.type_hints: + option_values = getattr(args, option_key, {}) + setattr(self, option_key, option_values) + # TODO - remove this loop once all worlds use options dataclasses + options_dataclass: typing.Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass + self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player] + for option_key in options_dataclass.type_hints}) def set_item_links(self): item_links = {} replacement_prio = [False, True, None] for player in self.player_ids: - for item_link in self.item_links[player].value: + for item_link in self.worlds[player].options.item_links.value: if item_link["name"] in item_links: if item_links[item_link["name"]]["game"] != self.game[player]: raise Exception(f"Cannot ItemLink across games. Link: {item_link['name']}") @@ -299,14 +298,6 @@ def set_item_links(self): group["non_local_items"] = item_link["non_local_items"] group["link_replacement"] = replacement_prio[item_link["link_replacement"]] - # intended for unittests - def set_default_common_options(self): - for option_key, option in Options.common_options.items(): - setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids}) - for option_key, option in Options.per_game_common_options.items(): - setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids}) - self.state = CollectionState(self) - def secure(self): self.random = ThreadBarrierProxy(secrets.SystemRandom()) self.is_race = True @@ -863,19 +854,19 @@ def add_locations(self, locations: Dict[str, Optional[int]], """ Adds locations to the Region object, where location_type is your Location class and locations is a dict of location names to address. - + :param locations: dictionary of locations to be created and added to this Region `{name: ID}` :param location_type: Location class to be used to create the locations with""" if location_type is None: location_type = Location for location, address in locations.items(): self.locations.append(location_type(self.player, location, address, self)) - + def connect(self, connecting_region: Region, name: Optional[str] = None, rule: Optional[Callable[[CollectionState], bool]] = None) -> None: """ Connects this Region to another Region, placing the provided rule on the connection. - + :param connecting_region: Region object to connect to path is `self -> exiting_region` :param name: name of the connection being created :param rule: callable to determine access of this connection to go from self to the exiting_region""" @@ -883,11 +874,11 @@ def connect(self, connecting_region: Region, name: Optional[str] = None, if rule: exit_.access_rule = rule exit_.connect(connecting_region) - + def create_exit(self, name: str) -> Entrance: """ Creates and returns an Entrance object as an exit of this region. - + :param name: name of the Entrance being created """ exit_ = self.entrance_type(self.player, name, self) @@ -1257,7 +1248,7 @@ def get_path(state: CollectionState, region: Region) -> List[Union[Tuple[str, st def to_file(self, filename: str) -> None: def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None: - res = getattr(self.multiworld, option_key)[player] + res = getattr(self.multiworld.worlds[player].options, option_key) display_name = getattr(option_obj, "display_name", option_key) outfile.write(f"{display_name + ':':33}{res.current_option_name}\n") @@ -1275,8 +1266,7 @@ def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None: outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player))) outfile.write('Game: %s\n' % self.multiworld.game[player]) - options = ChainMap(Options.per_game_common_options, self.multiworld.worlds[player].option_definitions) - for f_option, option in options.items(): + for f_option, option in self.multiworld.worlds[player].options_dataclass.type_hints.items(): write_option(f_option, option) AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile) diff --git a/Fill.py b/Fill.py index 7c81aed7ba9..600d18ef2a5 100644 --- a/Fill.py +++ b/Fill.py @@ -5,6 +5,8 @@ from collections import Counter, deque from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld +from Options import Accessibility + from worlds.AutoWorld import call_all from worlds.generic.Rules import add_item_rule @@ -70,7 +72,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: spot_to_fill: typing.Optional[Location] = None # if minimal accessibility, only check whether location is reachable if game not beatable - if world.accessibility[item_to_place.player] == 'minimal': + if world.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal: perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) \ if single_player_placement else not has_beaten_game @@ -265,7 +267,7 @@ def fast_fill(world: MultiWorld, def accessibility_corrections(world: MultiWorld, state: CollectionState, locations, pool=[]): maximum_exploration_state = sweep_from_pool(state, pool) - minimal_players = {player for player in world.player_ids if world.accessibility[player] == "minimal"} + minimal_players = {player for player in world.player_ids if world.worlds[player].options.accessibility == "minimal"} unreachable_locations = [location for location in world.get_locations() if location.player in minimal_players and not location.can_reach(maximum_exploration_state)] for location in unreachable_locations: @@ -288,7 +290,7 @@ def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locat unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)] if unreachable_locations: def forbid_important_item_rule(item: Item): - return not ((item.classification & 0b0011) and world.accessibility[item.player] != 'minimal') + return not ((item.classification & 0b0011) and world.worlds[item.player].options.accessibility != 'minimal') for location in unreachable_locations: add_item_rule(location, forbid_important_item_rule) @@ -531,9 +533,9 @@ def balance_multiworld_progression(world: MultiWorld) -> None: # If other players are below the threshold value, swap progression in this sphere into earlier spheres, # which gives more locations available by this sphere. balanceable_players: typing.Dict[int, float] = { - player: world.progression_balancing[player] / 100 + player: world.worlds[player].options.progression_balancing / 100 for player in world.player_ids - if world.progression_balancing[player] > 0 + if world.worlds[player].options.progression_balancing > 0 } if not balanceable_players: logging.info('Skipping multiworld progression balancing.') diff --git a/Generate.py b/Generate.py index 5d44a1db455..08fe2b90833 100644 --- a/Generate.py +++ b/Generate.py @@ -157,7 +157,8 @@ def main(args=None, callback=ERmain): for yaml in weights_cache[path]: if category_name is None: for category in yaml: - if category in AutoWorldRegister.world_types and key in Options.common_options: + if category in AutoWorldRegister.world_types and \ + key in Options.CommonOptions.type_hints: yaml[category][key] = option elif category_name not in yaml: logging.warning(f"Meta: Category {category_name} is not present in {path}.") @@ -340,7 +341,7 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any: return get_choice(option_key, category_dict) if game in AutoWorldRegister.world_types: game_world = AutoWorldRegister.world_types[game] - options = ChainMap(game_world.option_definitions, Options.per_game_common_options) + options = game_world.options_dataclass.type_hints if option_key in options: if options[option_key].supports_weighting: return get_choice(option_key, category_dict) @@ -445,8 +446,8 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b f"which is not enabled.") ret = argparse.Namespace() - for option_key in Options.per_game_common_options: - if option_key in weights and option_key not in Options.common_options: + for option_key in Options.PerGameCommonOptions.type_hints: + if option_key in weights and option_key not in Options.CommonOptions.type_hints: raise Exception(f"Option {option_key} has to be in a game's section, not on its own.") ret.game = get_choice("game", weights) @@ -466,16 +467,11 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b game_weights = weights[ret.game] ret.name = get_choice('name', weights) - for option_key, option in Options.common_options.items(): + for option_key, option in Options.CommonOptions.type_hints.items(): setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default))) - for option_key, option in world_type.option_definitions.items(): + for option_key, option in world_type.options_dataclass.type_hints.items(): handle_option(ret, game_weights, option_key, option, plando_options) - for option_key, option in Options.per_game_common_options.items(): - # skip setting this option if already set from common_options, defaulting to root option - if option_key not in world_type.option_definitions and \ - (option_key not in Options.common_options or option_key in game_weights): - handle_option(ret, game_weights, option_key, option, plando_options) if PlandoOptions.items in plando_options: ret.plando_items = game_weights.get("plando_items", []) if ret.game == "Minecraft" or ret.game == "Ocarina of Time": diff --git a/Main.py b/Main.py index 48b37764a99..dfc4ed17930 100644 --- a/Main.py +++ b/Main.py @@ -108,7 +108,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No logger.info('') for player in world.player_ids: - for item_name, count in world.start_inventory[player].value.items(): + for item_name, count in world.worlds[player].options.start_inventory.value.items(): for _ in range(count): world.push_precollected(world.create_item(item_name, player)) @@ -130,15 +130,15 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for player in world.player_ids: # items can't be both local and non-local, prefer local - world.non_local_items[player].value -= world.local_items[player].value - world.non_local_items[player].value -= set(world.local_early_items[player]) + world.worlds[player].options.non_local_items.value -= world.worlds[player].options.local_items.value + world.worlds[player].options.non_local_items.value -= set(world.local_early_items[player]) AutoWorld.call_all(world, "set_rules") for player in world.player_ids: - exclusion_rules(world, player, world.exclude_locations[player].value) - world.priority_locations[player].value -= world.exclude_locations[player].value - for location_name in world.priority_locations[player].value: + exclusion_rules(world, player, world.worlds[player].options.exclude_locations.value) + world.worlds[player].options.priority_locations.value -= world.worlds[player].options.exclude_locations.value + for location_name in world.worlds[player].options.priority_locations.value: try: location = world.get_location(location_name, player) except KeyError as e: # failed to find the given location. Check if it's a legitimate location @@ -151,8 +151,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No if world.players > 1: locality_rules(world) else: - world.non_local_items[1].value = set() - world.local_items[1].value = set() + world.worlds[1].options.non_local_items.value = set() + world.worlds[1].options.local_items.value = set() AutoWorld.call_all(world, "generate_basic") @@ -360,11 +360,11 @@ def precollect_hint(location): f" {location}" locations_data[location.player][location.address] = \ location.item.code, location.item.player, location.item.flags - if location.name in world.start_location_hints[location.player]: + if location.name in world.worlds[location.player].options.start_location_hints: precollect_hint(location) - elif location.item.name in world.start_hints[location.item.player]: + elif location.item.name in world.worlds[location.item.player].options.start_hints: precollect_hint(location) - elif any([location.item.name in world.start_hints[player] + elif any([location.item.name in world.worlds[player].options.start_hints for player in world.groups.get(location.item.player, {}).get("players", [])]): precollect_hint(location) diff --git a/Options.py b/Options.py index 960e6c19d1a..d9ddfc2e2fd 100644 --- a/Options.py +++ b/Options.py @@ -2,6 +2,9 @@ import abc import logging +from copy import deepcopy +from dataclasses import dataclass +import functools import math import numbers import random @@ -211,6 +214,12 @@ def __gt__(self, other: typing.Union[int, NumericOption]) -> bool: else: return self.value > other + def __ge__(self, other: typing.Union[int, NumericOption]) -> bool: + if isinstance(other, NumericOption): + return self.value >= other.value + else: + return self.value >= other + def __bool__(self) -> bool: return bool(self.value) @@ -896,10 +905,55 @@ class ProgressionBalancing(SpecialRange): } -common_options = { - "progression_balancing": ProgressionBalancing, - "accessibility": Accessibility -} +class OptionsMetaProperty(type): + def __new__(mcs, + name: str, + bases: typing.Tuple[type, ...], + attrs: typing.Dict[str, typing.Any]) -> "OptionsMetaProperty": + for attr_type in attrs.values(): + assert not isinstance(attr_type, AssembleOptions),\ + f"Options for {name} should be type hinted on the class, not assigned" + return super().__new__(mcs, name, bases, attrs) + + @property + @functools.lru_cache(maxsize=None) + def type_hints(cls) -> typing.Dict[str, typing.Type[Option[typing.Any]]]: + """Returns type hints of the class as a dictionary.""" + return typing.get_type_hints(cls) + + +@dataclass +class CommonOptions(metaclass=OptionsMetaProperty): + progression_balancing: ProgressionBalancing + accessibility: Accessibility + + def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]: + """ + Returns a dictionary of [str, Option.value] + + :param option_names: names of the options to return + :param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab` + """ + option_results = {} + for option_name in option_names: + if option_name in type(self).type_hints: + if casing == "snake": + display_name = option_name + elif casing == "camel": + split_name = [name.title() for name in option_name.split("_")] + split_name[0] = split_name[0].lower() + display_name = "".join(split_name) + elif casing == "pascal": + display_name = "".join([name.title() for name in option_name.split("_")]) + elif casing == "kebab": + display_name = option_name.replace("_", "-") + else: + raise ValueError(f"{casing} is invalid casing for as_dict. " + "Valid names are 'snake', 'camel', 'pascal', 'kebab'.") + option_results[display_name] = getattr(self, option_name).value + else: + raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}") + return option_results class LocalItems(ItemSet): @@ -1020,17 +1074,16 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P link.setdefault("link_replacement", None) -per_game_common_options = { - **common_options, # can be overwritten per-game - "local_items": LocalItems, - "non_local_items": NonLocalItems, - "start_inventory": StartInventory, - "start_hints": StartHints, - "start_location_hints": StartLocationHints, - "exclude_locations": ExcludeLocations, - "priority_locations": PriorityLocations, - "item_links": ItemLinks -} +@dataclass +class PerGameCommonOptions(CommonOptions): + local_items: LocalItems + non_local_items: NonLocalItems + start_inventory: StartInventory + start_hints: StartHints + start_location_hints: StartLocationHints + exclude_locations: ExcludeLocations + priority_locations: PriorityLocations + item_links: ItemLinks def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True): @@ -1071,10 +1124,7 @@ def dictify_range(option: typing.Union[Range, SpecialRange]): for game_name, world in AutoWorldRegister.world_types.items(): if not world.hidden or generate_hidden: - all_options: typing.Dict[str, AssembleOptions] = { - **per_game_common_options, - **world.option_definitions - } + all_options: typing.Dict[str, AssembleOptions] = world.options_dataclass.type_hints with open(local_path("data", "options.yaml")) as f: file_data = f.read() diff --git a/WebHostLib/options.py b/WebHostLib/options.py index fca01407e06..18a28045ee2 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -36,10 +36,7 @@ def get_html_doc(option_type: type(Options.Option)) -> str: for game_name, world in AutoWorldRegister.world_types.items(): - all_options: typing.Dict[str, Options.AssembleOptions] = { - **Options.per_game_common_options, - **world.option_definitions - } + all_options: typing.Dict[str, Options.AssembleOptions] = world.options_dataclass.type_hints # Generate JSON files for player-settings pages player_settings = { diff --git a/docs/options api.md b/docs/options api.md index fdabd9facd8..2c86833800c 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -28,19 +28,23 @@ Choice, and defining `alias_true = option_full`. and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`. As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's -create our option class (with a docstring), give it a `display_name`, and add it to a dictionary that keeps track of our -options: +create our option class (with a docstring), give it a `display_name`, and add it to our game's options dataclass: ```python # Options.py +from dataclasses import dataclass + +from Options import Toggle, PerGameCommonOptions + + class StartingSword(Toggle): """Adds a sword to your starting inventory.""" display_name = "Start With Sword" -example_options = { - "starting_sword": StartingSword -} +@dataclass +class ExampleGameOptions(PerGameCommonOptions): + starting_sword: StartingSword ``` This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it @@ -48,27 +52,30 @@ to our world's `__init__.py`: ```python from worlds.AutoWorld import World -from .Options import options +from .Options import ExampleGameOptions class ExampleWorld(World): - option_definitions = options + # this gives the generator all the definitions for our options + options_dataclass = ExampleGameOptions + # this gives us typing hints for all the options we defined + options: ExampleGameOptions ``` ### Option Checking Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after world instantiation. These are created as attributes on the MultiWorld and can be accessed with -`self.multiworld.my_option_name[self.player]`. This is the option class, which supports direct comparison methods to +`self.options.my_option_name`. This is an instance of the option class, which supports direct comparison methods to relevant objects (like comparing a Toggle class to a `bool`). If you need to access the option result directly, this is the option class's `value` attribute. For our example above we can do a simple check: ```python -if self.multiworld.starting_sword[self.player]: +if self.options.starting_sword: do_some_things() ``` or if I need a boolean object, such as in my slot_data I can access it as: ```python -start_with_sword = bool(self.multiworld.starting_sword[self.player].value) +start_with_sword = bool(self.options.starting_sword.value) ``` ## Generic Option Classes @@ -120,7 +127,7 @@ Like Toggle, but 1 (true) is the default value. A numeric option allowing you to define different sub options. Values are stored as integers, but you can also do comparison methods with the class and strings, so if you have an `option_early_sword`, this can be compared with: ```python -if self.multiworld.sword_availability[self.player] == "early_sword": +if self.options.sword_availability == "early_sword": do_early_sword_things() ``` @@ -128,7 +135,7 @@ or: ```python from .Options import SwordAvailability -if self.multiworld.sword_availability[self.player] == SwordAvailability.option_early_sword: +if self.options.sword_availability == SwordAvailability.option_early_sword: do_early_sword_things() ``` @@ -160,7 +167,7 @@ within the world. Like choice allows you to predetermine options and has all of the same comparison methods and handling. Also accepts any user defined string as a valid option, so will either need to be validated by adding a validation step to the option class or within world, if necessary. Value for this class is `Union[str, int]` so if you need the value at a specified -point, `self.multiworld.my_option[self.player].current_key` will always return a string. +point, `self.options.my_option.current_key` will always return a string. ### PlandoBosses An option specifically built for handling boss rando, if your game can use it. Is a subclass of TextChoice so supports diff --git a/docs/world api.md b/docs/world api.md index 05b9e093994..6fb5b3ac9c6 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -86,9 +86,11 @@ inside a `World` object. ### Player Options Players provide customized settings for their World in the form of yamls. -Those are accessible through `self.multiworld.[self.player]`. A dict -of valid options has to be provided in `self.option_definitions`. Options are automatically -added to the `World` object for easy access. +A `dataclass` of valid options definitions has to be provided in `self.options_dataclass`. +(It must be a subclass of `PerGameCommonOptions`.) +Option results are automatically added to the `World` object for easy access. +Those are accessible through `self.options.`, and you can get a dictionary of the option values via +`self.options.as_dict()`, passing the desired options as strings. ### World Settings @@ -221,11 +223,11 @@ See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requireme AP will only import the `__init__.py`. Depending on code size it makes sense to use multiple files and use relative imports to access them. -e.g. `from .Options import mygame_options` from your `__init__.py` will load -`worlds//Options.py` and make its `mygame_options` accessible. +e.g. `from .Options import MyGameOptions` from your `__init__.py` will load +`world/[world_name]/Options.py` and make its `MyGameOptions` accessible. When imported names pile up it may be easier to use `from . import Options` -and access the variable as `Options.mygame_options`. +and access the variable as `Options.MyGameOptions`. Imports from directories outside your world should use absolute imports. Correct use of relative / absolute imports is required for zipped worlds to @@ -273,8 +275,9 @@ Each option has its own class, inherits from a base option type, has a docstring to describe it and a `display_name` property for display on the website and in spoiler logs. -The actual name as used in the yaml is defined in a `Dict[str, AssembleOptions]`, that is -assigned to the world under `self.option_definitions`. +The actual name as used in the yaml is defined via the field names of a `dataclass` that is +assigned to the world under `self.options_dataclass`. By convention, the strings +that define your option names should be in `snake_case`. Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`. For more see `Options.py` in AP's base directory. @@ -309,8 +312,8 @@ default = 0 ```python # Options.py -from Options import Toggle, Range, Choice, Option -import typing +from dataclasses import dataclass +from Options import Toggle, Range, Choice, PerGameCommonOptions class Difficulty(Choice): """Sets overall game difficulty.""" @@ -333,23 +336,27 @@ class FixXYZGlitch(Toggle): """Fixes ABC when you do XYZ""" display_name = "Fix XYZ Glitch" -# By convention we call the options dict variable `_options`. -mygame_options: typing.Dict[str, AssembleOptions] = { - "difficulty": Difficulty, - "final_boss_hp": FinalBossHP, - "fix_xyz_glitch": FixXYZGlitch, -} +# By convention, we call the options dataclass `Options`. +# It has to be derived from 'PerGameCommonOptions'. +@dataclass +class MyGameOptions(PerGameCommonOptions): + difficulty: Difficulty + final_boss_hp: FinalBossHP + fix_xyz_glitch: FixXYZGlitch ``` + ```python # __init__.py from worlds.AutoWorld import World -from .Options import mygame_options # import the options dict +from .Options import MyGameOptions # import the options dataclass + class MyGameWorld(World): - #... - option_definitions = mygame_options # assign the options dict to the world - #... + # ... + options_dataclass = MyGameOptions # assign the options dataclass to the world + options: MyGameOptions # typing for option results + # ... ``` ### A World Class Skeleton @@ -359,13 +366,14 @@ class MyGameWorld(World): import settings import typing -from .Options import mygame_options # the options we defined earlier +from .Options import MyGameOptions # the options we defined earlier from .Items import mygame_items # data used below to add items to the World from .Locations import mygame_locations # same as above from worlds.AutoWorld import World from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification + class MyGameItem(Item): # or from Items import MyGameItem game = "My Game" # name of the game/world this item is from @@ -374,6 +382,7 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation game = "My Game" # name of the game/world this location is in + class MyGameSettings(settings.Group): class RomFile(settings.SNESRomPath): """Insert help text for host.yaml here.""" @@ -384,7 +393,8 @@ class MyGameSettings(settings.Group): class MyGameWorld(World): """Insert description of the world/game here.""" game = "My Game" # name of the game/world - option_definitions = mygame_options # options the player can set + options_dataclass = MyGameOptions # options the player can set + options: MyGameOptions # typing hints for option results settings: typing.ClassVar[MyGameSettings] # will be automatically assigned from type hint topology_present = True # show path to required location checks in spoiler @@ -460,7 +470,7 @@ In addition, the following methods can be implemented and are called in this ord ```python def generate_early(self) -> None: # read player settings to world instance - self.final_boss_hp = self.multiworld.final_boss_hp[self.player].value + self.final_boss_hp = self.options.final_boss_hp.value ``` #### create_item @@ -687,9 +697,9 @@ def generate_output(self, output_directory: str): in self.multiworld.precollected_items[self.player]], "final_boss_hp": self.final_boss_hp, # store option name "easy", "normal" or "hard" for difficuly - "difficulty": self.multiworld.difficulty[self.player].current_key, + "difficulty": self.options.difficulty.current_key, # store option value True or False for fixing a glitch - "fix_xyz_glitch": self.multiworld.fix_xyz_glitch[self.player].value, + "fix_xyz_glitch": self.options.fix_xyz_glitch.value, } # point to a ROM specified by the installation src = self.settings.rom_file @@ -702,6 +712,26 @@ def generate_output(self, output_directory: str): generate_mod(src, out_file, data) ``` +### Slot Data + +If the game client needs to know information about the generated seed, a preferred method of transferring the data +is through the slot data. This can be filled from the `fill_slot_data` method of your world by returning a `Dict[str, Any]`, +but should be limited to data that is absolutely necessary to not waste resources. Slot data is sent to your client once +it has successfully [connected](network%20protocol.md#connected). +If you need to know information about locations in your world, instead +of propagating the slot data, it is preferable to use [LocationScouts](network%20protocol.md#locationscouts) since that +data already exists on the server. The most common usage of slot data is to send option results that the client needs +to be aware of. + +```python +def fill_slot_data(self): + # in order for our game client to handle the generated seed correctly we need to know what the user selected + # for their difficulty and final boss HP + # a dictionary returned from this method gets set as the slot_data and will be sent to the client after connecting + # the options dataclass has a method to return a `Dict[str, Any]` of each option name provided and the option's value + return self.options.as_dict("difficulty", "final_boss_hp") +``` + ### Documentation Each world implementation should have a tutorial and a game info page. These are both rendered on the website by reading diff --git a/test/TestBase.py b/test/TestBase.py index 61088e581c8..e6fbafd95aa 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -125,13 +125,13 @@ def world_setup(self, seed: typing.Optional[int] = None) -> None: self.multiworld.game[1] = self.game self.multiworld.player_name = {1: "Tester"} self.multiworld.set_seed(seed) + self.multiworld.state = CollectionState(self.multiworld) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items(): setattr(args, name, { 1: option.from_any(self.options.get(name, getattr(option, "default"))) }) self.multiworld.set_options(args) - self.multiworld.set_default_common_options() for step in gen_steps: call_all(self.multiworld, step) diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 99f48cd0c70..0933603dfdd 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -1,16 +1,20 @@ from typing import List, Iterable import unittest + +import Options +from Options import Accessibility from worlds.AutoWorld import World from Fill import FillError, balance_multiworld_progression, fill_restrictive, \ distribute_early_items, distribute_items_restrictive from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, Item, Location, \ - ItemClassification + ItemClassification, CollectionState from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule def generate_multi_world(players: int = 1) -> MultiWorld: multi_world = MultiWorld(players) multi_world.player_name = {} + multi_world.state = CollectionState(multi_world) for i in range(players): player_id = i+1 world = World(multi_world, player_id) @@ -19,9 +23,16 @@ def generate_multi_world(players: int = 1) -> MultiWorld: multi_world.player_name[player_id] = "Test Player " + str(player_id) region = Region("Menu", player_id, multi_world, "Menu Region Hint") multi_world.regions.append(region) + for option_key, option in Options.PerGameCommonOptions.type_hints.items(): + if hasattr(multi_world, option_key): + getattr(multi_world, option_key).setdefault(player_id, option.from_any(getattr(option, "default"))) + else: + setattr(multi_world, option_key, {player_id: option.from_any(getattr(option, "default"))}) + # TODO - remove this loop once all worlds use options dataclasses + world.options = world.options_dataclass(**{option_key: getattr(multi_world, option_key)[player_id] + for option_key in world.options_dataclass.type_hints}) multi_world.set_seed(0) - multi_world.set_default_common_options() return multi_world @@ -186,7 +197,7 @@ def test_minimal_fill(self): items = player1.prog_items locations = player1.locations - multi_world.accessibility[player1.id].value = multi_world.accessibility[player1.id].option_minimal + multi_world.worlds[player1.id].options.accessibility = Accessibility.from_any(Accessibility.option_minimal) multi_world.completion_condition[player1.id] = lambda state: state.has( items[1].name, player1.id) set_rule(locations[1], lambda state: state.has( diff --git a/test/general/TestHelpers.py b/test/general/TestHelpers.py index c0b560c7e4a..17fdce653c8 100644 --- a/test/general/TestHelpers.py +++ b/test/general/TestHelpers.py @@ -1,3 +1,4 @@ +from argparse import Namespace from typing import Dict, Optional, Callable from BaseClasses import MultiWorld, CollectionState, Region @@ -13,7 +14,6 @@ def setUp(self) -> None: self.multiworld.game[self.player] = "helper_test_game" self.multiworld.player_name = {1: "Tester"} self.multiworld.set_seed() - self.multiworld.set_default_common_options() def testRegionHelpers(self) -> None: regions: Dict[str, str] = { diff --git a/test/general/TestOptions.py b/test/general/TestOptions.py index b7058183e09..4a3bd0b02a0 100644 --- a/test/general/TestOptions.py +++ b/test/general/TestOptions.py @@ -6,6 +6,6 @@ class TestOptions(unittest.TestCase): def testOptionsHaveDocString(self): for gamename, world_type in AutoWorldRegister.world_types.items(): if not world_type.hidden: - for option_key, option in world_type.option_definitions.items(): + for option_key, option in world_type.options_dataclass.type_hints.items(): with self.subTest(game=gamename, option=option_key): self.assertTrue(option.__doc__) diff --git a/test/general/__init__.py b/test/general/__init__.py index b0fb7ca32e7..d7ecc957493 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -1,7 +1,7 @@ from argparse import Namespace from typing import Type, Tuple -from BaseClasses import MultiWorld +from BaseClasses import MultiWorld, CollectionState from worlds.AutoWorld import call_all, World gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") @@ -12,11 +12,11 @@ def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_ multiworld.game[1] = world_type.game multiworld.player_name = {1: "Tester"} multiworld.set_seed() + multiworld.state = CollectionState(multiworld) args = Namespace() - for name, option in world_type.option_definitions.items(): + for name, option in world_type.options_dataclass.type_hints.items(): setattr(args, name, {1: option.from_any(option.default)}) multiworld.set_options(args) - multiworld.set_default_common_options() for step in steps: call_all(multiworld, step) return multiworld diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index e2fda16b87d..9a8b6a56ef3 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -4,11 +4,12 @@ import logging import pathlib import sys -from typing import Any, Callable, ClassVar, Dict, FrozenSet, List, Optional, Set, TYPE_CHECKING, TextIO, Tuple, Type, \ +from dataclasses import make_dataclass +from typing import Any, Callable, ClassVar, Dict, Set, Tuple, FrozenSet, List, Optional, TYPE_CHECKING, TextIO, Type, \ Union +from Options import PerGameCommonOptions from BaseClasses import CollectionState -from Options import AssembleOptions if TYPE_CHECKING: import random @@ -63,6 +64,12 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut dct["required_client_version"] = max(dct["required_client_version"], base.__dict__["required_client_version"]) + # create missing options_dataclass from legacy option_definitions + # TODO - remove this once all worlds use options dataclasses + if "options_dataclass" not in dct and "option_definitions" in dct: + dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(), + bases=(PerGameCommonOptions,)) + # construct class new_class = super().__new__(mcs, name, bases, dct) if "game" in dct: @@ -163,8 +170,11 @@ class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. A Game should have its own subclass of World in which it defines the required data structures.""" - option_definitions: ClassVar[Dict[str, AssembleOptions]] = {} + options_dataclass: ClassVar[Type[PerGameCommonOptions]] = PerGameCommonOptions """link your Options mapping""" + options: PerGameCommonOptions + """resulting options for the player of this world""" + game: ClassVar[str] """name the game""" topology_present: ClassVar[bool] = False @@ -362,16 +372,14 @@ def get_filler_item_name(self) -> str: def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World: """Creates a group, which is an instance of World that is responsible for multiple others. An example case is ItemLinks creating these.""" - import Options - - for option_key, option in cls.option_definitions.items(): - getattr(multiworld, option_key)[new_player_id] = option(option.default) - for option_key, option in Options.common_options.items(): - getattr(multiworld, option_key)[new_player_id] = option(option.default) - for option_key, option in Options.per_game_common_options.items(): + # TODO remove loop when worlds use options dataclass + for option_key, option in cls.options_dataclass.type_hints.items(): getattr(multiworld, option_key)[new_player_id] = option(option.default) + group = cls(multiworld, new_player_id) + group.options = cls.options_dataclass(**{option_key: option(option.default) + for option_key, option in cls.options_dataclass.type_hints.items()}) - return cls(multiworld, new_player_id) + return group # decent place to implement progressive items, in most cases can stay as-is def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]: diff --git a/worlds/alttp/test/__init__.py b/worlds/alttp/test/__init__.py index e69de29bb2d..5baaa7e88e6 100644 --- a/worlds/alttp/test/__init__.py +++ b/worlds/alttp/test/__init__.py @@ -0,0 +1,16 @@ +import unittest +from argparse import Namespace + +from BaseClasses import MultiWorld, CollectionState +from worlds import AutoWorldRegister + + +class LTTPTestBase(unittest.TestCase): + def world_setup(self): + self.multiworld = MultiWorld(1) + self.multiworld.state = CollectionState(self.multiworld) + self.multiworld.set_seed(None) + args = Namespace() + for name, option in AutoWorldRegister.world_types["A Link to the Past"].options_dataclass.type_hints.items(): + setattr(args, name, {1: option.from_any(getattr(option, "default"))}) + self.multiworld.set_options(args) diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index 81085ab10a1..94c30c34939 100644 --- a/worlds/alttp/test/dungeons/TestDungeon.py +++ b/worlds/alttp/test/dungeons/TestDungeon.py @@ -1,25 +1,16 @@ -import unittest -from argparse import Namespace - -from BaseClasses import MultiWorld, CollectionState, ItemClassification -from worlds.alttp.Dungeons import get_dungeon_item_pool +from BaseClasses import CollectionState, ItemClassification +from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import create_regions from worlds.alttp.Shops import create_shops -from worlds import AutoWorld +from worlds.alttp.test import LTTPTestBase -class TestDungeon(unittest.TestCase): +class TestDungeon(LTTPTestBase): def setUp(self): - self.multiworld = MultiWorld(1) - self.multiworld.set_seed(None) - args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): - setattr(args, name, {1: option.from_any(option.default)}) - self.multiworld.set_options(args) - self.multiworld.set_default_common_options() + self.world_setup() self.starting_regions = [] # Where to start exploring self.remove_exits = [] # Block dungeon exits self.multiworld.difficulty_requirements[1] = difficulties['normal'] diff --git a/worlds/alttp/test/inverted/TestInverted.py b/worlds/alttp/test/inverted/TestInverted.py index ad7458202ea..f5608ba07b2 100644 --- a/worlds/alttp/test/inverted/TestInverted.py +++ b/worlds/alttp/test/inverted/TestInverted.py @@ -1,6 +1,3 @@ -from argparse import Namespace - -from BaseClasses import MultiWorld from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool from worlds.alttp.EntranceShuffle import link_inverted_entrances from worlds.alttp.InvertedRegions import create_inverted_regions @@ -10,17 +7,12 @@ from worlds.alttp.Shops import create_shops from test.TestBase import TestBase -from worlds import AutoWorld +from worlds.alttp.test import LTTPTestBase + -class TestInverted(TestBase): +class TestInverted(TestBase, LTTPTestBase): def setUp(self): - self.multiworld = MultiWorld(1) - self.multiworld.set_seed(None) - args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): - setattr(args, name, {1: option.from_any(option.default)}) - self.multiworld.set_options(args) - self.multiworld.set_default_common_options() + self.world_setup() self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.mode[1] = "inverted" create_inverted_regions(self.multiworld, 1) diff --git a/worlds/alttp/test/inverted/TestInvertedBombRules.py b/worlds/alttp/test/inverted/TestInvertedBombRules.py index 89c5d786032..d9eacb5ad98 100644 --- a/worlds/alttp/test/inverted/TestInvertedBombRules.py +++ b/worlds/alttp/test/inverted/TestInvertedBombRules.py @@ -1,27 +1,17 @@ -import unittest -from argparse import Namespace - -from BaseClasses import MultiWorld from worlds.alttp.Dungeons import create_dungeons from worlds.alttp.EntranceShuffle import connect_entrance, Inverted_LW_Entrances, Inverted_LW_Dungeon_Entrances, Inverted_LW_Single_Cave_Doors, Inverted_Old_Man_Entrances, Inverted_DW_Entrances, Inverted_DW_Dungeon_Entrances, Inverted_DW_Single_Cave_Doors, \ Inverted_LW_Entrances_Must_Exit, Inverted_LW_Dungeon_Entrances_Must_Exit, Inverted_Bomb_Shop_Multi_Cave_Doors, Inverted_Bomb_Shop_Single_Cave_Doors, Blacksmith_Single_Cave_Doors, Inverted_Blacksmith_Multi_Cave_Doors from worlds.alttp.InvertedRegions import create_inverted_regions from worlds.alttp.ItemPool import difficulties from worlds.alttp.Rules import set_inverted_big_bomb_rules -from worlds import AutoWorld +from worlds.alttp.test import LTTPTestBase -class TestInvertedBombRules(unittest.TestCase): +class TestInvertedBombRules(LTTPTestBase): def setUp(self): - self.multiworld = MultiWorld(1) - self.multiworld.set_seed(None) + self.world_setup() self.multiworld.mode[1] = "inverted" - args = Namespace - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): - setattr(args, name, {1: option.from_any(option.default)}) - self.multiworld.set_options(args) - self.multiworld.set_default_common_options() self.multiworld.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.multiworld, 1) self.multiworld.worlds[1].create_dungeons() diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py index 72049e17742..33e58229818 100644 --- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py @@ -1,27 +1,18 @@ -from argparse import Namespace - -from BaseClasses import MultiWorld from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool from worlds.alttp.EntranceShuffle import link_inverted_entrances from worlds.alttp.InvertedRegions import create_inverted_regions -from worlds.alttp.ItemPool import generate_itempool, difficulties +from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Shops import create_shops -from worlds.alttp.Rules import set_rules from test.TestBase import TestBase -from worlds import AutoWorld +from worlds.alttp.test import LTTPTestBase + -class TestInvertedMinor(TestBase): +class TestInvertedMinor(TestBase, LTTPTestBase): def setUp(self): - self.multiworld = MultiWorld(1) - self.multiworld.set_seed(None) - args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): - setattr(args, name, {1: option.from_any(option.default)}) - self.multiworld.set_options(args) - self.multiworld.set_default_common_options() + self.world_setup() self.multiworld.mode[1] = "inverted" self.multiworld.logic[1] = "minorglitches" self.multiworld.difficulty_requirements[1] = difficulties['normal'] diff --git a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py index 77a551db6f8..a4e84fce9b6 100644 --- a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py +++ b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py @@ -1,28 +1,18 @@ -from argparse import Namespace - -from BaseClasses import MultiWorld from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool from worlds.alttp.EntranceShuffle import link_inverted_entrances from worlds.alttp.InvertedRegions import create_inverted_regions -from worlds.alttp.ItemPool import generate_itempool, difficulties +from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Shops import create_shops -from worlds.alttp.Rules import set_rules from test.TestBase import TestBase -from worlds import AutoWorld +from worlds.alttp.test import LTTPTestBase -class TestInvertedOWG(TestBase): +class TestInvertedOWG(TestBase, LTTPTestBase): def setUp(self): - self.multiworld = MultiWorld(1) - self.multiworld.set_seed(None) - args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): - setattr(args, name, {1: option.from_any(option.default)}) - self.multiworld.set_options(args) - self.multiworld.set_default_common_options() + self.world_setup() self.multiworld.logic[1] = "owglitches" self.multiworld.mode[1] = "inverted" self.multiworld.difficulty_requirements[1] = difficulties['normal'] diff --git a/worlds/alttp/test/minor_glitches/TestMinor.py b/worlds/alttp/test/minor_glitches/TestMinor.py index fdf626fe9d3..d5cfd3095b9 100644 --- a/worlds/alttp/test/minor_glitches/TestMinor.py +++ b/worlds/alttp/test/minor_glitches/TestMinor.py @@ -1,27 +1,15 @@ -from argparse import Namespace - -from BaseClasses import MultiWorld -from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool -from worlds.alttp.EntranceShuffle import link_entrances +from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.InvertedRegions import mark_dark_world_regions from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import ItemFactory -from worlds.alttp.Regions import create_regions -from worlds.alttp.Shops import create_shops from test.TestBase import TestBase -from worlds import AutoWorld +from worlds.alttp.test import LTTPTestBase -class TestMinor(TestBase): +class TestMinor(TestBase, LTTPTestBase): def setUp(self): - self.multiworld = MultiWorld(1) - self.multiworld.set_seed(None) - args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): - setattr(args, name, {1: option.from_any(option.default)}) - self.multiworld.set_options(args) - self.multiworld.set_default_common_options() + self.world_setup() self.multiworld.logic[1] = "minorglitches" self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.worlds[1].er_seed = 0 diff --git a/worlds/alttp/test/owg/TestVanillaOWG.py b/worlds/alttp/test/owg/TestVanillaOWG.py index c0888aa32fe..37b0b6ccb86 100644 --- a/worlds/alttp/test/owg/TestVanillaOWG.py +++ b/worlds/alttp/test/owg/TestVanillaOWG.py @@ -1,24 +1,15 @@ -from argparse import Namespace - -from BaseClasses import MultiWorld from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.InvertedRegions import mark_dark_world_regions from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import ItemFactory from test.TestBase import TestBase -from worlds import AutoWorld +from worlds.alttp.test import LTTPTestBase -class TestVanillaOWG(TestBase): +class TestVanillaOWG(TestBase, LTTPTestBase): def setUp(self): - self.multiworld = MultiWorld(1) - self.multiworld.set_seed(None) - args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): - setattr(args, name, {1: option.from_any(option.default)}) - self.multiworld.set_options(args) - self.multiworld.set_default_common_options() + self.world_setup() self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.logic[1] = "owglitches" self.multiworld.worlds[1].er_seed = 0 diff --git a/worlds/alttp/test/vanilla/TestVanilla.py b/worlds/alttp/test/vanilla/TestVanilla.py index e338410df20..3c983e98504 100644 --- a/worlds/alttp/test/vanilla/TestVanilla.py +++ b/worlds/alttp/test/vanilla/TestVanilla.py @@ -1,22 +1,14 @@ -from argparse import Namespace - -from BaseClasses import MultiWorld from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.InvertedRegions import mark_dark_world_regions from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import ItemFactory from test.TestBase import TestBase -from worlds import AutoWorld +from worlds.alttp.test import LTTPTestBase + -class TestVanilla(TestBase): +class TestVanilla(TestBase, LTTPTestBase): def setUp(self): - self.multiworld = MultiWorld(1) - self.multiworld.set_seed(None) - args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): - setattr(args, name, {1: option.from_any(option.default)}) - self.multiworld.set_options(args) - self.multiworld.set_default_common_options() + self.world_setup() self.multiworld.logic[1] = "noglitches" self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.worlds[1].er_seed = 0 diff --git a/worlds/dlcquest/Items.py b/worlds/dlcquest/Items.py index 61f4cd30fab..61d1be54cbd 100644 --- a/worlds/dlcquest/Items.py +++ b/worlds/dlcquest/Items.py @@ -1,11 +1,12 @@ import csv import enum import math -from typing import Protocol, Union, Dict, List, Set -from BaseClasses import Item, ItemClassification -from . import Options, data from dataclasses import dataclass, field from random import Random +from typing import Dict, List, Set + +from BaseClasses import Item, ItemClassification +from . import Options, data class DLCQuestItem(Item): @@ -93,38 +94,35 @@ def create_trap_items(world, World_Options: Options.DLCQuestOptions, trap_needed def create_items(world, World_Options: Options.DLCQuestOptions, locations_count: int, random: Random): created_items = [] - if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[ - Options.Campaign] == Options.Campaign.option_both: + if World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign == Options.Campaign.option_both: for item in items_by_group[Group.DLCQuest]: if item.has_any_group(Group.DLC): created_items.append(world.create_item(item)) - if item.has_any_group(Group.Item) and World_Options[ - Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + if item.has_any_group(Group.Item) and World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: created_items.append(world.create_item(item)) - if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: - coin_bundle_needed = math.floor(825 / World_Options[Options.CoinSanityRange]) + if World_Options.coinsanity == Options.CoinSanity.option_coin: + coin_bundle_needed = math.floor(825 / World_Options.coinbundlequantity) for item in items_by_group[Group.DLCQuest]: if item.has_any_group(Group.Coin): for i in range(coin_bundle_needed): created_items.append(world.create_item(item)) - if 825 % World_Options[Options.CoinSanityRange] != 0: + if 825 % World_Options.coinbundlequantity != 0: created_items.append(world.create_item(item)) - if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[ - Options.Campaign] == Options.Campaign.option_both: + if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or + World_Options.campaign == Options.Campaign.option_both): for item in items_by_group[Group.Freemium]: if item.has_any_group(Group.DLC): created_items.append(world.create_item(item)) - if item.has_any_group(Group.Item) and World_Options[ - Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + if item.has_any_group(Group.Item) and World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: created_items.append(world.create_item(item)) - if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: - coin_bundle_needed = math.floor(889 / World_Options[Options.CoinSanityRange]) + if World_Options.coinsanity == Options.CoinSanity.option_coin: + coin_bundle_needed = math.floor(889 / World_Options.coinbundlequantity) for item in items_by_group[Group.Freemium]: if item.has_any_group(Group.Coin): for i in range(coin_bundle_needed): created_items.append(world.create_item(item)) - if 889 % World_Options[Options.CoinSanityRange] != 0: + if 889 % World_Options.coinbundlequantity != 0: created_items.append(world.create_item(item)) trap_items = create_trap_items(world, World_Options, locations_count - len(created_items), random) diff --git a/worlds/dlcquest/Locations.py b/worlds/dlcquest/Locations.py index 08d37e78121..a9fdd00a202 100644 --- a/worlds/dlcquest/Locations.py +++ b/worlds/dlcquest/Locations.py @@ -1,5 +1,4 @@ -from BaseClasses import Location, MultiWorld -from . import Options +from BaseClasses import Location class DLCQuestLocation(Location): diff --git a/worlds/dlcquest/Options.py b/worlds/dlcquest/Options.py index a1674a4d5a8..ce728b4e924 100644 --- a/worlds/dlcquest/Options.py +++ b/worlds/dlcquest/Options.py @@ -1,22 +1,6 @@ -from typing import Union, Dict, runtime_checkable, Protocol -from Options import Option, DeathLink, Choice, Toggle, SpecialRange from dataclasses import dataclass - -@runtime_checkable -class DLCQuestOption(Protocol): - internal_name: str - - -@dataclass -class DLCQuestOptions: - options: Dict[str, Union[bool, int]] - - def __getitem__(self, item: Union[str, DLCQuestOption]) -> Union[bool, int]: - if isinstance(item, DLCQuestOption): - item = item.internal_name - - return self.options.get(item, None) +from Options import Choice, DeathLink, PerGameCommonOptions, SpecialRange class DoubleJumpGlitch(Choice): @@ -94,31 +78,13 @@ class ItemShuffle(Choice): default = 0 -DLCQuest_options: Dict[str, type(Option)] = { - option.internal_name: option - for option in [ - DoubleJumpGlitch, - CoinSanity, - CoinSanityRange, - TimeIsMoney, - EndingChoice, - Campaign, - ItemShuffle, - ] -} -default_options = {option.internal_name: option.default for option in DLCQuest_options.values()} -DLCQuest_options["death_link"] = DeathLink - - -def fetch_options(world, player: int) -> DLCQuestOptions: - return DLCQuestOptions({option: get_option_value(world, player, option) for option in DLCQuest_options}) - - -def get_option_value(world, player: int, name: str) -> Union[bool, int]: - assert name in DLCQuest_options, f"{name} is not a valid option for DLC Quest." - - value = getattr(world, name) - - if issubclass(DLCQuest_options[name], Toggle): - return bool(value[player].value) - return value[player].value +@dataclass +class DLCQuestOptions(PerGameCommonOptions): + double_jump_glitch: DoubleJumpGlitch + coinsanity: CoinSanity + coinbundlequantity: CoinSanityRange + time_is_money: TimeIsMoney + ending_choice: EndingChoice + campaign: Campaign + item_shuffle: ItemShuffle + death_link: DeathLink diff --git a/worlds/dlcquest/Regions.py b/worlds/dlcquest/Regions.py index 8135a1c362c..dfb5f6c021b 100644 --- a/worlds/dlcquest/Regions.py +++ b/worlds/dlcquest/Regions.py @@ -1,8 +1,9 @@ import math -from BaseClasses import MultiWorld, Region, Location, Entrance, ItemClassification + +from BaseClasses import Entrance, MultiWorld, Region +from . import Options from .Locations import DLCQuestLocation, location_table from .Rules import create_event -from . import Options DLCQuestRegion = ["Movement Pack", "Behind Tree", "Psychological Warfare", "Double Jump Left", "Double Jump Behind the Tree", "The Forest", "Final Room"] @@ -26,16 +27,16 @@ def add_coin_dlcquest(region: Region, Coin: int, player: int): def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQuestOptions): Regmenu = Region("Menu", player, world) - if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[ - Options.Campaign] == Options.Campaign.option_both: + if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign + == Options.Campaign.option_both): Regmenu.exits += [Entrance(player, "DLC Quest Basic", Regmenu)] - if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[ - Options.Campaign] == Options.Campaign.option_both: + if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign + == Options.Campaign.option_both): Regmenu.exits += [Entrance(player, "Live Freemium or Die", Regmenu)] world.regions.append(Regmenu) - if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[ - Options.Campaign] == Options.Campaign.option_both: + if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign + == Options.Campaign.option_both): Regmoveright = Region("Move Right", player, world, "Start of the basic game") Locmoveright_name = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"] @@ -43,13 +44,13 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue Regmoveright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmoveright) for loc_name in Locmoveright_name] add_coin_dlcquest(Regmoveright, 4, player) - if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: - coin_bundle_needed = math.floor(825 / World_Options[Options.CoinSanityRange]) + if World_Options.coinsanity == Options.CoinSanity.option_coin: + coin_bundle_needed = math.floor(825 / World_Options.coinbundlequantity) for i in range(coin_bundle_needed): - item_coin = f"DLC Quest: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin" + item_coin = f"DLC Quest: {World_Options.coinbundlequantity * (i + 1)} Coin" Regmoveright.locations += [ DLCQuestLocation(player, item_coin, location_table[item_coin], Regmoveright)] - if 825 % World_Options[Options.CoinSanityRange] != 0: + if 825 % World_Options.coinbundlequantity != 0: Regmoveright.locations += [ DLCQuestLocation(player, "DLC Quest: 825 Coin", location_table["DLC Quest: 825 Coin"], Regmoveright)] @@ -58,7 +59,7 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue Regmovpack = Region("Movement Pack", player, world) Locmovpack_name = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack", "Shepherd Sheep"] - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: Locmovpack_name += ["Sword"] Regmovpack.exits = [Entrance(player, "Tree", Regmovpack), Entrance(player, "Cloud", Regmovpack)] Regmovpack.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmovpack) for loc_name @@ -68,7 +69,7 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue Regbtree = Region("Behind Tree", player, world) Locbtree_name = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"] - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: Locbtree_name += ["Gun"] Regbtree.exits = [Entrance(player, "Behind Tree Double Jump", Regbtree), Entrance(player, "Forest Entrance", Regbtree)] @@ -191,27 +192,27 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue world.get_entrance("True Double Jump", player).connect(world.get_region("True Double Jump Behind Tree", player)) - if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[ - Options.Campaign] == Options.Campaign.option_both: + if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign + == Options.Campaign.option_both): Regfreemiumstart = Region("Freemium Start", player, world) Locfreemiumstart_name = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack", "Nice Try", "Story is Important", "I Get That Reference!"] - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: Locfreemiumstart_name += ["Wooden Sword"] Regfreemiumstart.exits = [Entrance(player, "Vines", Regfreemiumstart)] Regfreemiumstart.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfreemiumstart) for loc_name in Locfreemiumstart_name] add_coin_freemium(Regfreemiumstart, 50, player) - if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: - coin_bundle_needed = math.floor(889 / World_Options[Options.CoinSanityRange]) + if World_Options.coinsanity == Options.CoinSanity.option_coin: + coin_bundle_needed = math.floor(889 / World_Options.coinbundlequantity) for i in range(coin_bundle_needed): - item_coin_freemium = f"Live Freemium or Die: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin" + item_coin_freemium = f"Live Freemium or Die: {World_Options.coinbundlequantity * (i + 1)} Coin" Regfreemiumstart.locations += [ DLCQuestLocation(player, item_coin_freemium, location_table[item_coin_freemium], Regfreemiumstart)] - if 889 % World_Options[Options.CoinSanityRange] != 0: + if 889 % World_Options.coinbundlequantity != 0: Regfreemiumstart.locations += [ DLCQuestLocation(player, "Live Freemium or Die: 889 Coin", location_table["Live Freemium or Die: 889 Coin"], @@ -220,7 +221,7 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue Regbehindvine = Region("Behind the Vines", player, world) Locbehindvine_name = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"] - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: Locbehindvine_name += ["Pickaxe"] Regbehindvine.exits = [Entrance(player, "Wall Jump Entrance", Regbehindvine)] Regbehindvine.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbehindvine) for @@ -260,7 +261,7 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue Regcutcontent = Region("Cut Content", player, world) Loccutcontent_name = [] - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: Loccutcontent_name += ["Humble Indie Bindle"] Regcutcontent.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regcutcontent) for loc_name in Loccutcontent_name] @@ -269,7 +270,7 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue Regnamechange = Region("Name Change", player, world) Locnamechange_name = [] - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: Locnamechange_name += ["Box of Various Supplies"] Regnamechange.exits = [Entrance(player, "Behind Rocks", Regnamechange)] Regnamechange.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regnamechange) for diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py index d2495121f4e..c5fdfe8282c 100644 --- a/worlds/dlcquest/Rules.py +++ b/worlds/dlcquest/Rules.py @@ -1,10 +1,10 @@ import math import re -from .Locations import DLCQuestLocation -from ..generic.Rules import add_rule, set_rule, item_name_in_locations -from .Items import DLCQuestItem + from BaseClasses import ItemClassification +from worlds.generic.Rules import add_rule, item_name_in_locations, set_rule from . import Options +from .Items import DLCQuestItem def create_event(player, event: str): @@ -42,7 +42,7 @@ def has_coin(state, player: int, coins: int): def set_basic_rules(World_Options, has_enough_coin, player, world): - if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die: + if World_Options.campaign == Options.Campaign.option_live_freemium_or_die: return set_basic_entrance_rules(player, world) set_basic_self_obtained_items_rules(World_Options, player, world) @@ -66,12 +66,12 @@ def set_basic_entrance_rules(player, world): def set_basic_self_obtained_items_rules(World_Options, player, world): - if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_disabled: + if World_Options.item_shuffle != Options.ItemShuffle.option_disabled: return set_rule(world.get_entrance("Behind Ogre", player), lambda state: state.has("Gun Pack", player)) - if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required: + if World_Options.time_is_money == Options.TimeIsMoney.option_required: set_rule(world.get_entrance("Tree", player), lambda state: state.has("Time is Money Pack", player)) set_rule(world.get_entrance("Cave Tree", player), @@ -87,7 +87,7 @@ def set_basic_self_obtained_items_rules(World_Options, player, world): def set_basic_shuffled_items_rules(World_Options, player, world): - if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_shuffled: + if World_Options.item_shuffle != Options.ItemShuffle.option_shuffled: return set_rule(world.get_entrance("Behind Ogre", player), lambda state: state.has("Gun", player)) @@ -108,13 +108,13 @@ def set_basic_shuffled_items_rules(World_Options, player, world): set_rule(world.get_location("Gun", player), lambda state: state.has("Gun Pack", player)) - if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required: + if World_Options.time_is_money == Options.TimeIsMoney.option_required: set_rule(world.get_location("Sword", player), lambda state: state.has("Time is Money Pack", player)) def set_double_jump_glitchless_rules(World_Options, player, world): - if World_Options[Options.DoubleJumpGlitch] != Options.DoubleJumpGlitch.option_none: + if World_Options.double_jump_glitch != Options.DoubleJumpGlitch.option_none: return set_rule(world.get_entrance("Cloud Double Jump", player), lambda state: state.has("Double Jump Pack", player)) @@ -123,7 +123,7 @@ def set_double_jump_glitchless_rules(World_Options, player, world): def set_easy_double_jump_glitch_rules(World_Options, player, world): - if World_Options[Options.DoubleJumpGlitch] == Options.DoubleJumpGlitch.option_all: + if World_Options.double_jump_glitch == Options.DoubleJumpGlitch.option_all: return set_rule(world.get_entrance("Behind Tree Double Jump", player), lambda state: state.has("Double Jump Pack", player)) @@ -132,70 +132,70 @@ def set_easy_double_jump_glitch_rules(World_Options, player, world): def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world): - if World_Options[Options.CoinSanity] != Options.CoinSanity.option_coin: + if World_Options.coinsanity != Options.CoinSanity.option_coin: return - number_of_bundle = math.floor(825 / World_Options[Options.CoinSanityRange]) + number_of_bundle = math.floor(825 / World_Options.coinbundlequantity) for i in range(number_of_bundle): - item_coin = f"DLC Quest: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin" + item_coin = f"DLC Quest: {World_Options.coinbundlequantity * (i + 1)} Coin" set_rule(world.get_location(item_coin, player), - has_enough_coin(player, World_Options[Options.CoinSanityRange] * (i + 1))) - if 825 % World_Options[Options.CoinSanityRange] != 0: + has_enough_coin(player, World_Options.coinbundlequantity * (i + 1))) + if 825 % World_Options.coinbundlequantity != 0: set_rule(world.get_location("DLC Quest: 825 Coin", player), has_enough_coin(player, 825)) set_rule(world.get_location("Movement Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(4 / World_Options[Options.CoinSanityRange]))) + math.ceil(4 / World_Options.coinbundlequantity))) set_rule(world.get_location("Animation Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) + math.ceil(5 / World_Options.coinbundlequantity))) set_rule(world.get_location("Audio Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) + math.ceil(5 / World_Options.coinbundlequantity))) set_rule(world.get_location("Pause Menu Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) + math.ceil(5 / World_Options.coinbundlequantity))) set_rule(world.get_location("Time is Money Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(20 / World_Options[Options.CoinSanityRange]))) + math.ceil(20 / World_Options.coinbundlequantity))) set_rule(world.get_location("Double Jump Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(100 / World_Options[Options.CoinSanityRange]))) + math.ceil(100 / World_Options.coinbundlequantity))) set_rule(world.get_location("Pet Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) + math.ceil(5 / World_Options.coinbundlequantity))) set_rule(world.get_location("Sexy Outfits Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) + math.ceil(5 / World_Options.coinbundlequantity))) set_rule(world.get_location("Top Hat Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) + math.ceil(5 / World_Options.coinbundlequantity))) set_rule(world.get_location("Map Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(140 / World_Options[Options.CoinSanityRange]))) + math.ceil(140 / World_Options.coinbundlequantity))) set_rule(world.get_location("Gun Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(75 / World_Options[Options.CoinSanityRange]))) + math.ceil(75 / World_Options.coinbundlequantity))) set_rule(world.get_location("The Zombie Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) + math.ceil(5 / World_Options.coinbundlequantity))) set_rule(world.get_location("Night Map Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(75 / World_Options[Options.CoinSanityRange]))) + math.ceil(75 / World_Options.coinbundlequantity))) set_rule(world.get_location("Psychological Warfare Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(50 / World_Options[Options.CoinSanityRange]))) + math.ceil(50 / World_Options.coinbundlequantity))) set_rule(world.get_location("Armor for your Horse Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(250 / World_Options[Options.CoinSanityRange]))) + math.ceil(250 / World_Options.coinbundlequantity))) set_rule(world.get_location("Finish the Fight Pack", player), lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) + math.ceil(5 / World_Options.coinbundlequantity))) def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world): - if World_Options[Options.CoinSanity] != Options.CoinSanity.option_none: + if World_Options.coinsanity != Options.CoinSanity.option_none: return set_rule(world.get_location("Movement Pack", player), has_enough_coin(player, 4)) @@ -232,17 +232,17 @@ def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, def self_basic_win_condition(World_Options, player, world): - if World_Options[Options.EndingChoice] == Options.EndingChoice.option_any: + if World_Options.ending_choice == Options.EndingChoice.option_any: set_rule(world.get_location("Winning Basic", player), lambda state: state.has("Finish the Fight Pack", player)) - if World_Options[Options.EndingChoice] == Options.EndingChoice.option_true: + if World_Options.ending_choice == Options.EndingChoice.option_true: set_rule(world.get_location("Winning Basic", player), lambda state: state.has("Armor for your Horse Pack", player) and state.has("Finish the Fight Pack", player)) def set_lfod_rules(World_Options, has_enough_coin_freemium, player, world): - if World_Options[Options.Campaign] == Options.Campaign.option_basic: + if World_Options.campaign == Options.Campaign.option_basic: return set_lfod_entrance_rules(player, world) set_boss_door_requirements_rules(player, world) @@ -297,7 +297,7 @@ def set_boss_door_requirements_rules(player, world): def set_lfod_self_obtained_items_rules(World_Options, player, world): - if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_disabled: + if World_Options.item_shuffle != Options.ItemShuffle.option_disabled: return set_rule(world.get_entrance("Vines", player), lambda state: state.has("Incredibly Important Pack", player)) @@ -309,7 +309,7 @@ def set_lfod_self_obtained_items_rules(World_Options, player, world): def set_lfod_shuffled_items_rules(World_Options, player, world): - if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_shuffled: + if World_Options.item_shuffle != Options.ItemShuffle.option_shuffled: return set_rule(world.get_entrance("Vines", player), lambda state: state.has("Wooden Sword", player) or state.has("Pickaxe", player)) @@ -328,79 +328,79 @@ def set_lfod_shuffled_items_rules(World_Options, player, world): def self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world): - if World_Options[Options.CoinSanity] != Options.CoinSanity.option_coin: + if World_Options.coinsanity != Options.CoinSanity.option_coin: return - number_of_bundle = math.floor(889 / World_Options[Options.CoinSanityRange]) + number_of_bundle = math.floor(889 / World_Options.coinbundlequantity) for i in range(number_of_bundle): item_coin_freemium = "Live Freemium or Die: number Coin" - item_coin_loc_freemium = re.sub("number", str(World_Options[Options.CoinSanityRange] * (i + 1)), + item_coin_loc_freemium = re.sub("number", str(World_Options.coinbundlequantity * (i + 1)), item_coin_freemium) set_rule(world.get_location(item_coin_loc_freemium, player), - has_enough_coin_freemium(player, World_Options[Options.CoinSanityRange] * (i + 1))) - if 889 % World_Options[Options.CoinSanityRange] != 0: + has_enough_coin_freemium(player, World_Options.coinbundlequantity * (i + 1))) + if 889 % World_Options.coinbundlequantity != 0: set_rule(world.get_location("Live Freemium or Die: 889 Coin", player), has_enough_coin_freemium(player, 889)) add_rule(world.get_entrance("Boss Door", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(889 / World_Options[Options.CoinSanityRange]))) + math.ceil(889 / World_Options.coinbundlequantity))) set_rule(world.get_location("Particles Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) + math.ceil(5 / World_Options.coinbundlequantity))) set_rule(world.get_location("Day One Patch Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) + math.ceil(5 / World_Options.coinbundlequantity))) set_rule(world.get_location("Checkpoint Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) + math.ceil(5 / World_Options.coinbundlequantity))) set_rule(world.get_location("Incredibly Important Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(15 / World_Options[Options.CoinSanityRange]))) + math.ceil(15 / World_Options.coinbundlequantity))) set_rule(world.get_location("Wall Jump Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(35 / World_Options[Options.CoinSanityRange]))) + math.ceil(35 / World_Options.coinbundlequantity))) set_rule(world.get_location("Health Bar Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) + math.ceil(5 / World_Options.coinbundlequantity))) set_rule(world.get_location("Parallax Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) + math.ceil(5 / World_Options.coinbundlequantity))) set_rule(world.get_location("Harmless Plants Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(130 / World_Options[Options.CoinSanityRange]))) + math.ceil(130 / World_Options.coinbundlequantity))) set_rule(world.get_location("Death of Comedy Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(15 / World_Options[Options.CoinSanityRange]))) + math.ceil(15 / World_Options.coinbundlequantity))) set_rule(world.get_location("Canadian Dialog Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(10 / World_Options[Options.CoinSanityRange]))) + math.ceil(10 / World_Options.coinbundlequantity))) set_rule(world.get_location("DLC NPC Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(15 / World_Options[Options.CoinSanityRange]))) + math.ceil(15 / World_Options.coinbundlequantity))) set_rule(world.get_location("Cut Content Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(40 / World_Options[Options.CoinSanityRange]))) + math.ceil(40 / World_Options.coinbundlequantity))) set_rule(world.get_location("Name Change Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(150 / World_Options[Options.CoinSanityRange]))) + math.ceil(150 / World_Options.coinbundlequantity))) set_rule(world.get_location("Season Pass", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(199 / World_Options[Options.CoinSanityRange]))) + math.ceil(199 / World_Options.coinbundlequantity))) set_rule(world.get_location("High Definition Next Gen Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(20 / World_Options[Options.CoinSanityRange]))) + math.ceil(20 / World_Options.coinbundlequantity))) set_rule(world.get_location("Increased HP Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(10 / World_Options[Options.CoinSanityRange]))) + math.ceil(10 / World_Options.coinbundlequantity))) set_rule(world.get_location("Remove Ads Pack", player), lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(25 / World_Options[Options.CoinSanityRange]))) + math.ceil(25 / World_Options.coinbundlequantity))) def set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world): - if World_Options[Options.CoinSanity] != Options.CoinSanity.option_none: + if World_Options.coinsanity != Options.CoinSanity.option_none: return add_rule(world.get_entrance("Boss Door", player), has_enough_coin_freemium(player, 889)) @@ -442,10 +442,10 @@ def set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, def set_completion_condition(World_Options, player, world): - if World_Options[Options.Campaign] == Options.Campaign.option_basic: + if World_Options.campaign == Options.Campaign.option_basic: world.completion_condition[player] = lambda state: state.has("Victory Basic", player) - if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die: + if World_Options.campaign == Options.Campaign.option_live_freemium_or_die: world.completion_condition[player] = lambda state: state.has("Victory Freemium", player) - if World_Options[Options.Campaign] == Options.Campaign.option_both: + if World_Options.campaign == Options.Campaign.option_both: world.completion_condition[player] = lambda state: state.has("Victory Basic", player) and state.has( "Victory Freemium", player) diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index 9569d0efcc1..392eac7796f 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -1,12 +1,13 @@ -from typing import Dict, Any, Iterable, Optional, Union +from typing import Union + from BaseClasses import Tutorial -from worlds.AutoWorld import World, WebWorld -from .Items import DLCQuestItem, item_table, ItemData, create_items -from .Locations import location_table, DLCQuestLocation -from .Options import DLCQuest_options, DLCQuestOptions, fetch_options -from .Rules import set_rules -from .Regions import create_regions +from worlds.AutoWorld import WebWorld, World from . import Options +from .Items import DLCQuestItem, ItemData, create_items, item_table +from .Locations import DLCQuestLocation, location_table +from .Options import DLCQuestOptions +from .Regions import create_regions +from .Rules import set_rules client_version = 0 @@ -35,10 +36,8 @@ class DLCqworld(World): data_version = 1 - option_definitions = DLCQuest_options - - def generate_early(self): - self.options = fetch_options(self.multiworld, self.player) + options_dataclass = DLCQuestOptions + options: DLCQuestOptions def create_regions(self): create_regions(self.multiworld, self.player, self.options) @@ -68,8 +67,8 @@ def create_items(self): self.multiworld.itempool.remove(item) def precollect_coinsanity(self): - if self.options[Options.Campaign] == Options.Campaign.option_basic: - if self.options[Options.CoinSanity] == Options.CoinSanity.option_coin and self.options[Options.CoinSanityRange] >= 5: + if self.options.campaign == Options.Campaign.option_basic: + if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5: self.multiworld.push_precollected(self.create_item("Movement Pack")) @@ -80,12 +79,11 @@ def create_item(self, item: Union[str, ItemData]) -> DLCQuestItem: return DLCQuestItem(item.name, item.classification, item.code, self.player) def fill_slot_data(self): - return { - "death_link": self.multiworld.death_link[self.player].value, - "ending_choice": self.multiworld.ending_choice[self.player].value, - "campaign": self.multiworld.campaign[self.player].value, - "coinsanity": self.multiworld.coinsanity[self.player].value, - "coinbundlerange": self.multiworld.coinbundlequantity[self.player].value, - "item_shuffle": self.multiworld.item_shuffle[self.player].value, - "seed": self.multiworld.per_slot_randoms[self.player].randrange(99999999) - } + options_dict = self.options.as_dict( + "death_link", "ending_choice", "campaign", "coinsanity", "item_shuffle" + ) + options_dict.update({ + "coinbundlerange": self.options.coinbundlequantity.value, + "seed": self.random.randrange(99999999) + }) + return options_dict diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index c7fd2536329..4be699e9cf7 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -4,7 +4,7 @@ from BaseClasses import CollectionState, Item, ItemClassification, Tutorial from worlds.AutoWorld import WebWorld, World from .constants import ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER, NOTES, PHOBEKINS -from .options import Goal, Logic, NotesNeeded, PowerSeals, messenger_options +from .options import Goal, Logic, MessengerOptions, NotesNeeded, PowerSeals from .regions import MEGA_SHARDS, REGIONS, REGION_CONNECTIONS, SEALS from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules from .shop import FIGURINES, SHOP_ITEMS, shuffle_shop_prices @@ -44,7 +44,8 @@ class MessengerWorld(World): "Phobekin": set(PHOBEKINS), } - option_definitions = messenger_options + options_dataclass = MessengerOptions + options: MessengerOptions base_offset = 0xADD_000 item_name_to_id = {item: item_id @@ -74,9 +75,9 @@ class MessengerWorld(World): _filler_items: List[str] def generate_early(self) -> None: - if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: - self.multiworld.shuffle_seals[self.player].value = PowerSeals.option_true - self.total_seals = self.multiworld.total_seals[self.player].value + if self.options.goal == Goal.option_power_seal_hunt: + self.options.shuffle_seals.value = PowerSeals.option_true + self.total_seals = self.options.total_seals.value self.shop_prices, self.figurine_prices = shuffle_shop_prices(self) @@ -87,7 +88,7 @@ def create_regions(self) -> None: def create_items(self) -> None: # create items that are always in the item pool - itempool = [ + itempool: List[MessengerItem] = [ self.create_item(item) for item in self.item_name_to_id if item not in @@ -97,13 +98,13 @@ def create_items(self) -> None: } and "Time Shard" not in item ] - if self.multiworld.goal[self.player] == Goal.option_open_music_box: + if self.options.goal == Goal.option_open_music_box: # make a list of all notes except those in the player's defined starting inventory, and adjust the # amount we need to put in the itempool and precollect based on that notes = [note for note in NOTES if note not in self.multiworld.precollected_items[self.player]] self.random.shuffle(notes) precollected_notes_amount = NotesNeeded.range_end - \ - self.multiworld.notes_needed[self.player] - \ + self.options.notes_needed - \ (len(NOTES) - len(notes)) if precollected_notes_amount: for note in notes[:precollected_notes_amount]: @@ -111,15 +112,14 @@ def create_items(self) -> None: notes = notes[precollected_notes_amount:] itempool += [self.create_item(note) for note in notes] - elif self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: + elif self.options.goal == Goal.option_power_seal_hunt: total_seals = min(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool), - self.multiworld.total_seals[self.player].value) + self.options.total_seals.value) if total_seals < self.total_seals: logging.warning(f"Not enough locations for total seals setting " - f"({self.multiworld.total_seals[self.player].value}). Adjusting to {total_seals}") + f"({self.options.total_seals}). Adjusting to {total_seals}") self.total_seals = total_seals - self.required_seals =\ - int(self.multiworld.percent_seals_required[self.player].value / 100 * self.total_seals) + self.required_seals = int(self.options.percent_seals_required.value / 100 * self.total_seals) seals = [self.create_item("Power Seal") for _ in range(self.total_seals)] for i in range(self.required_seals): @@ -138,7 +138,7 @@ def create_items(self) -> None: self.multiworld.itempool += itempool def set_rules(self) -> None: - logic = self.multiworld.logic_level[self.player] + logic = self.options.logic_level if logic == Logic.option_normal: MessengerRules(self).set_messenger_rules() elif logic == Logic.option_hard: @@ -151,12 +151,12 @@ def fill_slot_data(self) -> Dict[str, Any]: figure_prices = {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()} return { - "deathlink": self.multiworld.death_link[self.player].value, - "goal": self.multiworld.goal[self.player].current_key, - "music_box": self.multiworld.music_box[self.player].value, + "deathlink": self.options.death_link.value, + "goal": self.options.goal.current_key, + "music_box": self.options.music_box.value, "required_seals": self.required_seals, - "mega_shards": self.multiworld.shuffle_shards[self.player].value, - "logic": self.multiworld.logic_level[self.player].current_key, + "mega_shards": self.options.shuffle_shards.value, + "logic": self.options.logic_level.current_key, "shop": shop_prices, "figures": figure_prices, "max_price": self.total_shards, @@ -175,7 +175,7 @@ def create_item(self, name: str) -> MessengerItem: item_id: Optional[int] = self.item_name_to_id.get(name, None) override_prog = getattr(self, "multiworld") is not None and \ name in {"Windmill Shuriken"} and \ - self.multiworld.logic_level[self.player] > Logic.option_normal + self.options.logic_level > Logic.option_normal count = 0 if "Time Shard " in name: count = int(name.strip("Time Shard ()")) diff --git a/worlds/messenger/constants.py b/worlds/messenger/constants.py index e6608be043b..f05d276ceaf 100644 --- a/worlds/messenger/constants.py +++ b/worlds/messenger/constants.py @@ -1,7 +1,7 @@ +from .shop import FIGURINES, SHOP_ITEMS + # items # listing individual groups first for easy lookup -from .shop import SHOP_ITEMS, FIGURINES - NOTES = [ "Key of Hope", "Key of Chaos", diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 8e8b61a2049..1da544bee70 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -1,7 +1,10 @@ +from dataclasses import dataclass from typing import Dict -from schema import Schema, Or, And, Optional -from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice, Toggle, OptionDict, StartInventoryPool +from schema import And, Optional, Or, Schema + +from Options import Accessibility, Choice, DeathLink, DefaultOnToggle, OptionDict, PerGameCommonOptions, Range, \ + StartInventoryPool, Toggle class MessengerAccessibility(Accessibility): @@ -129,18 +132,19 @@ class PlannedShopPrices(OptionDict): }) -messenger_options = { - "accessibility": MessengerAccessibility, - "start_inventory": StartInventoryPool, - "logic_level": Logic, - "shuffle_seals": PowerSeals, - "shuffle_shards": MegaShards, - "goal": Goal, - "music_box": MusicBox, - "notes_needed": NotesNeeded, - "total_seals": AmountSeals, - "percent_seals_required": RequiredSeals, - "shop_price": ShopPrices, - "shop_price_plan": PlannedShopPrices, - "death_link": DeathLink, -} +@dataclass +class MessengerOptions(PerGameCommonOptions): + accessibility: MessengerAccessibility + start_inventory: StartInventoryPool + logic_level: Logic + shuffle_seals: PowerSeals + shuffle_shards: MegaShards + goal: Goal + music_box: MusicBox + notes_needed: NotesNeeded + total_seals: AmountSeals + percent_seals_required: RequiredSeals + shop_price: ShopPrices + shop_price_plan: PlannedShopPrices + death_link: DeathLink + diff --git a/worlds/messenger/regions.py b/worlds/messenger/regions.py index 88579a9704f..28750b949ed 100644 --- a/worlds/messenger/regions.py +++ b/worlds/messenger/regions.py @@ -1,4 +1,4 @@ -from typing import Dict, Set, List +from typing import Dict, List, Set REGIONS: Dict[str, List[str]] = { "Menu": [], diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index 65a99627f22..c9bd9b86253 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -1,9 +1,9 @@ -from typing import Dict, Callable, TYPE_CHECKING +from typing import Callable, Dict, TYPE_CHECKING -from BaseClasses import CollectionState, MultiWorld -from worlds.generic.Rules import set_rule, allow_self_locking_items, add_rule -from .options import MessengerAccessibility, Goal +from BaseClasses import CollectionState +from worlds.generic.Rules import add_rule, allow_self_locking_items, set_rule from .constants import NOTES, PHOBEKINS +from .options import Goal, MessengerAccessibility from .subclasses import MessengerShopLocation if TYPE_CHECKING: @@ -145,13 +145,13 @@ def set_messenger_rules(self) -> None: if region.name == "The Shop": for loc in [location for location in region.locations if isinstance(location, MessengerShopLocation)]: loc.access_rule = loc.can_afford - if multiworld.goal[self.player] == Goal.option_power_seal_hunt: + if self.world.options.goal == Goal.option_power_seal_hunt: set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player), lambda state: state.has("Shop Chest", self.player)) multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player) if multiworld.accessibility[self.player] > MessengerAccessibility.option_locations: - set_self_locking_items(multiworld, self.player) + set_self_locking_items(self.world, self.player) class MessengerHardRules(MessengerRules): @@ -212,9 +212,9 @@ def has_windmill(self, state: CollectionState) -> bool: def set_messenger_rules(self) -> None: super().set_messenger_rules() for loc, rule in self.extra_rules.items(): - if not self.world.multiworld.shuffle_seals[self.player] and "Seal" in loc: + if not self.world.options.shuffle_seals and "Seal" in loc: continue - if not self.world.multiworld.shuffle_shards[self.player] and "Shard" in loc: + if not self.world.options.shuffle_shards and "Shard" in loc: continue add_rule(self.world.multiworld.get_location(loc, self.player), rule, "or") @@ -249,20 +249,22 @@ def __init__(self, world: MessengerWorld) -> None: def set_messenger_rules(self) -> None: super().set_messenger_rules() self.world.multiworld.completion_condition[self.player] = lambda state: True - self.world.multiworld.accessibility[self.player].value = MessengerAccessibility.option_minimal + self.world.options.accessibility.value = MessengerAccessibility.option_minimal -def set_self_locking_items(multiworld: MultiWorld, player: int) -> None: +def set_self_locking_items(world: MessengerWorld, player: int) -> None: + multiworld = world.multiworld + # do the ones for seal shuffle on and off first allow_self_locking_items(multiworld.get_location("Searing Crags - Key of Strength", player), "Power Thistle") allow_self_locking_items(multiworld.get_location("Sunken Shrine - Key of Love", player), "Sun Crest", "Moon Crest") allow_self_locking_items(multiworld.get_location("Corrupted Future - Key of Courage", player), "Demon King Crown") # add these locations when seals are shuffled - if multiworld.shuffle_seals[player]: + if world.options.shuffle_seals: allow_self_locking_items(multiworld.get_location("Elemental Skylands Seal - Water", player), "Currents Master") # add these locations when seals and shards aren't shuffled - elif not multiworld.shuffle_shards[player]: + elif not world.options.shuffle_shards: for entrance in multiworld.get_region("Cloud Ruins", player).entrances: entrance.access_rule = lambda state: state.has("Wingsuit", player) or state.has("Rope Dart", player) allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS) diff --git a/worlds/messenger/shop.py b/worlds/messenger/shop.py index f0915f5d02c..3c8c7bf6f21 100644 --- a/worlds/messenger/shop.py +++ b/worlds/messenger/shop.py @@ -74,8 +74,8 @@ class ShopData(NamedTuple): def shuffle_shop_prices(world: MessengerWorld) -> Tuple[Dict[str, int], Dict[str, int]]: - shop_price_mod = world.multiworld.shop_price[world.player].value - shop_price_planned = world.multiworld.shop_price_plan[world.player] + shop_price_mod = world.options.shop_price.value + shop_price_planned = world.options.shop_price_plan shop_prices: Dict[str, int] = {} figurine_prices: Dict[str, int] = {} diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index e6a18b2e4e7..c5d90e00c85 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -9,16 +9,15 @@ if TYPE_CHECKING: from . import MessengerWorld -else: - MessengerWorld = object class MessengerRegion(Region): - def __init__(self, name: str, world: MessengerWorld) -> None: + + def __init__(self, name: str, world: "MessengerWorld") -> None: super().__init__(name, world.player, world.multiworld) locations = [loc for loc in REGIONS[self.name]] if self.name == "The Shop": - if self.multiworld.goal[self.player] > Goal.option_open_music_box: + if world.options.goal > Goal.option_open_music_box: locations.append("Shop Chest") shop_locations = {f"The Shop - {shop_loc}": world.location_name_to_id[f"The Shop - {shop_loc}"] for shop_loc in SHOP_ITEMS} @@ -26,9 +25,9 @@ def __init__(self, name: str, world: MessengerWorld) -> None: self.add_locations(shop_locations, MessengerShopLocation) elif self.name == "Tower HQ": locations.append("Money Wrench") - if self.multiworld.shuffle_seals[self.player] and self.name in SEALS: + if world.options.shuffle_seals and self.name in SEALS: locations += [seal_loc for seal_loc in SEALS[self.name]] - if self.multiworld.shuffle_shards[self.player] and self.name in MEGA_SHARDS: + if world.options.shuffle_shards and self.name in MEGA_SHARDS: locations += [shard for shard in MEGA_SHARDS[self.name]] loc_dict = {loc: world.location_name_to_id[loc] if loc in world.location_name_to_id else None for loc in locations} @@ -49,7 +48,7 @@ class MessengerShopLocation(MessengerLocation): @cached_property def cost(self) -> int: name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped - world: MessengerWorld = self.parent_region.multiworld.worlds[self.player] + world = cast("MessengerWorld", self.parent_region.multiworld.worlds[self.player]) # short circuit figurines which all require demon's bane be purchased, but nothing else if "Figurine" in name: return world.figurine_prices[name] +\ @@ -70,9 +69,8 @@ def cost(self) -> int: return world.shop_prices[name] def can_afford(self, state: CollectionState) -> bool: - world: MessengerWorld = state.multiworld.worlds[self.player] - cost = self.cost - can_afford = state.has("Shards", self.player, min(cost, world.total_shards)) + world = cast("MessengerWorld", state.multiworld.worlds[self.player]) + can_afford = state.has("Shards", self.player, min(self.cost, world.total_shards)) if "Figurine" in self.name: can_afford = state.has("Money Wrench", self.player) and can_afford\ and state.can_reach("Money Wrench", "Location", self.player) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 655f4989b24..539abd96747 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -32,7 +32,7 @@ from Utils import get_options from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType -from Options import Range, Toggle, VerifyKeys +from Options import Range, Toggle, VerifyKeys, Accessibility from Fill import fill_restrictive, fast_fill, FillError from worlds.generic.Rules import exclusion_rules, add_item_rule from ..AutoWorld import World, AutoLogicRegister, WebWorld @@ -286,7 +286,7 @@ def generate_early(self): # No Logic forces all tricks on, prog balancing off and beatable-only elif self.logic_rules == 'no_logic': self.multiworld.progression_balancing[self.player].value = False - self.multiworld.accessibility[self.player] = self.multiworld.accessibility[self.player].from_text("minimal") + self.multiworld.accessibility[self.player].value = Accessibility.option_minimal for trick in normalized_name_tricks.values(): setattr(self, trick['name'], True) diff --git a/worlds/overcooked2/Options.py b/worlds/overcooked2/Options.py index 9ddcf5e85ff..18a2c18ed4f 100644 --- a/worlds/overcooked2/Options.py +++ b/worlds/overcooked2/Options.py @@ -1,6 +1,7 @@ +from dataclasses import dataclass from enum import IntEnum from typing import TypedDict -from Options import DefaultOnToggle, Toggle, Range, Choice, OptionSet +from Options import DefaultOnToggle, PerGameCommonOptions, Toggle, Range, Choice, OptionSet from .Overcooked2Levels import Overcooked2Dlc class LocationBalancingMode(IntEnum): @@ -167,32 +168,30 @@ class StarThresholdScale(Range): default = 35 -overcooked_options = { +@dataclass +class OC2Options(PerGameCommonOptions): # generator options - "location_balancing": LocationBalancing, - "ramp_tricks": RampTricks, - + location_balancing: LocationBalancing + ramp_tricks: RampTricks + # deathlink - "deathlink": DeathLink, - + deathlink: DeathLink + # randomization options - "shuffle_level_order": ShuffleLevelOrder, - "include_dlcs": DLCOptionSet, - "include_horde_levels": IncludeHordeLevels, - "prep_levels": PrepLevels, - "kevin_levels": KevinLevels, - + shuffle_level_order: ShuffleLevelOrder + include_dlcs: DLCOptionSet + include_horde_levels: IncludeHordeLevels + prep_levels: PrepLevels + kevin_levels: KevinLevels + # quality of life options - "fix_bugs": FixBugs, - "shorter_level_duration": ShorterLevelDuration, - "short_horde_levels": ShortHordeLevels, - "always_preserve_cooking_progress": AlwaysPreserveCookingProgress, - "always_serve_oldest_order": AlwaysServeOldestOrder, - "display_leaderboard_scores": DisplayLeaderboardScores, - + fix_bugs: FixBugs + shorter_level_duration: ShorterLevelDuration + short_horde_levels: ShortHordeLevels + always_preserve_cooking_progress: AlwaysPreserveCookingProgress + always_serve_oldest_order: AlwaysServeOldestOrder + display_leaderboard_scores: DisplayLeaderboardScores + # difficulty settings - "stars_to_win": StarsToWin, - "star_threshold_scale": StarThresholdScale, -} - -OC2Options = TypedDict("OC2Options", {option.__name__: option for option in overcooked_options.values()}) + stars_to_win: StarsToWin + star_threshold_scale: StarThresholdScale diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index afffa744fa8..2bf523b347c 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -6,7 +6,7 @@ from .Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, Overcooked2GenericLevel from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name -from .Options import overcooked_options, OC2Options, OC2OnToggle, LocationBalancingMode, DeathLinkMode +from .Options import OC2Options, OC2OnToggle, LocationBalancingMode, DeathLinkMode from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies, dlc_exclusives from .Logic import has_requirements_for_level_star, has_requirements_for_level_access, level_shuffle_factory, is_item_progression, is_useful @@ -47,7 +47,6 @@ class Overcooked2World(World): game = "Overcooked! 2" web = Overcooked2Web() required_client_version = (0, 3, 8) - option_definitions = overcooked_options topology_present: bool = False data_version = 3 @@ -57,13 +56,14 @@ class Overcooked2World(World): location_id_to_name = oc2_location_id_to_name location_name_to_id = oc2_location_name_to_id - options: Dict[str, Any] + options_dataclass = OC2Options + options: OC2Options itempool: List[Overcooked2Item] # Helper Functions def is_level_horde(self, level_id: int) -> bool: - return self.options["IncludeHordeLevels"] and \ + return self.options.include_horde_levels and \ (self.level_mapping is not None) and \ level_id in self.level_mapping.keys() and \ self.level_mapping[level_id].is_horde @@ -145,11 +145,6 @@ def add_level_location( location ) - def get_options(self) -> Dict[str, Any]: - return OC2Options({option.__name__: getattr(self.multiworld, name)[self.player].result - if issubclass(option, OC2OnToggle) else getattr(self.multiworld, name)[self.player].value - for name, option in overcooked_options.items()}) - def get_n_random_locations(self, n: int) -> List[int]: """Return a list of n random non-repeating level locations""" levels = list() @@ -160,7 +155,7 @@ def get_n_random_locations(self, n: int) -> List[int]: for level in Overcooked2Level(): if level.level_id == 36: continue - elif not self.options["KevinLevels"] and level.level_id > 36: + elif not self.options.kevin_levels and level.level_id > 36: break levels.append(level.level_id) @@ -231,26 +226,25 @@ def get_priority_locations(self) -> List[int]: def generate_early(self): self.player_name = self.multiworld.player_name[self.player] - self.options = self.get_options() # 0.0 to 1.0 where 1.0 is World Record - self.star_threshold_scale = self.options["StarThresholdScale"] / 100.0 + self.star_threshold_scale = self.options.star_threshold_scale / 100.0 # Parse DLCOptionSet back into enums - self.enabled_dlc = {Overcooked2Dlc(x) for x in self.options["DLCOptionSet"]} + self.enabled_dlc = {Overcooked2Dlc(x) for x in self.options.include_dlcs.value} # Generate level unlock requirements such that the levels get harder to unlock # the further the game has progressed, and levels progress radially rather than linearly - self.level_unlock_counts = level_unlock_requirement_factory(self.options["StarsToWin"]) + self.level_unlock_counts = level_unlock_requirement_factory(self.options.stars_to_win.value) # Assign new kitchens to each spot on the overworld using pure random chance and nothing else - if self.options["ShuffleLevelOrder"]: + if self.options.shuffle_level_order: self.level_mapping = \ level_shuffle_factory( self.multiworld.random, - self.options["PrepLevels"] != PrepLevelMode.excluded, - self.options["IncludeHordeLevels"], - self.options["KevinLevels"], + self.options.prep_levels != PrepLevelMode.excluded, + self.options.include_horde_levels.result, + self.options.kevin_levels.result, self.enabled_dlc, self.player_name, ) @@ -277,7 +271,7 @@ def create_regions(self) -> None: # Create and populate "regions" (a.k.a. levels) for level in Overcooked2Level(): - if not self.options["KevinLevels"] and level.level_id > 36: + if not self.options.kevin_levels and level.level_id > 36: break # Create Region (e.g. "1-1") @@ -336,7 +330,7 @@ def create_regions(self) -> None: level_access_rule: Callable[[CollectionState], bool] = \ lambda state, level_name=level.level_name, previous_level_completed_event_name=previous_level_completed_event_name, required_star_count=required_star_count: \ - has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.options["RampTricks"], self.player) + has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.options.ramp_tricks.result, self.player) self.connect_regions("Overworld", level.level_name, level_access_rule) # Level --> Overworld @@ -369,11 +363,11 @@ def create_items(self): # Item is always useless with these settings continue - if not self.options["IncludeHordeLevels"] and item_name in ["Calmer Unbread", "Coin Purse"]: + if not self.options.include_horde_levels and item_name in ["Calmer Unbread", "Coin Purse"]: # skip horde-specific items if no horde levels continue - if not self.options["KevinLevels"]: + if not self.options.kevin_levels: if item_name.startswith("Kevin"): # skip kevin items if no kevin levels continue @@ -382,7 +376,7 @@ def create_items(self): # skip dark green ramp if there's no Kevin-1 to reveal it continue - if is_item_progression(item_name, self.level_mapping, self.options["KevinLevels"]): + if is_item_progression(item_name, self.level_mapping, self.options.kevin_levels): # progression.append(item_name) classification = ItemClassification.progression else: @@ -404,7 +398,7 @@ def create_items(self): # Fill any free space with filler pool_count = len(oc2_location_name_to_id) - if not self.options["KevinLevels"]: + if not self.options.kevin_levels: pool_count -= 8 while len(self.itempool) < pool_count: @@ -416,7 +410,7 @@ def create_items(self): def place_events(self): # Add Events (Star Acquisition) for level in Overcooked2Level(): - if not self.options["KevinLevels"] and level.level_id > 36: + if not self.options.kevin_levels and level.level_id > 36: break if level.level_id != 36: @@ -449,7 +443,7 @@ def fill_json_data(self) -> Dict[str, Any]: # Serialize Level Order story_level_order = dict() - if self.options["ShuffleLevelOrder"]: + if self.options.shuffle_level_order: for level_id in self.level_mapping: level: Overcooked2GenericLevel = self.level_mapping[level_id] story_level_order[str(level_id)] = { @@ -481,7 +475,7 @@ def fill_json_data(self) -> Dict[str, Any]: level_unlock_requirements[str(level_id)] = level_id - 1 # Set Kevin Unlock Requirements - if self.options["KevinLevels"]: + if self.options.kevin_levels: def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]: location = self.multiworld.find_item(f"Kevin-{level_id-36}", self.player) if location.player != self.player: @@ -506,7 +500,7 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]: on_level_completed[level_id] = [item_to_unlock_event(location.item.name)] # Put it all together - star_threshold_scale = self.options["StarThresholdScale"] / 100 + star_threshold_scale = self.options.star_threshold_scale / 100 base_data = { # Changes Inherent to rando @@ -528,13 +522,13 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]: "SaveFolderName": mod_name, "CustomOrderTimeoutPenalty": 10, "LevelForceHide": [37, 38, 39, 40, 41, 42, 43, 44], - "LocalDeathLink": self.options["DeathLink"] != DeathLinkMode.disabled, - "BurnTriggersDeath": self.options["DeathLink"] == DeathLinkMode.death_and_overcook, + "LocalDeathLink": self.options.deathlink != DeathLinkMode.disabled, + "BurnTriggersDeath": self.options.deathlink == DeathLinkMode.death_and_overcook, # Game Modifications "LevelPurchaseRequirements": level_purchase_requirements, "Custom66TimerScale": max(0.4, 0.25 + (1.0 - star_threshold_scale)*0.6), - "ShortHordeLevels": self.options["ShortHordeLevels"], + "ShortHordeLevels": self.options.short_horde_levels, "CustomLevelOrder": custom_level_order, # Items (Starting Inventory) @@ -580,28 +574,27 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]: # Set remaining data in the options dict bugs = ["FixDoubleServing", "FixSinkBug", "FixControlStickThrowBug", "FixEmptyBurnerThrow"] for bug in bugs: - self.options[bug] = self.options["FixBugs"] - self.options["PreserveCookingProgress"] = self.options["AlwaysPreserveCookingProgress"] - self.options["TimerAlwaysStarts"] = self.options["PrepLevels"] == PrepLevelMode.ayce - self.options["LevelTimerScale"] = 0.666 if self.options["ShorterLevelDuration"] else 1.0 - self.options["LeaderboardScoreScale"] = { + base_data[bug] = self.options.fix_bugs.result + base_data["PreserveCookingProgress"] = self.options.always_preserve_cooking_progress.result + base_data["TimerAlwaysStarts"] = self.options.prep_levels == PrepLevelMode.ayce + base_data["LevelTimerScale"] = 0.666 if self.options.shorter_level_duration else 1.0 + base_data["LeaderboardScoreScale"] = { "FourStars": 1.0, "ThreeStars": star_threshold_scale, "TwoStars": star_threshold_scale * 0.75, "OneStar": star_threshold_scale * 0.35, } - base_data.update(self.options) return base_data def fill_slot_data(self) -> Dict[str, Any]: return self.fill_json_data() def write_spoiler(self, spoiler_handle: TextIO) -> None: - if not self.options["ShuffleLevelOrder"]: + if not self.options.shuffle_level_order: return - world: Overcooked2World = self.multiworld.worlds[self.player] + world: Overcooked2World = self spoiler_handle.write(f"\n\n{self.player_name}'s Level Order:\n\n") for overworld_id in world.level_mapping: overworld_name = Overcooked2GenericLevel(overworld_id).shortname.split("Story ")[1] diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py index cdd548d33f2..79739e85efc 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/Options.py @@ -1,5 +1,5 @@ -from typing import Dict -from Options import Option, Toggle, DefaultOnToggle, DeathLink, Range, Choice +from dataclasses import dataclass +from Options import Toggle, DefaultOnToggle, DeathLink, Range, Choice, PerGameCommonOptions # NOTE be aware that since the range of item ids that RoR2 uses is based off of the maximums of checks @@ -274,39 +274,40 @@ class ItemWeights(Choice): option_void = 9 -# define a dictionary for the weights of the generated item pool. -ror2_weights: Dict[str, type(Option)] = { - "green_scrap": GreenScrap, - "red_scrap": RedScrap, - "yellow_scrap": YellowScrap, - "white_scrap": WhiteScrap, - "common_item": CommonItem, - "uncommon_item": UncommonItem, - "legendary_item": LegendaryItem, - "boss_item": BossItem, - "lunar_item": LunarItem, - "void_item": VoidItem, - "equipment": Equipment -} - -ror2_options: Dict[str, type(Option)] = { - "goal": Goal, - "total_locations": TotalLocations, - "chests_per_stage": ChestsPerEnvironment, - "shrines_per_stage": ShrinesPerEnvironment, - "scavengers_per_stage": ScavengersPerEnvironment, - "scanner_per_stage": ScannersPerEnvironment, - "altars_per_stage": AltarsPerEnvironment, - "total_revivals": TotalRevivals, - "start_with_revive": StartWithRevive, - "final_stage_death": FinalStageDeath, - "begin_with_loop": BeginWithLoop, - "dlc_sotv": DLC_SOTV, - "death_link": DeathLink, - "item_pickup_step": ItemPickupStep, - "shrine_use_step": ShrineUseStep, - "enable_lunar": AllowLunarItems, - "item_weights": ItemWeights, - "item_pool_presets": ItemPoolPresetToggle, - **ror2_weights -} + + +# define a class for the weights of the generated item pool. +@dataclass +class ROR2Weights: + green_scrap: GreenScrap + red_scrap: RedScrap + yellow_scrap: YellowScrap + white_scrap: WhiteScrap + common_item: CommonItem + uncommon_item: UncommonItem + legendary_item: LegendaryItem + boss_item: BossItem + lunar_item: LunarItem + void_item: VoidItem + equipment: Equipment + +@dataclass +class ROR2Options(PerGameCommonOptions, ROR2Weights): + goal: Goal + total_locations: TotalLocations + chests_per_stage: ChestsPerEnvironment + shrines_per_stage: ShrinesPerEnvironment + scavengers_per_stage: ScavengersPerEnvironment + scanner_per_stage: ScannersPerEnvironment + altars_per_stage: AltarsPerEnvironment + total_revivals: TotalRevivals + start_with_revive: StartWithRevive + final_stage_death: FinalStageDeath + begin_with_loop: BeginWithLoop + dlc_sotv: DLC_SOTV + death_link: DeathLink + item_pickup_step: ItemPickupStep + shrine_use_step: ShrineUseStep + enable_lunar: AllowLunarItems + item_weights: ItemWeights + item_pool_presets: ItemPoolPresetToggle \ No newline at end of file diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 7c638f50b34..22c65dd9deb 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -6,7 +6,7 @@ from .RoR2Environments import * from BaseClasses import Region, Entrance, Item, ItemClassification, MultiWorld, Tutorial -from .Options import ror2_options, ItemWeights +from .Options import ItemWeights, ROR2Options from worlds.AutoWorld import World, WebWorld from .Regions import create_regions @@ -28,8 +28,9 @@ class RiskOfRainWorld(World): Combine loot in surprising ways and master each character until you become the havoc you feared upon your first crash landing. """ - game: str = "Risk of Rain 2" - option_definitions = ror2_options + game = "Risk of Rain 2" + options_dataclass = ROR2Options + options: ROR2Options topology_present = False item_name_to_id = item_table @@ -46,45 +47,44 @@ def __init__(self, multiworld: "MultiWorld", player: int): def generate_early(self) -> None: # figure out how many revivals should exist in the pool - if self.multiworld.goal[self.player] == "classic": - total_locations = self.multiworld.total_locations[self.player].value + if self.options.goal == "classic": + total_locations = self.options.total_locations.value else: total_locations = len( orderedstage_location.get_locations( - chests=self.multiworld.chests_per_stage[self.player].value, - shrines=self.multiworld.shrines_per_stage[self.player].value, - scavengers=self.multiworld.scavengers_per_stage[self.player].value, - scanners=self.multiworld.scanner_per_stage[self.player].value, - altars=self.multiworld.altars_per_stage[self.player].value, - dlc_sotv=self.multiworld.dlc_sotv[self.player].value + chests=self.options.chests_per_stage.value, + shrines=self.options.shrines_per_stage.value, + scavengers=self.options.scavengers_per_stage.value, + scanners=self.options.scanner_per_stage.value, + altars=self.options.altars_per_stage.value, + dlc_sotv=self.options.dlc_sotv.value ) ) - self.total_revivals = int(self.multiworld.total_revivals[self.player].value / 100 * + self.total_revivals = int(self.options.total_revivals.value / 100 * total_locations) - # self.total_revivals = self.multiworld.total_revivals[self.player].value - if self.multiworld.start_with_revive[self.player].value: + if self.options.start_with_revive: self.total_revivals -= 1 def create_items(self) -> None: # shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend - if self.multiworld.start_with_revive[self.player]: + if self.options.start_with_revive: self.multiworld.push_precollected(self.multiworld.create_item("Dio's Best Friend", self.player)) environments_pool = {} # only mess with the environments if they are set as items - if self.multiworld.goal[self.player] == "explore": + if self.options.goal == "explore": # figure out all available ordered stages for each tier environment_available_orderedstages_table = environment_vanilla_orderedstages_table - if self.multiworld.dlc_sotv[self.player]: + if self.options.dlc_sotv: environment_available_orderedstages_table = collapse_dict_list_vertical(environment_available_orderedstages_table, environment_sotv_orderedstages_table) environments_pool = shift_by_offset(environment_vanilla_table, environment_offest) - if self.multiworld.dlc_sotv[self.player]: + if self.options.dlc_sotv: environment_offset_table = shift_by_offset(environment_sotv_table, environment_offest) environments_pool = {**environments_pool, **environment_offset_table} - environments_to_precollect = 5 if self.multiworld.begin_with_loop[self.player].value else 1 + environments_to_precollect = 5 if self.options.begin_with_loop else 1 # percollect environments for each stage (or just stage 1) for i in range(environments_to_precollect): unlock = self.multiworld.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1) @@ -100,19 +100,19 @@ def create_items(self) -> None: for env_name, _ in environments_pool.items(): itempool += [env_name] - if self.multiworld.goal[self.player] == "classic": + if self.options.goal == "classic": # classic mode - total_locations = self.multiworld.total_locations[self.player].value + total_locations = self.options.total_locations.value else: # explore mode total_locations = len( orderedstage_location.get_locations( - chests=self.multiworld.chests_per_stage[self.player].value, - shrines=self.multiworld.shrines_per_stage[self.player].value, - scavengers=self.multiworld.scavengers_per_stage[self.player].value, - scanners=self.multiworld.scanner_per_stage[self.player].value, - altars=self.multiworld.altars_per_stage[self.player].value, - dlc_sotv=self.multiworld.dlc_sotv[self.player].value + chests=self.options.chests_per_stage.value, + shrines=self.options.shrines_per_stage.value, + scavengers=self.options.scavengers_per_stage.value, + scanners=self.options.scanner_per_stage.value, + altars=self.options.altars_per_stage.value, + dlc_sotv=self.options.dlc_sotv.value ) ) # Create junk items @@ -138,9 +138,9 @@ def get_filler_item_name(self) -> str: def create_junk_pool(self) -> Dict: # if presets are enabled generate junk_pool from the selected preset - pool_option = self.multiworld.item_weights[self.player].value + pool_option = self.options.item_weights.value junk_pool: Dict[str, int] = {} - if self.multiworld.item_pool_presets[self.player]: + if self.options.item_pool_presets: # generate chaos weights if the preset is chosen if pool_option == ItemWeights.option_chaos: for name, max_value in item_pool_weights[pool_option].items(): @@ -149,31 +149,31 @@ def create_junk_pool(self) -> Dict: junk_pool = item_pool_weights[pool_option].copy() else: # generate junk pool from user created presets junk_pool = { - "Item Scrap, Green": self.multiworld.green_scrap[self.player].value, - "Item Scrap, Red": self.multiworld.red_scrap[self.player].value, - "Item Scrap, Yellow": self.multiworld.yellow_scrap[self.player].value, - "Item Scrap, White": self.multiworld.white_scrap[self.player].value, - "Common Item": self.multiworld.common_item[self.player].value, - "Uncommon Item": self.multiworld.uncommon_item[self.player].value, - "Legendary Item": self.multiworld.legendary_item[self.player].value, - "Boss Item": self.multiworld.boss_item[self.player].value, - "Lunar Item": self.multiworld.lunar_item[self.player].value, - "Void Item": self.multiworld.void_item[self.player].value, - "Equipment": self.multiworld.equipment[self.player].value + "Item Scrap, Green": self.options.green_scrap.value, + "Item Scrap, Red": self.options.red_scrap.value, + "Item Scrap, Yellow": self.options.yellow_scrap.value, + "Item Scrap, White": self.options.white_scrap.value, + "Common Item": self.options.common_item.value, + "Uncommon Item": self.options.uncommon_item.value, + "Legendary Item": self.options.legendary_item.value, + "Boss Item": self.options.boss_item.value, + "Lunar Item": self.options.lunar_item.value, + "Void Item": self.options.void_item.value, + "Equipment": self.options.equipment.value } # remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled - if not (self.multiworld.enable_lunar[self.player] or pool_option == ItemWeights.option_lunartic): + if not (self.options.enable_lunar or pool_option == ItemWeights.option_lunartic): junk_pool.pop("Lunar Item") # remove void items from the pool - if not (self.multiworld.dlc_sotv[self.player] or pool_option == ItemWeights.option_void): + if not (self.options.dlc_sotv or pool_option == ItemWeights.option_void): junk_pool.pop("Void Item") return junk_pool def create_regions(self) -> None: - if self.multiworld.goal[self.player] == "classic": + if self.options.goal == "classic": # classic mode menu = create_region(self.multiworld, self.player, "Menu") self.multiworld.regions.append(menu) @@ -182,7 +182,7 @@ def create_regions(self) -> None: victory_region = create_region(self.multiworld, self.player, "Victory") self.multiworld.regions.append(victory_region) petrichor = create_region(self.multiworld, self.player, "Petrichor V", - get_classic_item_pickups(self.multiworld.total_locations[self.player].value)) + get_classic_item_pickups(self.options.total_locations.value)) self.multiworld.regions.append(petrichor) # classic mode can get to victory from the beginning of the game @@ -200,21 +200,13 @@ def create_regions(self) -> None: create_events(self.multiworld, self.player) def fill_slot_data(self): + options_dict = self.options.as_dict("item_pickup_step", "shrine_use_step", "goal", "total_locations", + "chests_per_stage", "shrines_per_stage", "scavengers_per_stage", + "scanner_per_stage", "altars_per_stage", "total_revivals", "start_with_revive", + "final_stage_death", "death_link", casing="camel") return { - "itemPickupStep": self.multiworld.item_pickup_step[self.player].value, - "shrineUseStep": self.multiworld.shrine_use_step[self.player].value, - "goal": self.multiworld.goal[self.player].value, + **options_dict, "seed": "".join(self.multiworld.per_slot_randoms[self.player].choice(string.digits) for _ in range(16)), - "totalLocations": self.multiworld.total_locations[self.player].value, - "chestsPerStage": self.multiworld.chests_per_stage[self.player].value, - "shrinesPerStage": self.multiworld.shrines_per_stage[self.player].value, - "scavengersPerStage": self.multiworld.scavengers_per_stage[self.player].value, - "scannerPerStage": self.multiworld.scanner_per_stage[self.player].value, - "altarsPerStage": self.multiworld.altars_per_stage[self.player].value, - "totalRevivals": self.multiworld.total_revivals[self.player].value, - "startWithDio": self.multiworld.start_with_revive[self.player].value, - "finalStageDeath": self.multiworld.final_stage_death[self.player].value, - "deathLink": self.multiworld.death_link[self.player].value, } def create_item(self, name: str) -> Item: @@ -241,12 +233,12 @@ def create_item(self, name: str) -> Item: def create_events(world: MultiWorld, player: int) -> None: - total_locations = world.total_locations[player].value + total_locations = world.worlds[player].options.total_locations.value num_of_events = total_locations // 25 if total_locations / 25 == num_of_events: num_of_events -= 1 world_region = world.get_region("Petrichor V", player) - if world.goal[player] == "classic": + if world.worlds[player].options.goal == "classic": # only setup Pickups when using classic_mode for i in range(num_of_events): event_loc = RiskOfRainLocation(player, f"Pickup{(i + 1) * 25}", None, world_region) @@ -254,7 +246,7 @@ def create_events(world: MultiWorld, player: int) -> None: event_loc.access_rule = \ lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", player) world_region.locations.append(event_loc) - elif world.goal[player] == "explore": + elif world.worlds[player].options.goal == "explore": for n in range(1, 6): event_region = world.get_region(f"OrderedStage_{n}", player) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 21e29868eda..9d6f28607ec 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -148,7 +148,7 @@ def generate_early(self): self.remote_items = self.multiworld.remote_items[self.player] if (len(self.variaRando.randoExec.setup.restrictedLocs) > 0): - self.multiworld.accessibility[self.player] = self.multiworld.accessibility[self.player].from_text("minimal") + self.multiworld.accessibility[self.player].value = Accessibility.option_minimal logger.warning(f"accessibility forced to 'minimal' for player {self.multiworld.get_player_name(self.player)} because of 'fun' settings") def create_items(self): diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 150e3589b07..1f46eb79d79 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -1,18 +1,20 @@ import logging -from typing import Dict, Any, Iterable, Optional, Union, Set +from typing import Dict, Any, Iterable, Optional, Union, Set, List from BaseClasses import Region, Entrance, Location, Item, Tutorial, CollectionState, ItemClassification, MultiWorld +from Options import PerGameCommonOptions from worlds.AutoWorld import World, WebWorld -from . import rules, logic, options +from . import rules from .bundles import get_all_bundles, Bundle from .items import item_table, create_items, ItemData, Group, items_by_group from .locations import location_table, create_locations, LocationData from .logic import StardewLogic, StardewRule, True_, MAX_MONTHS -from .options import stardew_valley_options, StardewOptions, fetch_options +from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, NumberOfLuckBuffs, NumberOfMovementBuffs, \ + BackpackProgression, BuildingProgression, ExcludeGingerIsland from .regions import create_regions from .rules import set_rules from worlds.generic.Rules import set_rule -from .strings.goal_names import Goal +from .strings.goal_names import Goal as GoalName client_version = 0 @@ -50,7 +52,6 @@ class StardewValleyWorld(World): befriend villagers, and uncover dark secrets. """ game = "Stardew Valley" - option_definitions = stardew_valley_options topology_present = False item_name_to_id = {name: data.code for name, data in item_table.items()} @@ -59,7 +60,8 @@ class StardewValleyWorld(World): data_version = 3 required_client_version = (0, 4, 0) - options: StardewOptions + options_dataclass = StardewValleyOptions + options: StardewValleyOptions logic: StardewLogic web = StardewWebWorld() @@ -72,25 +74,24 @@ def __init__(self, world: MultiWorld, player: int): self.all_progression_items = set() def generate_early(self): - self.options = fetch_options(self.multiworld, self.player) self.force_change_options_if_incompatible() self.logic = StardewLogic(self.player, self.options) self.modified_bundles = get_all_bundles(self.multiworld.random, self.logic, - self.options[options.BundleRandomization], - self.options[options.BundlePrice]) + self.options.bundle_randomization, + self.options.bundle_price) def force_change_options_if_incompatible(self): - goal_is_walnut_hunter = self.options[options.Goal] == options.Goal.option_greatest_walnut_hunter - goal_is_perfection = self.options[options.Goal] == options.Goal.option_perfection + goal_is_walnut_hunter = self.options.goal == Goal.option_greatest_walnut_hunter + goal_is_perfection = self.options.goal == Goal.option_perfection goal_is_island_related = goal_is_walnut_hunter or goal_is_perfection - exclude_ginger_island = self.options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true + exclude_ginger_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true if goal_is_island_related and exclude_ginger_island: - self.options[options.ExcludeGingerIsland] = options.ExcludeGingerIsland.option_false - goal = options.Goal.name_lookup[self.options[options.Goal]] + 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(f"Goal '{goal}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({player_name})") + logging.warning(f"Goal '{goal_name}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({player_name})") def create_regions(self): def create_region(name: str, exits: Iterable[str]) -> Region: @@ -116,7 +117,7 @@ def create_items(self): if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK, Group.FRIENDSHIP_PACK)] - if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled: + if self.options.season_randomization == SeasonRandomization.option_disabled: items_to_exclude = [item for item in items_to_exclude if item_table[item.name] not in items_by_group[Group.SEASON]] @@ -134,12 +135,12 @@ def create_items(self): self.setup_victory() def precollect_starting_season(self) -> Optional[StardewItem]: - if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive: + if self.options.season_randomization == SeasonRandomization.option_progressive: return season_pool = items_by_group[Group.SEASON] - if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled: + if self.options.season_randomization == SeasonRandomization.option_disabled: for season in season_pool: self.multiworld.push_precollected(self.create_item(season)) return @@ -148,18 +149,18 @@ def precollect_starting_season(self) -> Optional[StardewItem]: if item.name in {season.name for season in items_by_group[Group.SEASON]}]: return - if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_randomized_not_winter: + if self.options.season_randomization == SeasonRandomization.option_randomized_not_winter: season_pool = [season for season in season_pool if season.name != "Winter"] starting_season = self.create_item(self.multiworld.random.choice(season_pool)) self.multiworld.push_precollected(starting_season) def setup_early_items(self): - if (self.options[options.BuildingProgression] == - options.BuildingProgression.option_progressive_early_shipping_bin): + if (self.options.building_progression == + BuildingProgression.option_progressive_early_shipping_bin): self.multiworld.early_items[self.player]["Shipping Bin"] = 1 - if self.options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive: + if self.options.backpack_progression == BackpackProgression.option_early_progressive: self.multiworld.early_items[self.player]["Progressive Backpack"] = 1 def setup_month_events(self): @@ -172,40 +173,40 @@ def setup_month_events(self): self.create_event_location(month_end, self.logic.received("Month End", i).simplify(), "Month End") def setup_victory(self): - if self.options[options.Goal] == options.Goal.option_community_center: - self.create_event_location(location_table[Goal.community_center], + if self.options.goal == Goal.option_community_center: + self.create_event_location(location_table[GoalName.community_center], self.logic.can_complete_community_center().simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_grandpa_evaluation: - self.create_event_location(location_table[Goal.grandpa_evaluation], + elif self.options.goal == Goal.option_grandpa_evaluation: + self.create_event_location(location_table[GoalName.grandpa_evaluation], self.logic.can_finish_grandpa_evaluation().simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_bottom_of_the_mines: - self.create_event_location(location_table[Goal.bottom_of_the_mines], + elif self.options.goal == Goal.option_bottom_of_the_mines: + self.create_event_location(location_table[GoalName.bottom_of_the_mines], self.logic.can_mine_to_floor(120).simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_cryptic_note: - self.create_event_location(location_table[Goal.cryptic_note], + elif self.options.goal == Goal.option_cryptic_note: + self.create_event_location(location_table[GoalName.cryptic_note], self.logic.can_complete_quest("Cryptic Note").simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_master_angler: - self.create_event_location(location_table[Goal.master_angler], + elif self.options.goal == Goal.option_master_angler: + self.create_event_location(location_table[GoalName.master_angler], self.logic.can_catch_every_fish().simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_complete_collection: - self.create_event_location(location_table[Goal.complete_museum], + elif self.options.goal == Goal.option_complete_collection: + self.create_event_location(location_table[GoalName.complete_museum], self.logic.can_complete_museum().simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_full_house: - self.create_event_location(location_table[Goal.full_house], + elif self.options.goal == Goal.option_full_house: + self.create_event_location(location_table[GoalName.full_house], (self.logic.has_children(2) & self.logic.can_reproduce()).simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_greatest_walnut_hunter: - self.create_event_location(location_table[Goal.greatest_walnut_hunter], + elif self.options.goal == Goal.option_greatest_walnut_hunter: + self.create_event_location(location_table[GoalName.greatest_walnut_hunter], self.logic.has_walnut(130).simplify(), "Victory") - elif self.options[options.Goal] == options.Goal.option_perfection: - self.create_event_location(location_table[Goal.perfection], + elif self.options.goal == Goal.option_perfection: + self.create_event_location(location_table[GoalName.perfection], self.logic.has_everything(self.all_progression_items).simplify(), "Victory") @@ -230,7 +231,7 @@ def create_event_location(self, location_data: LocationData, rule: StardewRule, location.place_locked_item(self.create_item(item)) def set_rules(self): - set_rules(self.multiworld, self.player, self.options, self.logic, self.modified_bundles) + set_rules(self) self.force_first_month_once_all_early_items_are_found() def force_first_month_once_all_early_items_are_found(self): @@ -276,11 +277,12 @@ def fill_slot_data(self) -> Dict[str, Any]: key, value = self.modified_bundles[bundle_key].to_pair() modified_bundles[key] = value - excluded_options = [options.BundleRandomization, options.BundlePrice, - options.NumberOfMovementBuffs, options.NumberOfLuckBuffs] - slot_data = dict(self.options.options) - for option in excluded_options: - slot_data.pop(option.internal_name) + excluded_options = [BundleRandomization, BundlePrice, NumberOfMovementBuffs, NumberOfLuckBuffs] + excluded_option_names = [option.internal_name for option in excluded_options] + generic_option_names = [option_name for option_name in PerGameCommonOptions.type_hints] + excluded_option_names.extend(generic_option_names) + 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({ "seed": self.multiworld.per_slot_randoms[self.player].randrange(1000000000), # Seed should be max 9 digits "randomized_entrances": self.randomized_entrances, diff --git a/worlds/stardew_valley/bundles.py b/worlds/stardew_valley/bundles.py index 7cbb1392370..4af21542a4e 100644 --- a/worlds/stardew_valley/bundles.py +++ b/worlds/stardew_valley/bundles.py @@ -152,7 +152,7 @@ def parse_stardew_bundle_items(string_objects: str) -> List[BundleItem]: # shuffle_vault_amongst_themselves(random, bundles) -def get_all_bundles(random: Random, logic: StardewLogic, randomization: int, price: int) -> Dict[str, Bundle]: +def get_all_bundles(random: Random, logic: StardewLogic, randomization: BundleRandomization, price: BundlePrice) -> Dict[str, Bundle]: bundles = {} for bundle_key in vanilla_bundles: bundle_value = vanilla_bundles[bundle_key] diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 1035997034d..d5c71dae469 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -7,10 +7,11 @@ from typing import Dict, List, Protocol, Union, Set, Optional from BaseClasses import Item, ItemClassification -from . import options, data +from . import data from .data.villagers_data import all_villagers from .mods.mod_data import ModNames -from .options import StardewOptions +from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Cropsanity, Friendsanity, Museumsanity, \ + Fishsanity, BuildingProgression, SkillProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations from .strings.ap_names.buff_names import Buff ITEM_CODE_OFFSET = 717000 @@ -138,10 +139,9 @@ def initialize_item_table(): def create_items(item_factory: StardewItemFactory, locations_count: int, items_to_exclude: List[Item], - world_options: StardewOptions, - random: Random) -> List[Item]: + options: StardewValleyOptions, random: Random) -> List[Item]: items = [] - unique_items = create_unique_items(item_factory, world_options, random) + unique_items = create_unique_items(item_factory, options, random) for item in items_to_exclude: if item in unique_items: @@ -151,58 +151,58 @@ def create_items(item_factory: StardewItemFactory, locations_count: int, items_t items += unique_items logger.debug(f"Created {len(unique_items)} unique items") - unique_filler_items = create_unique_filler_items(item_factory, world_options, random, locations_count - len(items)) + unique_filler_items = create_unique_filler_items(item_factory, options, random, locations_count - len(items)) items += unique_filler_items logger.debug(f"Created {len(unique_filler_items)} unique filler items") - resource_pack_items = fill_with_resource_packs_and_traps(item_factory, world_options, random, items, locations_count) + resource_pack_items = fill_with_resource_packs_and_traps(item_factory, options, random, items, locations_count) items += resource_pack_items logger.debug(f"Created {len(resource_pack_items)} resource packs") return items -def create_unique_items(item_factory: StardewItemFactory, world_options: StardewOptions, random: Random) -> List[Item]: +def create_unique_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random) -> List[Item]: items = [] items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD]) - create_backpack_items(item_factory, world_options, items) + create_backpack_items(item_factory, options, items) create_mine_rewards(item_factory, items, random) - create_elevators(item_factory, world_options, items) - create_tools(item_factory, world_options, items) - create_skills(item_factory, world_options, items) - create_wizard_buildings(item_factory, world_options, items) - create_carpenter_buildings(item_factory, world_options, items) + create_elevators(item_factory, options, items) + create_tools(item_factory, options, items) + create_skills(item_factory, options, items) + create_wizard_buildings(item_factory, options, items) + create_carpenter_buildings(item_factory, options, items) items.append(item_factory("Beach Bridge")) items.append(item_factory("Dark Talisman")) create_tv_channels(item_factory, items) create_special_quest_rewards(item_factory, items) - create_stardrops(item_factory, world_options, items) - create_museum_items(item_factory, world_options, items) - create_arcade_machine_items(item_factory, world_options, items) + create_stardrops(item_factory, options, items) + create_museum_items(item_factory, options, items) + create_arcade_machine_items(item_factory, options, items) items.append(item_factory(random.choice(items_by_group[Group.GALAXY_WEAPONS]))) - create_player_buffs(item_factory, world_options, items) + create_player_buffs(item_factory, options, items) create_traveling_merchant_items(item_factory, items) items.append(item_factory("Return Scepter")) - create_seasons(item_factory, world_options, items) - create_seeds(item_factory, world_options, items) - create_friendsanity_items(item_factory, world_options, items) - create_festival_rewards(item_factory, world_options, items) + create_seasons(item_factory, options, items) + create_seeds(item_factory, options, items) + create_friendsanity_items(item_factory, options, items) + create_festival_rewards(item_factory, options, items) create_babies(item_factory, items, random) - create_special_order_board_rewards(item_factory, world_options, items) - create_special_order_qi_rewards(item_factory, world_options, items) - create_walnut_purchase_rewards(item_factory, world_options, items) - create_magic_mod_spells(item_factory, world_options, items) + create_special_order_board_rewards(item_factory, options, items) + create_special_order_qi_rewards(item_factory, options, items) + create_walnut_purchase_rewards(item_factory, options, items) + create_magic_mod_spells(item_factory, options, items) return items -def create_backpack_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if (world_options[options.BackpackProgression] == options.BackpackProgression.option_progressive or - world_options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive): +def create_backpack_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if (options.backpack_progression == BackpackProgression.option_progressive or + options.backpack_progression == BackpackProgression.option_early_progressive): items.extend(item_factory(item) for item in ["Progressive Backpack"] * 2) - if ModNames.big_backpack in world_options[options.Mods]: + if ModNames.big_backpack in options.mods: items.append(item_factory("Progressive Backpack")) @@ -220,46 +220,46 @@ def create_mine_rewards(item_factory: StardewItemFactory, items: List[Item], ran items.append(item_factory("Skull Key")) -def create_elevators(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if world_options[options.ElevatorProgression] == options.ElevatorProgression.option_vanilla: +def create_elevators(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.elevator_progression == ElevatorProgression.option_vanilla: return items.extend([item_factory(item) for item in ["Progressive Mine Elevator"] * 24]) - if ModNames.deepwoods in world_options[options.Mods]: + if ModNames.deepwoods in options.mods: items.extend([item_factory(item) for item in ["Progressive Woods Obelisk Sigils"] * 10]) - if ModNames.skull_cavern_elevator in world_options[options.Mods]: + if ModNames.skull_cavern_elevator in options.mods: items.extend([item_factory(item) for item in ["Progressive Skull Cavern Elevator"] * 8]) -def create_tools(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if world_options[options.ToolProgression] == options.ToolProgression.option_progressive: +def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.tool_progression == ToolProgression.option_progressive: items.extend(item_factory(item) for item in items_by_group[Group.PROGRESSIVE_TOOLS] * 4) items.append(item_factory("Golden Scythe")) -def create_skills(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if world_options[options.SkillProgression] == options.SkillProgression.option_progressive: +def create_skills(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.skill_progression == SkillProgression.option_progressive: for item in items_by_group[Group.SKILL_LEVEL_UP]: - if item.mod_name not in world_options[options.Mods] and item.mod_name is not None: + if item.mod_name not in options.mods and item.mod_name is not None: continue items.extend(item_factory(item) for item in [item.name] * 10) -def create_wizard_buildings(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): +def create_wizard_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): items.append(item_factory("Earth Obelisk")) items.append(item_factory("Water Obelisk")) items.append(item_factory("Desert Obelisk")) items.append(item_factory("Junimo Hut")) items.append(item_factory("Gold Clock")) - if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_false: + if options.exclude_ginger_island == ExcludeGingerIsland.option_false: items.append(item_factory("Island Obelisk")) - if ModNames.deepwoods in world_options[options.Mods]: + if ModNames.deepwoods in options.mods: items.append(item_factory("Woods Obelisk")) -def create_carpenter_buildings(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if world_options[options.BuildingProgression] in {options.BuildingProgression.option_progressive, - options.BuildingProgression.option_progressive_early_shipping_bin}: +def create_carpenter_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.building_progression in {BuildingProgression.option_progressive, + BuildingProgression.option_progressive_early_shipping_bin}: items.append(item_factory("Progressive Coop")) items.append(item_factory("Progressive Coop")) items.append(item_factory("Progressive Coop")) @@ -278,7 +278,7 @@ def create_carpenter_buildings(item_factory: StardewItemFactory, world_options: items.append(item_factory("Progressive House")) items.append(item_factory("Progressive House")) items.append(item_factory("Progressive House")) - if ModNames.tractor in world_options[options.Mods]: + if ModNames.tractor in options.mods: items.append(item_factory("Tractor Garage")) @@ -290,17 +290,17 @@ def create_special_quest_rewards(item_factory: StardewItemFactory, items: List[I items.append(item_factory("Iridium Snake Milk")) -def create_stardrops(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): +def create_stardrops(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): items.append(item_factory("Stardrop")) # The Mines level 100 items.append(item_factory("Stardrop")) # Old Master Cannoli - if world_options[options.Fishsanity] != options.Fishsanity.option_none: + if options.fishsanity != Fishsanity.option_none: items.append(item_factory("Stardrop")) #Master Angler Stardrop - if ModNames.deepwoods in world_options[options.Mods]: + if ModNames.deepwoods in options.mods: items.append(item_factory("Stardrop")) # Petting the Unicorn -def create_museum_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if world_options[options.Museumsanity] == options.Museumsanity.option_none: +def create_museum_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.museumsanity == Museumsanity.option_none: return items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 5) items.extend(item_factory(item) for item in ["Ancient Seeds"] * 5) @@ -311,17 +311,17 @@ def create_museum_items(item_factory: StardewItemFactory, world_options: Stardew items.append(item_factory("Dwarvish Translation Guide")) -def create_friendsanity_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if world_options[options.Friendsanity] == options.Friendsanity.option_none: +def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.friendsanity == Friendsanity.option_none: return - exclude_non_bachelors = world_options[options.Friendsanity] == options.Friendsanity.option_bachelors - exclude_locked_villagers = world_options[options.Friendsanity] == options.Friendsanity.option_starting_npcs or \ - world_options[options.Friendsanity] == options.Friendsanity.option_bachelors - include_post_marriage_hearts = world_options[options.Friendsanity] == options.Friendsanity.option_all_with_marriage - exclude_ginger_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true - heart_size = world_options[options.FriendsanityHeartSize] + exclude_non_bachelors = options.friendsanity == Friendsanity.option_bachelors + exclude_locked_villagers = options.friendsanity == Friendsanity.option_starting_npcs or \ + options.friendsanity == Friendsanity.option_bachelors + include_post_marriage_hearts = options.friendsanity == Friendsanity.option_all_with_marriage + exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true + heart_size = options.friendsanity_heart_size for villager in all_villagers: - if villager.mod_name not in world_options[options.Mods] and villager.mod_name is not None: + if villager.mod_name not in options.mods and villager.mod_name is not None: continue if not villager.available and exclude_locked_villagers: continue @@ -350,8 +350,8 @@ def create_babies(item_factory: StardewItemFactory, items: List[Item], random: R items.append(item_factory(chosen_baby)) -def create_arcade_machine_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling: +def create_arcade_machine_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.arcade_machine_locations == ArcadeMachineLocations.option_full_shuffling: items.append(item_factory("JotPK: Progressive Boots")) items.append(item_factory("JotPK: Progressive Boots")) items.append(item_factory("JotPK: Progressive Gun")) @@ -367,11 +367,9 @@ def create_arcade_machine_items(item_factory: StardewItemFactory, world_options: items.extend(item_factory(item) for item in ["Junimo Kart: Extra Life"] * 8) -def create_player_buffs(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): - number_of_movement_buffs: int = world_options[options.NumberOfMovementBuffs] - number_of_luck_buffs: int = world_options[options.NumberOfLuckBuffs] - items.extend(item_factory(item) for item in [Buff.movement] * number_of_movement_buffs) - items.extend(item_factory(item) for item in [Buff.luck] * number_of_luck_buffs) +def create_player_buffs(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + items.extend(item_factory(item) for item in [Buff.movement] * options.number_of_movement_buffs.value) + items.extend(item_factory(item) for item in [Buff.luck] * options.number_of_luck_buffs.value) def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]): @@ -380,36 +378,36 @@ def create_traveling_merchant_items(item_factory: StardewItemFactory, items: Lis *(item_factory(item) for item in ["Traveling Merchant Discount"] * 8)]) -def create_seasons(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if world_options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled: +def create_seasons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.season_randomization == SeasonRandomization.option_disabled: return - if world_options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive: + if options.season_randomization == SeasonRandomization.option_progressive: items.extend([item_factory(item) for item in ["Progressive Season"] * 3]) return items.extend([item_factory(item) for item in items_by_group[Group.SEASON]]) -def create_seeds(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if world_options[options.Cropsanity] == options.Cropsanity.option_disabled: +def create_seeds(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.cropsanity == Cropsanity.option_disabled: return - include_ginger_island = world_options[options.ExcludeGingerIsland] != options.ExcludeGingerIsland.option_true + include_ginger_island = options.exclude_ginger_island != ExcludeGingerIsland.option_true seed_items = [item_factory(item) for item in items_by_group[Group.CROPSANITY] if include_ginger_island or Group.GINGER_ISLAND not in item.groups] items.extend(seed_items) -def create_festival_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if world_options[options.FestivalLocations] == options.FestivalLocations.option_disabled: +def create_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.festival_locations == FestivalLocations.option_disabled: return items.extend([*[item_factory(item) for item in items_by_group[Group.FESTIVAL] if item.classification != ItemClassification.filler], item_factory("Stardrop")]) -def create_walnut_purchase_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true: +def create_walnut_purchase_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.exclude_ginger_island == ExcludeGingerIsland.option_true: return items.extend([item_factory("Boat Repair"), @@ -420,16 +418,16 @@ def create_walnut_purchase_rewards(item_factory: StardewItemFactory, world_optio -def create_special_order_board_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled: +def create_special_order_board_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.special_order_locations == SpecialOrderLocations.option_disabled: return items.extend([item_factory(item) for item in items_by_group[Group.SPECIAL_ORDER_BOARD]]) -def create_special_order_qi_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if (world_options[options.SpecialOrderLocations] != options.SpecialOrderLocations.option_board_qi or - world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true): +def create_special_order_qi_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if (options.special_order_locations != SpecialOrderLocations.option_board_qi or + options.exclude_ginger_island == ExcludeGingerIsland.option_true): return qi_gem_rewards = ["100 Qi Gems", "10 Qi Gems", "40 Qi Gems", "25 Qi Gems", "25 Qi Gems", "40 Qi Gems", "20 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems"] @@ -441,35 +439,35 @@ def create_tv_channels(item_factory: StardewItemFactory, items: List[Item]): items.extend([item_factory(item) for item in items_by_group[Group.TV_CHANNEL]]) -def create_filler_festival_rewards(item_factory: StardewItemFactory, world_options: StardewOptions) -> List[Item]: - if world_options[options.FestivalLocations] == options.FestivalLocations.option_disabled: +def create_filler_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions) -> List[Item]: + if options.festival_locations == FestivalLocations.option_disabled: return [] return [item_factory(item) for item in items_by_group[Group.FESTIVAL] if item.classification == ItemClassification.filler] -def create_magic_mod_spells(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): - if ModNames.magic not in world_options[options.Mods]: +def create_magic_mod_spells(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if ModNames.magic not in options.mods: return [] items.extend([item_factory(item) for item in items_by_group[Group.MAGIC_SPELL]]) -def create_unique_filler_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random, +def create_unique_filler_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random, available_item_slots: int) -> List[Item]: items = [] - items.extend(create_filler_festival_rewards(item_factory, world_options)) + items.extend(create_filler_festival_rewards(item_factory, options)) if len(items) > available_item_slots: items = random.sample(items, available_item_slots) return items -def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random, +def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random, items_already_added: List[Item], number_locations: int) -> List[Item]: - include_traps = world_options[options.TrapItems] != options.TrapItems.option_no_traps + include_traps = options.trap_items != TrapItems.option_no_traps all_filler_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK]] all_filler_packs.extend(items_by_group[Group.TRASH]) if include_traps: @@ -479,15 +477,15 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, world_o if pack.name not in items_already_added_names] trap_items = [pack for pack in items_by_group[Group.TRAP] if pack.name not in items_already_added_names and - (pack.mod_name is None or pack.mod_name in world_options[options.Mods])] + (pack.mod_name is None or pack.mod_name in options.mods)] priority_filler_items = [] priority_filler_items.extend(useful_resource_packs) if include_traps: priority_filler_items.extend(trap_items) - all_filler_packs = remove_excluded_packs(all_filler_packs, world_options) - priority_filler_items = remove_excluded_packs(priority_filler_items, world_options) + all_filler_packs = remove_excluded_packs(all_filler_packs, options) + priority_filler_items = remove_excluded_packs(priority_filler_items, options) number_priority_items = len(priority_filler_items) required_resource_pack = number_locations - len(items_already_added) @@ -521,8 +519,8 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, world_o return items -def remove_excluded_packs(packs, world_options): +def remove_excluded_packs(packs, options: StardewValleyOptions): included_packs = [pack for pack in packs if Group.DEPRECATED not in pack.groups] - if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true: + if options.exclude_ginger_island == ExcludeGingerIsland.option_true: included_packs = [pack for pack in included_packs if Group.GINGER_ISLAND not in pack.groups] return included_packs diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index 67bffa13963..345796b0311 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -4,10 +4,12 @@ from random import Random from typing import Optional, Dict, Protocol, List, FrozenSet -from . import options, data +from . import data +from .options import StardewValleyOptions from .data.fish_data import legendary_fish, special_fish, all_fish from .data.museum_data import all_museum_items from .data.villagers_data import all_villagers +from .options import ExcludeGingerIsland, Friendsanity, ArcadeMachineLocations, SpecialOrderLocations, Cropsanity, Fishsanity, Museumsanity, FestivalLocations, SkillProgression, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression from .strings.goal_names import Goal from .strings.region_names import Region @@ -133,12 +135,12 @@ def initialize_groups(): initialize_groups() -def extend_cropsanity_locations(randomized_locations: List[LocationData], world_options): - if world_options[options.Cropsanity] == options.Cropsanity.option_disabled: +def extend_cropsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): + if options.cropsanity == Cropsanity.option_disabled: return cropsanity_locations = locations_by_tag[LocationTags.CROPSANITY] - cropsanity_locations = filter_ginger_island(world_options, cropsanity_locations) + cropsanity_locations = filter_ginger_island(options, cropsanity_locations) randomized_locations.extend(cropsanity_locations) @@ -157,56 +159,56 @@ def extend_help_wanted_quests(randomized_locations: List[LocationData], desired_ randomized_locations.append(location_table[f"Help Wanted: Gathering {batch + 1}"]) -def extend_fishsanity_locations(randomized_locations: List[LocationData], world_options, random: Random): +def extend_fishsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): prefix = "Fishsanity: " - if world_options[options.Fishsanity] == options.Fishsanity.option_none: + if options.fishsanity == Fishsanity.option_none: return - elif world_options[options.Fishsanity] == options.Fishsanity.option_legendaries: + elif options.fishsanity == Fishsanity.option_legendaries: randomized_locations.extend(location_table[f"{prefix}{legendary.name}"] for legendary in legendary_fish) - elif world_options[options.Fishsanity] == options.Fishsanity.option_special: + elif options.fishsanity == Fishsanity.option_special: randomized_locations.extend(location_table[f"{prefix}{special.name}"] for special in special_fish) - elif world_options[options.Fishsanity] == options.Fishsanity.option_randomized: + elif options.fishsanity == Fishsanity.option_randomized: fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if random.random() < 0.4] - randomized_locations.extend(filter_ginger_island(world_options, fish_locations)) - elif world_options[options.Fishsanity] == options.Fishsanity.option_all: + randomized_locations.extend(filter_ginger_island(options, fish_locations)) + elif options.fishsanity == Fishsanity.option_all: fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish] - randomized_locations.extend(filter_ginger_island(world_options, fish_locations)) - elif world_options[options.Fishsanity] == options.Fishsanity.option_exclude_legendaries: + randomized_locations.extend(filter_ginger_island(options, fish_locations)) + elif options.fishsanity == Fishsanity.option_exclude_legendaries: fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if fish not in legendary_fish] - randomized_locations.extend(filter_ginger_island(world_options, fish_locations)) - elif world_options[options.Fishsanity] == options.Fishsanity.option_exclude_hard_fish: + randomized_locations.extend(filter_ginger_island(options, fish_locations)) + elif options.fishsanity == Fishsanity.option_exclude_hard_fish: fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if fish.difficulty < 80] - randomized_locations.extend(filter_ginger_island(world_options, fish_locations)) - elif world_options[options.Fishsanity] == options.Fishsanity.option_only_easy_fish: + randomized_locations.extend(filter_ginger_island(options, fish_locations)) + elif options.fishsanity == Fishsanity.option_only_easy_fish: fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if fish.difficulty < 50] - randomized_locations.extend(filter_ginger_island(world_options, fish_locations)) + randomized_locations.extend(filter_ginger_island(options, fish_locations)) -def extend_museumsanity_locations(randomized_locations: List[LocationData], museumsanity: int, random: Random): +def extend_museumsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): prefix = "Museumsanity: " - if museumsanity == options.Museumsanity.option_none: + if options.museumsanity == Museumsanity.option_none: return - elif museumsanity == options.Museumsanity.option_milestones: + elif options.museumsanity == Museumsanity.option_milestones: randomized_locations.extend(locations_by_tag[LocationTags.MUSEUM_MILESTONES]) - elif museumsanity == options.Museumsanity.option_randomized: + elif options.museumsanity == Museumsanity.option_randomized: randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"] for museum_item in all_museum_items if random.random() < 0.4) - elif museumsanity == options.Museumsanity.option_all: + elif options.museumsanity == Museumsanity.option_all: randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"] for museum_item in all_museum_items) -def extend_friendsanity_locations(randomized_locations: List[LocationData], world_options: options.StardewOptions): - if world_options[options.Friendsanity] == options.Friendsanity.option_none: +def extend_friendsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): + if options.friendsanity == Friendsanity.option_none: return - exclude_leo = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true - exclude_non_bachelors = world_options[options.Friendsanity] == options.Friendsanity.option_bachelors - exclude_locked_villagers = world_options[options.Friendsanity] == options.Friendsanity.option_starting_npcs or \ - world_options[options.Friendsanity] == options.Friendsanity.option_bachelors - include_post_marriage_hearts = world_options[options.Friendsanity] == options.Friendsanity.option_all_with_marriage - heart_size = world_options[options.FriendsanityHeartSize] + exclude_leo = options.exclude_ginger_island == ExcludeGingerIsland.option_true + exclude_non_bachelors = options.friendsanity == Friendsanity.option_bachelors + exclude_locked_villagers = options.friendsanity == Friendsanity.option_starting_npcs or \ + options.friendsanity == Friendsanity.option_bachelors + include_post_marriage_hearts = options.friendsanity == Friendsanity.option_all_with_marriage + heart_size = options.friendsanity_heart_size for villager in all_villagers: - if villager.mod_name not in world_options[options.Mods] and villager.mod_name is not None: + if villager.mod_name not in options.mods and villager.mod_name is not None: continue if not villager.available and exclude_locked_villagers: continue @@ -228,38 +230,38 @@ def extend_friendsanity_locations(randomized_locations: List[LocationData], worl randomized_locations.append(location_table[f"Friendsanity: Pet {heart} <3"]) -def extend_festival_locations(randomized_locations: List[LocationData], festival_option: int): - if festival_option == options.FestivalLocations.option_disabled: +def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): + if options.festival_locations == FestivalLocations.option_disabled: return festival_locations = locations_by_tag[LocationTags.FESTIVAL] randomized_locations.extend(festival_locations) - extend_hard_festival_locations(randomized_locations, festival_option) + extend_hard_festival_locations(randomized_locations, options) -def extend_hard_festival_locations(randomized_locations, festival_option: int): - if festival_option != options.FestivalLocations.option_hard: +def extend_hard_festival_locations(randomized_locations, options: StardewValleyOptions): + if options.festival_locations != FestivalLocations.option_hard: return hard_festival_locations = locations_by_tag[LocationTags.FESTIVAL_HARD] randomized_locations.extend(hard_festival_locations) -def extend_special_order_locations(randomized_locations: List[LocationData], world_options): - if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled: +def extend_special_order_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): + if options.special_order_locations == SpecialOrderLocations.option_disabled: return - include_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_false - board_locations = filter_disabled_locations(world_options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]) + include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false + board_locations = filter_disabled_locations(options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]) randomized_locations.extend(board_locations) - if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_board_qi and include_island: - include_arcade = world_options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_disabled + if options.special_order_locations == SpecialOrderLocations.option_board_qi and include_island: + include_arcade = options.arcade_machine_locations != ArcadeMachineLocations.option_disabled qi_orders = [location for location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI] if include_arcade or LocationTags.JUNIMO_KART not in location.tags] randomized_locations.extend(qi_orders) -def extend_walnut_purchase_locations(randomized_locations: List[LocationData], world_options): - if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true: +def extend_walnut_purchase_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): + if options.exclude_ginger_island == ExcludeGingerIsland.option_true: return randomized_locations.append(location_table["Repair Ticket Machine"]) randomized_locations.append(location_table["Repair Boat Hull"]) @@ -269,82 +271,82 @@ def extend_walnut_purchase_locations(randomized_locations: List[LocationData], w randomized_locations.extend(locations_by_tag[LocationTags.WALNUT_PURCHASE]) -def extend_mandatory_locations(randomized_locations: List[LocationData], world_options): +def extend_mandatory_locations(randomized_locations: List[LocationData], options): mandatory_locations = [location for location in locations_by_tag[LocationTags.MANDATORY]] - filtered_mandatory_locations = filter_disabled_locations(world_options, mandatory_locations) + filtered_mandatory_locations = filter_disabled_locations(options, mandatory_locations) randomized_locations.extend(filtered_mandatory_locations) -def extend_backpack_locations(randomized_locations: List[LocationData], world_options): - if world_options[options.BackpackProgression] == options.BackpackProgression.option_vanilla: +def extend_backpack_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): + if options.backpack_progression == BackpackProgression.option_vanilla: return backpack_locations = [location for location in locations_by_tag[LocationTags.BACKPACK]] - filtered_backpack_locations = filter_modded_locations(world_options, backpack_locations) + filtered_backpack_locations = filter_modded_locations(options, backpack_locations) randomized_locations.extend(filtered_backpack_locations) -def extend_elevator_locations(randomized_locations: List[LocationData], world_options): - if world_options[options.ElevatorProgression] == options.ElevatorProgression.option_vanilla: +def extend_elevator_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): + if options.elevator_progression == ElevatorProgression.option_vanilla: return elevator_locations = [location for location in locations_by_tag[LocationTags.ELEVATOR]] - filtered_elevator_locations = filter_modded_locations(world_options, elevator_locations) + filtered_elevator_locations = filter_modded_locations(options, elevator_locations) randomized_locations.extend(filtered_elevator_locations) def create_locations(location_collector: StardewLocationCollector, - world_options: options.StardewOptions, + options: StardewValleyOptions, random: Random): randomized_locations = [] - extend_mandatory_locations(randomized_locations, world_options) - extend_backpack_locations(randomized_locations, world_options) + extend_mandatory_locations(randomized_locations, options) + extend_backpack_locations(randomized_locations, options) - if not world_options[options.ToolProgression] == options.ToolProgression.option_vanilla: + if not options.tool_progression == ToolProgression.option_vanilla: randomized_locations.extend(locations_by_tag[LocationTags.TOOL_UPGRADE]) - extend_elevator_locations(randomized_locations, world_options) + extend_elevator_locations(randomized_locations, options) - if not world_options[options.SkillProgression] == options.SkillProgression.option_vanilla: + if not options.skill_progression == SkillProgression.option_vanilla: for location in locations_by_tag[LocationTags.SKILL_LEVEL]: - if location.mod_name is None or location.mod_name in world_options[options.Mods]: + if location.mod_name is None or location.mod_name in options.mods: randomized_locations.append(location_table[location.name]) - if not world_options[options.BuildingProgression] == options.BuildingProgression.option_vanilla: + if not options.building_progression == BuildingProgression.option_vanilla: for location in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]: - if location.mod_name is None or location.mod_name in world_options[options.Mods]: + if location.mod_name is None or location.mod_name in options.mods: randomized_locations.append(location_table[location.name]) - if world_options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_disabled: + if options.arcade_machine_locations != ArcadeMachineLocations.option_disabled: randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE_VICTORY]) - if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling: + if options.arcade_machine_locations == ArcadeMachineLocations.option_full_shuffling: randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE]) - extend_cropsanity_locations(randomized_locations, world_options) - extend_help_wanted_quests(randomized_locations, world_options[options.HelpWantedLocations]) - extend_fishsanity_locations(randomized_locations, world_options, random) - extend_museumsanity_locations(randomized_locations, world_options[options.Museumsanity], random) - extend_friendsanity_locations(randomized_locations, world_options) + extend_cropsanity_locations(randomized_locations, options) + extend_help_wanted_quests(randomized_locations, options.help_wanted_locations.value) + extend_fishsanity_locations(randomized_locations, options, random) + extend_museumsanity_locations(randomized_locations, options, random) + extend_friendsanity_locations(randomized_locations, options) - extend_festival_locations(randomized_locations, world_options[options.FestivalLocations]) - extend_special_order_locations(randomized_locations, world_options) - extend_walnut_purchase_locations(randomized_locations, world_options) + extend_festival_locations(randomized_locations, options) + extend_special_order_locations(randomized_locations, options) + extend_walnut_purchase_locations(randomized_locations, options) for location_data in randomized_locations: location_collector(location_data.name, location_data.code, location_data.region) -def filter_ginger_island(world_options: options.StardewOptions, locations: List[LocationData]) -> List[LocationData]: - include_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_false +def filter_ginger_island(options: StardewValleyOptions, locations: List[LocationData]) -> List[LocationData]: + include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false return [location for location in locations if include_island or LocationTags.GINGER_ISLAND not in location.tags] -def filter_modded_locations(world_options: options.StardewOptions, locations: List[LocationData]) -> List[LocationData]: - current_mod_names = world_options[options.Mods] +def filter_modded_locations(options: StardewValleyOptions, locations: List[LocationData]) -> List[LocationData]: + current_mod_names = options.mods return [location for location in locations if location.mod_name is None or location.mod_name in current_mod_names] -def filter_disabled_locations(world_options: options.StardewOptions, locations: List[LocationData]) -> List[LocationData]: - locations_first_pass = filter_ginger_island(world_options, locations) - locations_second_pass = filter_modded_locations(world_options, locations_first_pass) +def filter_disabled_locations(options: StardewValleyOptions, locations: List[LocationData]) -> List[LocationData]: + locations_first_pass = filter_ginger_island(options, locations) + locations_second_pass = filter_modded_locations(options, locations_first_pass) return locations_second_pass diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py index 377fa0d03ba..b2841d1566d 100644 --- a/worlds/stardew_valley/logic.py +++ b/worlds/stardew_valley/logic.py @@ -4,7 +4,6 @@ from dataclasses import dataclass, field from typing import Dict, Union, Optional, Iterable, Sized, List, Set -from . import options from .data import all_fish, FishItem, all_purchasable_seeds, SeedItem, all_crops, CropItem from .data.bundle_data import BundleItem from .data.crops_data import crops_by_name @@ -20,7 +19,8 @@ from .mods.logic.skullcavernelevator import has_skull_cavern_elevator_to_floor from .mods.mod_data import ModNames from .mods.logic import magic, skills -from .options import StardewOptions +from .options import Museumsanity, SeasonRandomization, StardewValleyOptions, BuildingProgression, SkillProgression, ToolProgression, Friendsanity, Cropsanity, \ + ExcludeGingerIsland, ElevatorProgression, ArcadeMachineLocations, FestivalLocations, SpecialOrderLocations from .regions import vanilla_regions from .stardew_rule import False_, Reach, Or, True_, Received, Count, And, Has, TotalReceived, StardewRule from .strings.animal_names import Animal, coop_animals, barn_animals @@ -81,10 +81,11 @@ fishing_regions = [Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west] + @dataclass(frozen=True, repr=False) class StardewLogic: player: int - options: StardewOptions + options: StardewValleyOptions item_rules: Dict[str, StardewRule] = field(default_factory=dict) sapling_rules: Dict[str, StardewRule] = field(default_factory=dict) @@ -398,7 +399,7 @@ def __post_init__(self): Building.cellar: self.can_spend_money_at(Region.carpenter, 100000) & self.has_house(2), }) - self.building_rules.update(get_modded_building_rules(self, self.options[options.Mods])) + self.building_rules.update(get_modded_building_rules(self, self.options.mods)) self.quest_rules.update({ Quest.introductions: self.can_reach_region(Region.town), @@ -455,7 +456,7 @@ def __post_init__(self): self.can_meet(NPC.wizard) & self.can_meet(NPC.willy), }) - self.quest_rules.update(get_modded_quest_rules(self, self.options[options.Mods])) + self.quest_rules.update(get_modded_quest_rules(self, self.options.mods)) self.festival_rules.update({ FestivalCheck.egg_hunt: self.has_season(Season.spring) & self.can_reach_region(Region.town) & self.can_win_egg_hunt(), @@ -539,7 +540,7 @@ def __post_init__(self): self.can_spend_money(80000), # I need this extra rule because money rules aren't additive... }) - self.special_order_rules.update(get_modded_special_orders_rules(self, self.options[options.Mods])) + self.special_order_rules.update(get_modded_special_orders_rules(self, self.options.mods)) def has(self, items: Union[str, (Iterable[str], Sized)], count: Optional[int] = None) -> StardewRule: if isinstance(items, str): @@ -596,7 +597,7 @@ def can_have_earned_total_money(self, amount: int) -> StardewRule: return self.has_lived_months(min(8, amount // MONEY_PER_MONTH)) def can_spend_money(self, amount: int) -> StardewRule: - if self.options[options.StartingMoney] == -1: + if self.options.starting_money == -1: return True_() return self.has_lived_months(min(8, amount // (MONEY_PER_MONTH // 5))) @@ -607,7 +608,7 @@ def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule if material == ToolMaterial.basic or tool == Tool.scythe: return True_() - if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + if self.options.tool_progression == ToolProgression.option_progressive: return self.received(f"Progressive {tool}", count=tool_materials[material]) return self.has(f"{material} Bar") & self.can_spend_money(tool_upgrade_prices[material]) @@ -644,7 +645,7 @@ def has_skill_level(self, skill: str, level: int) -> StardewRule: if level <= 0: return True_() - if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + if self.options.skill_progression == SkillProgression.option_progressive: return self.received(f"{skill} Level", count=level) return self.can_earn_skill_level(skill, level) @@ -656,7 +657,7 @@ def has_total_skill_level(self, level: int, allow_modded_skills: bool = False) - if level <= 0: return True_() - if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + if self.options.skill_progression == SkillProgression.option_progressive: skills_items = ["Farming Level", "Mining Level", "Foraging Level", "Fishing Level", "Combat Level"] if allow_modded_skills: @@ -672,7 +673,7 @@ def has_total_skill_level(self, level: int, allow_modded_skills: bool = False) - def has_building(self, building: str) -> StardewRule: carpenter_rule = self.can_reach_region(Region.carpenter) - if not self.options[options.BuildingProgression] == options.BuildingProgression.option_vanilla: + if not self.options.building_progression == BuildingProgression.option_vanilla: count = 1 if building in [Building.coop, Building.barn, Building.shed]: building = f"Progressive {building}" @@ -693,7 +694,7 @@ def has_house(self, upgrade_level: int) -> StardewRule: if upgrade_level > 3: return False_() - if not self.options[options.BuildingProgression] == options.BuildingProgression.option_vanilla: + if not self.options.building_progression == BuildingProgression.option_vanilla: return self.received(f"Progressive House", upgrade_level) & self.can_reach_region(Region.carpenter) if upgrade_level == 1: @@ -734,7 +735,7 @@ def can_get_combat_xp(self) -> StardewRule: return tool_rule & enemy_rule def can_get_fishing_xp(self) -> StardewRule: - if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + if self.options.skill_progression == SkillProgression.option_progressive: return self.can_fish() | self.can_crab_pot() return self.can_fish() @@ -746,7 +747,7 @@ def can_fish(self, difficulty: int = 0) -> StardewRule: skill_rule = self.has_skill_level(Skill.fishing, skill_required) region_rule = self.can_reach_any_region(fishing_regions) number_fishing_rod_required = 1 if difficulty < 50 else 2 - if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + if self.options.tool_progression == ToolProgression.option_progressive: return self.received("Progressive Fishing Rod", number_fishing_rod_required) & skill_rule & region_rule return skill_rule & region_rule @@ -763,7 +764,7 @@ def can_fish_chests(self) -> StardewRule: return self.has_max_fishing_rod() & skill_rule def can_buy_seed(self, seed: SeedItem) -> StardewRule: - if self.options[options.Cropsanity] == options.Cropsanity.option_disabled: + if self.options.cropsanity == Cropsanity.option_disabled: item_rule = True_() else: item_rule = self.received(seed.name) @@ -781,7 +782,7 @@ def can_buy_sapling(self, fruit: str) -> StardewRule: Fruit.peach: 6000, Fruit.pomegranate: 6000, Fruit.banana: 0, Fruit.mango: 0} received_sapling = self.received(f"{fruit} Sapling") - if self.options[options.Cropsanity] == options.Cropsanity.option_disabled: + if self.options.cropsanity == Cropsanity.option_disabled: allowed_buy_sapling = True_() else: allowed_buy_sapling = received_sapling @@ -824,14 +825,14 @@ def can_catch_fish(self, fish: FishItem) -> StardewRule: def can_catch_every_fish(self) -> StardewRule: rules = [self.has_skill_level(Skill.fishing, 10), self.has_max_fishing_rod()] for fish in all_fish: - if self.options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true and \ + if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true and \ fish in island_fish: continue rules.append(self.can_catch_fish(fish)) return And(rules) def has_max_fishing_rod(self) -> StardewRule: - if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + if self.options.tool_progression == ToolProgression.option_progressive: return self.received(APTool.fishing_rod, 4) return self.can_get_fishing_xp() @@ -875,7 +876,7 @@ def can_do_panning(self, item: str = Generic.any) -> StardewRule: def can_crab_pot(self, region: str = Generic.any) -> StardewRule: crab_pot_rule = self.has(Craftable.bait) - if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + if self.options.skill_progression == SkillProgression.option_progressive: crab_pot_rule = crab_pot_rule & self.has(Machine.crab_pot) else: crab_pot_rule = crab_pot_rule & self.can_get_fishing_xp() @@ -926,9 +927,7 @@ def can_chop_perfectly(self) -> StardewRule: return region_rule & ((tool_rule & foraging_rule) | magic_rule) def has_max_buffs(self) -> StardewRule: - number_of_movement_buffs: int = self.options[options.NumberOfMovementBuffs] - number_of_luck_buffs: int = self.options[options.NumberOfLuckBuffs] - return self.received(Buff.movement, number_of_movement_buffs) & self.received(Buff.luck, number_of_luck_buffs) + return self.received(Buff.movement, self.options.number_of_movement_buffs.value) & self.received(Buff.luck, self.options.number_of_luck_buffs.value) def get_weapon_rule_for_floor_tier(self, tier: int): if tier >= 4: @@ -946,9 +945,9 @@ def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule: rules = [] weapon_rule = self.get_weapon_rule_for_floor_tier(tier) rules.append(weapon_rule) - if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + if self.options.tool_progression == ToolProgression.option_progressive: rules.append(self.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier])) - if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + if self.options.skill_progression == SkillProgression.option_progressive: combat_tier = min(10, max(0, tier * 2)) rules.append(self.has_skill_level(Skill.combat, combat_tier)) return And(rules) @@ -958,15 +957,15 @@ def can_progress_easily_in_the_mines_from_floor(self, floor: int) -> StardewRule rules = [] weapon_rule = self.get_weapon_rule_for_floor_tier(tier) rules.append(weapon_rule) - if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + if self.options.tool_progression == ToolProgression.option_progressive: rules.append(self.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier])) - if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + if self.options.skill_progression == SkillProgression.option_progressive: combat_tier = min(10, max(0, tier * 2)) rules.append(self.has_skill_level(Skill.combat, combat_tier)) return And(rules) def has_mine_elevator_to_floor(self, floor: int) -> StardewRule: - if self.options[options.ElevatorProgression] != options.ElevatorProgression.option_vanilla: + if self.options.elevator_progression != ElevatorProgression.option_vanilla: return self.received("Progressive Mine Elevator", count=int(floor / 5)) return True_() @@ -984,9 +983,9 @@ def can_progress_in_the_skull_cavern_from_floor(self, floor: int) -> StardewRule weapon_rule = self.has_great_weapon() rules.append(weapon_rule) rules.append(self.can_cook()) - if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + if self.options.tool_progression == ToolProgression.option_progressive: rules.append(self.received("Progressive Pickaxe", min(4, max(0, tier + 2)))) - if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + if self.options.skill_progression == SkillProgression.option_progressive: skill_tier = min(10, max(0, tier * 2 + 6)) rules.extend({self.has_skill_level(Skill.combat, skill_tier), self.has_skill_level(Skill.mining, skill_tier)}) @@ -1005,20 +1004,20 @@ def can_mine_to_skull_cavern_floor(self, floor: int) -> StardewRule: self.can_progress_easily_in_the_skull_cavern_from_floor(previous_previous_elevator))) & has_mine_elevator def has_jotpk_power_level(self, power_level: int) -> StardewRule: - if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling: + if self.options.arcade_machine_locations != ArcadeMachineLocations.option_full_shuffling: return True_() jotpk_buffs = ["JotPK: Progressive Boots", "JotPK: Progressive Gun", "JotPK: Progressive Ammo", "JotPK: Extra Life", "JotPK: Increased Drop Rate"] return self.received(jotpk_buffs, power_level) def has_junimo_kart_power_level(self, power_level: int) -> StardewRule: - if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling: + if self.options.arcade_machine_locations != ArcadeMachineLocations.option_full_shuffling: return True_() return self.received("Junimo Kart: Extra Life", power_level) def has_junimo_kart_max_level(self) -> StardewRule: play_rule = self.can_reach_region(Region.junimo_kart_3) - if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling: + if self.options.arcade_machine_locations != ArcadeMachineLocations.option_full_shuffling: return play_rule return self.has_junimo_kart_power_level(8) @@ -1043,12 +1042,12 @@ def can_reproduce(self, number_children: int = 1) -> StardewRule: def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule: if hearts <= 0: return True_() - friendsanity = self.options[options.Friendsanity] - if friendsanity == options.Friendsanity.option_none: + friendsanity = self.options.friendsanity + if friendsanity == Friendsanity.option_none: return self.can_earn_relationship(npc, hearts) if npc not in all_villagers_by_name: if npc == NPC.pet: - if friendsanity == options.Friendsanity.option_bachelors: + if friendsanity == Friendsanity.option_bachelors: return self.can_befriend_pet(hearts) return self.received_hearts(NPC.pet, hearts) if npc == Generic.any or npc == Generic.bachelor: @@ -1078,11 +1077,11 @@ def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule: if not self.npc_is_in_current_slot(npc): return True_() villager = all_villagers_by_name[npc] - if friendsanity == options.Friendsanity.option_bachelors and not villager.bachelor: + if friendsanity == Friendsanity.option_bachelors and not villager.bachelor: return self.can_earn_relationship(npc, hearts) - if friendsanity == options.Friendsanity.option_starting_npcs and not villager.available: + if friendsanity == Friendsanity.option_starting_npcs and not villager.available: return self.can_earn_relationship(npc, hearts) - is_capped_at_8 = villager.bachelor and friendsanity != options.Friendsanity.option_all_with_marriage + is_capped_at_8 = villager.bachelor and friendsanity != Friendsanity.option_all_with_marriage if is_capped_at_8 and hearts > 8: return self.received_hearts(villager, 8) & self.can_earn_relationship(npc, hearts) return self.received_hearts(villager, hearts) @@ -1090,7 +1089,7 @@ def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule: def received_hearts(self, npc: Union[str, Villager], hearts: int) -> StardewRule: if isinstance(npc, Villager): return self.received_hearts(npc.name, hearts) - heart_size: int = self.options[options.FriendsanityHeartSize] + heart_size = self.options.friendsanity_heart_size.value return self.received(self.heart(npc), math.ceil(hearts / heart_size)) def can_meet(self, npc: str) -> StardewRule: @@ -1122,13 +1121,13 @@ def can_earn_relationship(self, npc: str, hearts: int = 0) -> StardewRule: if hearts <= 0: return True_() - heart_size: int = self.options[options.FriendsanityHeartSize] + heart_size = self.options.friendsanity_heart_size.value previous_heart = hearts - heart_size previous_heart_rule = self.has_relationship(npc, previous_heart) if npc == NPC.pet: earn_rule = self.can_befriend_pet(hearts) - elif npc == NPC.wizard and ModNames.magic in self.options[options.Mods]: + elif npc == NPC.wizard and ModNames.magic in self.options.mods: earn_rule = self.can_meet(npc) & self.has_lived_months(hearts) elif npc in all_villagers_by_name: if not self.npc_is_in_current_slot(npc): @@ -1284,7 +1283,7 @@ def has_year_three(self) -> StardewRule: return self.has_lived_months(8) def can_speak_dwarf(self) -> StardewRule: - if self.options[options.Museumsanity] == options.Museumsanity.option_none: + if self.options.museumsanity == Museumsanity.option_none: return And([self.can_donate_museum_item(item) for item in dwarf_scrolls]) return self.received("Dwarvish Translation Guide") @@ -1334,7 +1333,7 @@ def can_find_museum_items(self, number: int) -> StardewRule: def can_complete_museum(self) -> StardewRule: rules = [self.can_reach_region(Region.museum), self.can_mine_perfectly()] - if self.options[options.Museumsanity] != options.Museumsanity.option_none: + if self.options.museumsanity != Museumsanity.option_none: rules.append(self.received("Traveling Merchant Metal Detector", 4)) for donation in all_museum_items: @@ -1345,9 +1344,9 @@ def has_season(self, season: str) -> StardewRule: if season == Generic.any: return True_() seasons_order = [Season.spring, Season.summer, Season.fall, Season.winter] - if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive: + if self.options.season_randomization == SeasonRandomization.option_progressive: return self.received(Season.progressive, seasons_order.index(season)) - if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled: + if self.options.season_randomization == SeasonRandomization.option_disabled: if season == Season.spring: return True_() return self.has_lived_months(1) @@ -1371,19 +1370,19 @@ def has_lived_months(self, number: int) -> StardewRule: return self.received("Month End", number) def has_rusty_key(self) -> StardewRule: - if self.options[options.Museumsanity] == options.Museumsanity.option_none: + if self.options.museumsanity == Museumsanity.option_none: required_donations = 80 # It's 60, but without a metal detector I'd rather overshoot so players don't get screwed by RNG return self.has([item.name for item in all_museum_items], required_donations) & self.can_reach_region(Region.museum) return self.received(Wallet.rusty_key) def can_win_egg_hunt(self) -> StardewRule: - number_of_movement_buffs: int = self.options[options.NumberOfMovementBuffs] - if self.options[options.FestivalLocations] == options.FestivalLocations.option_hard or number_of_movement_buffs < 2: + number_of_movement_buffs = self.options.number_of_movement_buffs.value + if self.options.festival_locations == FestivalLocations.option_hard or number_of_movement_buffs < 2: return True_() return self.received(Buff.movement, number_of_movement_buffs // 2) def can_succeed_luau_soup(self) -> StardewRule: - if self.options[options.FestivalLocations] != options.FestivalLocations.option_hard: + if self.options.festival_locations != FestivalLocations.option_hard: return True_() eligible_fish = [Fish.blobfish, Fish.crimsonfish, "Ice Pip", Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish, Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, "Super Cucumber"] @@ -1398,7 +1397,7 @@ def can_succeed_luau_soup(self) -> StardewRule: return Or(fish_rule) | Or(aged_rule) def can_succeed_grange_display(self) -> StardewRule: - if self.options[options.FestivalLocations] != options.FestivalLocations.option_hard: + if self.options.festival_locations != FestivalLocations.option_hard: return True_() animal_rule = self.has_animal(Generic.any) artisan_rule = self.can_keg(Generic.any) | self.can_preserves_jar(Generic.any) @@ -1527,12 +1526,12 @@ def can_open_geode(self, geode: str) -> StardewRule: return blacksmith_access & self.has(geode) def has_island_trader(self) -> StardewRule: - if self.options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true: + if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: return False_() return self.can_reach_region(Region.island_trader) def has_walnut(self, number: int) -> StardewRule: - if self.options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true: + if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: return False_() if number <= 0: return True_() @@ -1592,7 +1591,7 @@ def can_forage(self, season: str, region: str = Region.forest, need_hoe: bool = def npc_is_in_current_slot(self, name: str) -> bool: npc = all_villagers_by_name[name] mod = npc.mod_name - return mod is None or mod in self.options[options.Mods] + return mod is None or mod in self.options.mods def can_do_combat_at_level(self, level: str) -> StardewRule: if level == Performance.basic: @@ -1612,7 +1611,7 @@ def can_water(self, level: int) -> StardewRule: return tool_rule | spell_rule def has_prismatic_jelly_reward_access(self) -> StardewRule: - if self.options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled: + if self.options.special_order_locations == SpecialOrderLocations.option_disabled: return self.can_complete_special_order("Prismatic Jelly") return self.received("Monster Musk Recipe") diff --git a/worlds/stardew_valley/mods/logic/deepwoods.py b/worlds/stardew_valley/mods/logic/deepwoods.py index f6ecd6d8280..2aa90e5b76b 100644 --- a/worlds/stardew_valley/mods/logic/deepwoods.py +++ b/worlds/stardew_valley/mods/logic/deepwoods.py @@ -17,14 +17,14 @@ def can_reach_woods_depth(vanilla_logic, depth: int) -> StardewRule: if depth > 50: rules.append(vanilla_logic.can_do_combat_at_level(Performance.great) & vanilla_logic.can_cook() & vanilla_logic.received(ModTransportation.woods_obelisk)) - if vanilla_logic.options[options.SkillProgression] == options.SkillProgression.option_progressive: + if vanilla_logic.options.skill_progression == options.SkillProgression.option_progressive: combat_tier = min(10, max(0, tier + 5)) rules.append(vanilla_logic.has_skill_level(Skill.combat, combat_tier)) return And(rules) def has_woods_rune_to_depth(vanilla_logic, floor: int) -> StardewRule: - if vanilla_logic.options[options.ElevatorProgression] == options.ElevatorProgression.option_vanilla: + if vanilla_logic.options.elevator_progression == options.ElevatorProgression.option_vanilla: return True_() return vanilla_logic.received("Progressive Woods Obelisk Sigils", count=int(floor / 10)) diff --git a/worlds/stardew_valley/mods/logic/magic.py b/worlds/stardew_valley/mods/logic/magic.py index a084c6aa919..709376399c8 100644 --- a/worlds/stardew_valley/mods/logic/magic.py +++ b/worlds/stardew_valley/mods/logic/magic.py @@ -7,19 +7,19 @@ def can_use_clear_debris_instead_of_tool_level(vanilla_logic, level: int) -> StardewRule: - if ModNames.magic not in vanilla_logic.options[options.Mods]: + if ModNames.magic not in vanilla_logic.options.mods: return False_() return vanilla_logic.received(MagicSpell.clear_debris) & can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, level) def can_use_altar(vanilla_logic) -> StardewRule: - if ModNames.magic not in vanilla_logic.options[options.Mods]: + if ModNames.magic not in vanilla_logic.options.mods: return False_() return vanilla_logic.can_reach_region(MagicRegion.altar) def has_any_spell(vanilla_logic) -> StardewRule: - if ModNames.magic not in vanilla_logic.options[options.Mods]: + if ModNames.magic not in vanilla_logic.options.mods: return False_() return can_use_altar(vanilla_logic) @@ -40,7 +40,7 @@ def has_support_spell_count(vanilla_logic, count: int) -> StardewRule: def has_decent_spells(vanilla_logic) -> StardewRule: - if ModNames.magic not in vanilla_logic.options[options.Mods]: + if ModNames.magic not in vanilla_logic.options.mods: return False_() magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 2) magic_attack_options_rule = has_attack_spell_count(vanilla_logic, 1) @@ -48,7 +48,7 @@ def has_decent_spells(vanilla_logic) -> StardewRule: def has_good_spells(vanilla_logic) -> StardewRule: - if ModNames.magic not in vanilla_logic.options[options.Mods]: + if ModNames.magic not in vanilla_logic.options.mods: return False_() magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 4) magic_attack_options_rule = has_attack_spell_count(vanilla_logic, 2) @@ -57,7 +57,7 @@ def has_good_spells(vanilla_logic) -> StardewRule: def has_great_spells(vanilla_logic) -> StardewRule: - if ModNames.magic not in vanilla_logic.options[options.Mods]: + if ModNames.magic not in vanilla_logic.options.mods: return False_() magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 6) magic_attack_options_rule = has_attack_spell_count(vanilla_logic, 3) @@ -66,7 +66,7 @@ def has_great_spells(vanilla_logic) -> StardewRule: def has_amazing_spells(vanilla_logic) -> StardewRule: - if ModNames.magic not in vanilla_logic.options[options.Mods]: + if ModNames.magic not in vanilla_logic.options.mods: return False_() magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 8) magic_attack_options_rule = has_attack_spell_count(vanilla_logic, 4) @@ -75,6 +75,6 @@ def has_amazing_spells(vanilla_logic) -> StardewRule: def can_blink(vanilla_logic) -> StardewRule: - if ModNames.magic not in vanilla_logic.options[options.Mods]: + if ModNames.magic not in vanilla_logic.options.mods: return False_() return vanilla_logic.received(MagicSpell.blink) & can_use_altar(vanilla_logic) diff --git a/worlds/stardew_valley/mods/logic/skills.py b/worlds/stardew_valley/mods/logic/skills.py index 05b4c623e15..24402a088b1 100644 --- a/worlds/stardew_valley/mods/logic/skills.py +++ b/worlds/stardew_valley/mods/logic/skills.py @@ -29,17 +29,17 @@ def append_mod_skill_level(skills_items: List[str], active_mods): def can_earn_mod_skill_level(logic, skill: str, level: int) -> StardewRule: - if ModNames.luck_skill in logic.options[options.Mods] and skill == ModSkill.luck: + if ModNames.luck_skill in logic.options.mods and skill == ModSkill.luck: return can_earn_luck_skill_level(logic, level) - if ModNames.magic in logic.options[options.Mods] and skill == ModSkill.magic: + if ModNames.magic in logic.options.mods and skill == ModSkill.magic: return can_earn_magic_skill_level(logic, level) - if ModNames.socializing_skill in logic.options[options.Mods] and skill == ModSkill.socializing: + if ModNames.socializing_skill in logic.options.mods and skill == ModSkill.socializing: return can_earn_socializing_skill_level(logic, level) - if ModNames.archaeology in logic.options[options.Mods] and skill == ModSkill.archaeology: + if ModNames.archaeology in logic.options.mods and skill == ModSkill.archaeology: return can_earn_archaeology_skill_level(logic, level) - if ModNames.cooking_skill in logic.options[options.Mods] and skill == ModSkill.cooking: + if ModNames.cooking_skill in logic.options.mods and skill == ModSkill.cooking: return can_earn_cooking_skill_level(logic, level) - if ModNames.binning_skill in logic.options[options.Mods] and skill == ModSkill.binning: + if ModNames.binning_skill in logic.options.mods and skill == ModSkill.binning: return can_earn_binning_skill_level(logic, level) return False_() @@ -65,7 +65,7 @@ def can_earn_magic_skill_level(vanilla_logic, level: int) -> StardewRule: def can_earn_socializing_skill_level(vanilla_logic, level: int) -> StardewRule: villager_count = [] for villager in all_villagers: - if villager.mod_name in vanilla_logic.options[options.Mods] or villager.mod_name is None: + if villager.mod_name in vanilla_logic.options.mods or villager.mod_name is None: villager_count.append(vanilla_logic.can_earn_relationship(villager.name, level)) return Count(level * 2, villager_count) diff --git a/worlds/stardew_valley/mods/logic/skullcavernelevator.py b/worlds/stardew_valley/mods/logic/skullcavernelevator.py index 74db86d89aa..9a5140ae39c 100644 --- a/worlds/stardew_valley/mods/logic/skullcavernelevator.py +++ b/worlds/stardew_valley/mods/logic/skullcavernelevator.py @@ -4,7 +4,7 @@ def has_skull_cavern_elevator_to_floor(self, floor: int) -> StardewRule: - if self.options[options.ElevatorProgression] != options.ElevatorProgression.option_vanilla and \ - ModNames.skull_cavern_elevator in self.options[options.Mods]: + if self.options.elevator_progression != options.ElevatorProgression.option_vanilla and \ + ModNames.skull_cavern_elevator in self.options.mods: return self.received("Progressive Skull Cavern Elevator", floor // 25) return True_() diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index 78de9e8dbba..75573359a5a 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -1,29 +1,9 @@ from dataclasses import dataclass -from typing import Dict, Union, Protocol, runtime_checkable, ClassVar +from typing import Dict -from Options import Option, Range, DeathLink, SpecialRange, Toggle, Choice, OptionSet +from Options import Range, SpecialRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, Option from .mods.mod_data import ModNames -@runtime_checkable -class StardewOption(Protocol): - internal_name: ClassVar[str] - - -@dataclass -class StardewOptions: - options: Dict[str, Union[bool, int, str]] - - def __getitem__(self, item: Union[str, StardewOption]) -> Union[bool, int, str]: - if isinstance(item, StardewOption): - item = item.internal_name - - return self.options.get(item, None) - - def __setitem__(self, key: Union[str, StardewOption], value: Union[bool, int, str]): - if isinstance(key, StardewOption): - key = key.internal_name - self.options[key] = value - class Goal(Choice): """What's your goal with this play-through? @@ -553,56 +533,39 @@ class Mods(OptionSet): } -stardew_valley_option_classes = [ - Goal, - StartingMoney, - ProfitMargin, - BundleRandomization, - BundlePrice, - EntranceRandomization, - SeasonRandomization, - Cropsanity, - BackpackProgression, - ToolProgression, - SkillProgression, - BuildingProgression, - FestivalLocations, - ElevatorProgression, - ArcadeMachineLocations, - SpecialOrderLocations, - HelpWantedLocations, - Fishsanity, - Museumsanity, - Friendsanity, - FriendsanityHeartSize, - NumberOfMovementBuffs, - NumberOfLuckBuffs, - ExcludeGingerIsland, - TrapItems, - MultipleDaySleepEnabled, - MultipleDaySleepCost, - ExperienceMultiplier, - FriendshipMultiplier, - DebrisMultiplier, - QuickStart, - Gifting, - Mods, -] -stardew_valley_options: Dict[str, type(Option)] = {option.internal_name: option for option in - stardew_valley_option_classes} -default_options = {option.internal_name: option.default for option in stardew_valley_options.values()} -stardew_valley_options["death_link"] = DeathLink - - -def fetch_options(world, player: int) -> StardewOptions: - return StardewOptions({option: get_option_value(world, player, option) for option in stardew_valley_options}) - - -def get_option_value(world, player: int, name: str) -> Union[bool, int]: - assert name in stardew_valley_options, f"{name} is not a valid option for Stardew Valley." - - value = getattr(world, name) - - if issubclass(stardew_valley_options[name], Toggle): - return bool(value[player].value) - return value[player].value +@dataclass +class StardewValleyOptions(PerGameCommonOptions): + goal: Goal + starting_money: StartingMoney + profit_margin: ProfitMargin + bundle_randomization: BundleRandomization + bundle_price: BundlePrice + entrance_randomization: EntranceRandomization + season_randomization: SeasonRandomization + cropsanity: Cropsanity + backpack_progression: BackpackProgression + tool_progression: ToolProgression + skill_progression: SkillProgression + building_progression: BuildingProgression + festival_locations: FestivalLocations + elevator_progression: ElevatorProgression + arcade_machine_locations: ArcadeMachineLocations + special_order_locations: SpecialOrderLocations + help_wanted_locations: HelpWantedLocations + fishsanity: Fishsanity + museumsanity: Museumsanity + friendsanity: Friendsanity + friendsanity_heart_size: FriendsanityHeartSize + number_of_movement_buffs: NumberOfMovementBuffs + number_of_luck_buffs: NumberOfLuckBuffs + exclude_ginger_island: ExcludeGingerIsland + trap_items: TrapItems + multiple_day_sleep_enabled: MultipleDaySleepEnabled + multiple_day_sleep_cost: MultipleDaySleepCost + experience_multiplier: ExperienceMultiplier + friendship_multiplier: FriendshipMultiplier + debris_multiplier: DebrisMultiplier + quick_start: QuickStart + gifting: Gifting + mods: Mods + death_link: DeathLink diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index 60cad4c136f..e8daa772d88 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -2,11 +2,10 @@ from typing import Iterable, Dict, Protocol, List, Tuple, Set from BaseClasses import Region, Entrance -from . import options +from .options import EntranceRandomization, ExcludeGingerIsland, Museumsanity from .strings.entrance_names import Entrance from .strings.region_names import Region from .region_classes import RegionData, ConnectionData, RandomizationFlag -from .options import StardewOptions from .mods.mod_regions import ModDataList @@ -397,12 +396,12 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ] -def create_final_regions(world_options: StardewOptions) -> List[RegionData]: +def create_final_regions(world_options) -> List[RegionData]: final_regions = [] final_regions.extend(vanilla_regions) - if world_options[options.Mods] is None: + if world_options.mods is None: return final_regions - for mod in world_options[options.Mods]: + for mod in world_options.mods.value: if mod not in ModDataList: continue for mod_region in ModDataList[mod].regions: @@ -417,19 +416,19 @@ def create_final_regions(world_options: StardewOptions) -> List[RegionData]: return final_regions -def create_final_connections(world_options: StardewOptions) -> List[ConnectionData]: +def create_final_connections(world_options) -> List[ConnectionData]: final_connections = [] final_connections.extend(vanilla_connections) - if world_options[options.Mods] is None: + if world_options.mods is None: return final_connections - for mod in world_options[options.Mods]: + for mod in world_options.mods.value: if mod not in ModDataList: continue final_connections.extend(ModDataList[mod].connections) return final_connections -def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewOptions) -> Tuple[ +def create_regions(region_factory: RegionFactory, random: Random, world_options) -> Tuple[ Iterable[Region], Dict[str, str]]: final_regions = create_final_regions(world_options) regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in @@ -448,21 +447,21 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options: return regions.values(), randomized_data -def randomize_connections(random: Random, world_options: StardewOptions, regions_by_name) -> Tuple[ +def randomize_connections(random: Random, world_options, regions_by_name) -> Tuple[ List[ConnectionData], Dict[str, str]]: connections_to_randomize = [] final_connections = create_final_connections(world_options) connections_by_name: Dict[str, ConnectionData] = {connection.name: connection for connection in final_connections} - if world_options[options.EntranceRandomization] == options.EntranceRandomization.option_pelican_town: + if world_options.entrance_randomization == EntranceRandomization.option_pelican_town: connections_to_randomize = [connection for connection in final_connections if RandomizationFlag.PELICAN_TOWN in connection.flag] - elif world_options[options.EntranceRandomization] == options.EntranceRandomization.option_non_progression: + elif world_options.entrance_randomization == EntranceRandomization.option_non_progression: connections_to_randomize = [connection for connection in final_connections if RandomizationFlag.NON_PROGRESSION in connection.flag] - elif world_options[options.EntranceRandomization] == options.EntranceRandomization.option_buildings: + elif world_options.entrance_randomization == EntranceRandomization.option_buildings: connections_to_randomize = [connection for connection in final_connections if RandomizationFlag.BUILDINGS in connection.flag] - elif world_options[options.EntranceRandomization] == options.EntranceRandomization.option_chaos: + elif world_options.entrance_randomization == EntranceRandomization.option_chaos: connections_to_randomize = [connection for connection in final_connections if RandomizationFlag.BUILDINGS in connection.flag] connections_to_randomize = exclude_island_if_necessary(connections_to_randomize, world_options) @@ -491,8 +490,8 @@ def randomize_connections(random: Random, world_options: StardewOptions, regions def remove_excluded_entrances(connections_to_randomize, world_options): - exclude_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true - exclude_sewers = world_options[options.Museumsanity] == options.Museumsanity.option_none + exclude_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_true + exclude_sewers = world_options.museumsanity == Museumsanity.option_none if exclude_island: connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag] if exclude_sewers: @@ -502,7 +501,7 @@ def remove_excluded_entrances(connections_to_randomize, world_options): def exclude_island_if_necessary(connections_to_randomize: List[ConnectionData], world_options) -> List[ConnectionData]: - exclude_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true + exclude_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_true if exclude_island: connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag] diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 34ee1f807dd..f56dec39a1f 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -1,10 +1,10 @@ import itertools -from typing import Dict, List +from typing import List from BaseClasses import MultiWorld from worlds.generic import Rules as MultiWorldRules -from . import options, locations -from .bundles import Bundle +from .options import StardewValleyOptions, ToolProgression, BuildingProgression, SkillProgression, ExcludeGingerIsland, Cropsanity, SpecialOrderLocations, Museumsanity, \ + BackpackProgression, ArcadeMachineLocations from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, \ DeepWoodsEntrance, AlecEntrance, MagicEntrance from .data.museum_data import all_museum_items, all_museum_minerals, all_museum_artifacts, \ @@ -13,9 +13,8 @@ from .strings.region_names import Region from .mods.mod_data import ModNames from .mods.logic import magic, deepwoods -from .locations import LocationTags +from .locations import LocationTags, locations_by_tag from .logic import StardewLogic, And, tool_upgrade_prices -from .options import StardewOptions from .strings.ap_names.transport_names import Transportation from .strings.artisan_good_names import ArtisanGood from .strings.calendar_names import Weekday @@ -28,251 +27,256 @@ from .strings.wallet_item_names import Wallet -def set_rules(multi_world: MultiWorld, player: int, world_options: StardewOptions, logic: StardewLogic, - current_bundles: Dict[str, Bundle]): - all_location_names = list(location.name for location in multi_world.get_locations(player)) +def set_rules(world): + multiworld = world.multiworld + world_options = world.options + player = world.player + logic = world.logic + current_bundles = world.modified_bundles + + all_location_names = list(location.name for location in multiworld.get_locations(player)) - set_entrance_rules(logic, multi_world, player, world_options) + set_entrance_rules(logic, multiworld, player, world_options) - set_ginger_island_rules(logic, multi_world, player, world_options) + set_ginger_island_rules(logic, multiworld, player, world_options) # Those checks do not exist if ToolProgression is vanilla - if world_options[options.ToolProgression] != options.ToolProgression.option_vanilla: - MultiWorldRules.add_rule(multi_world.get_location("Purchase Fiberglass Rod", player), + if world_options.tool_progression != ToolProgression.option_vanilla: + MultiWorldRules.add_rule(multiworld.get_location("Purchase Fiberglass Rod", player), (logic.has_skill_level(Skill.fishing, 2) & logic.can_spend_money(1800)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Purchase Iridium Rod", player), + MultiWorldRules.add_rule(multiworld.get_location("Purchase Iridium Rod", player), (logic.has_skill_level(Skill.fishing, 6) & logic.can_spend_money(7500)).simplify()) materials = [None, "Copper", "Iron", "Gold", "Iridium"] tool = [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.watering_can, Tool.trash_can] for (previous, material), tool in itertools.product(zip(materials[:4], materials[1:]), tool): if previous is None: - MultiWorldRules.add_rule(multi_world.get_location(f"{material} {tool} Upgrade", player), + MultiWorldRules.add_rule(multiworld.get_location(f"{material} {tool} Upgrade", player), (logic.has(f"{material} Ore") & logic.can_spend_money(tool_upgrade_prices[material])).simplify()) else: - MultiWorldRules.add_rule(multi_world.get_location(f"{material} {tool} Upgrade", player), + MultiWorldRules.add_rule(multiworld.get_location(f"{material} {tool} Upgrade", player), (logic.has(f"{material} Ore") & logic.has_tool(tool, previous) & logic.can_spend_money(tool_upgrade_prices[material])).simplify()) - set_skills_rules(logic, multi_world, player, world_options) + set_skills_rules(logic, multiworld, player, world_options) # Bundles for bundle in current_bundles.values(): - location = multi_world.get_location(bundle.get_name_with_bundle(), player) + location = multiworld.get_location(bundle.get_name_with_bundle(), player) rules = logic.can_complete_bundle(bundle.requirements, bundle.number_required) simplified_rules = rules.simplify() MultiWorldRules.set_rule(location, simplified_rules) - MultiWorldRules.add_rule(multi_world.get_location("Complete Crafts Room", player), + MultiWorldRules.add_rule(multiworld.get_location("Complete Crafts Room", player), And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.CRAFTS_ROOM_BUNDLE]).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Complete Pantry", player), + for bundle in locations_by_tag[LocationTags.CRAFTS_ROOM_BUNDLE]).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Pantry", player), And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.PANTRY_BUNDLE]).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Complete Fish Tank", player), + for bundle in locations_by_tag[LocationTags.PANTRY_BUNDLE]).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Fish Tank", player), And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.FISH_TANK_BUNDLE]).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Complete Boiler Room", player), + for bundle in locations_by_tag[LocationTags.FISH_TANK_BUNDLE]).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Boiler Room", player), And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.BOILER_ROOM_BUNDLE]).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Complete Bulletin Board", player), + for bundle in locations_by_tag[LocationTags.BOILER_ROOM_BUNDLE]).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Bulletin Board", player), And(logic.can_reach_location(bundle.name) for bundle - in locations.locations_by_tag[LocationTags.BULLETIN_BOARD_BUNDLE]).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Complete Vault", player), + in locations_by_tag[LocationTags.BULLETIN_BOARD_BUNDLE]).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Vault", player), And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.VAULT_BUNDLE]).simplify()) + for bundle in locations_by_tag[LocationTags.VAULT_BUNDLE]).simplify()) # Buildings - if world_options[options.BuildingProgression] != options.BuildingProgression.option_vanilla: - for building in locations.locations_by_tag[LocationTags.BUILDING_BLUEPRINT]: - if building.mod_name is not None and building.mod_name not in world_options[options.Mods]: + if world_options.building_progression != BuildingProgression.option_vanilla: + for building in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]: + if building.mod_name is not None and building.mod_name not in world_options.mods: continue - MultiWorldRules.set_rule(multi_world.get_location(building.name, player), + MultiWorldRules.set_rule(multiworld.get_location(building.name, player), logic.building_rules[building.name.replace(" Blueprint", "")].simplify()) - set_cropsanity_rules(all_location_names, logic, multi_world, player, world_options) - set_story_quests_rules(all_location_names, logic, multi_world, player, world_options) - set_special_order_rules(all_location_names, logic, multi_world, player, world_options) - set_help_wanted_quests_rules(logic, multi_world, player, world_options) - set_fishsanity_rules(all_location_names, logic, multi_world, player) - set_museumsanity_rules(all_location_names, logic, multi_world, player, world_options) - set_friendsanity_rules(all_location_names, logic, multi_world, player) - set_backpack_rules(logic, multi_world, player, world_options) - set_festival_rules(all_location_names, logic, multi_world, player) - - MultiWorldRules.add_rule(multi_world.get_location("Old Master Cannoli", player), + set_cropsanity_rules(all_location_names, logic, multiworld, player, world_options) + set_story_quests_rules(all_location_names, logic, multiworld, player, world_options) + set_special_order_rules(all_location_names, logic, multiworld, player, world_options) + set_help_wanted_quests_rules(logic, multiworld, player, world_options) + set_fishsanity_rules(all_location_names, logic, multiworld, player) + set_museumsanity_rules(all_location_names, logic, multiworld, player, world_options) + set_friendsanity_rules(all_location_names, logic, multiworld, player) + set_backpack_rules(logic, multiworld, player, world_options) + set_festival_rules(all_location_names, logic, multiworld, player) + + MultiWorldRules.add_rule(multiworld.get_location("Old Master Cannoli", player), logic.has("Sweet Gem Berry").simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Galaxy Sword Shrine", player), + MultiWorldRules.add_rule(multiworld.get_location("Galaxy Sword Shrine", player), logic.has("Prismatic Shard").simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Have a Baby", player), + MultiWorldRules.add_rule(multiworld.get_location("Have a Baby", player), logic.can_reproduce(1).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Have Another Baby", player), + MultiWorldRules.add_rule(multiworld.get_location("Have Another Baby", player), logic.can_reproduce(2).simplify()) - set_traveling_merchant_rules(logic, multi_world, player) - set_arcade_machine_rules(logic, multi_world, player, world_options) - set_deepwoods_rules(logic, multi_world, player, world_options) - set_magic_spell_rules(logic, multi_world, player, world_options) + set_traveling_merchant_rules(logic, multiworld, player) + set_arcade_machine_rules(logic, multiworld, player, world_options) + set_deepwoods_rules(logic, multiworld, player, world_options) + set_magic_spell_rules(logic, multiworld, player, world_options) -def set_skills_rules(logic, multi_world, player, world_options): +def set_skills_rules(logic, multiworld, player, world_options): # Skills - if world_options[options.SkillProgression] != options.SkillProgression.option_vanilla: + if world_options.skill_progression != SkillProgression.option_vanilla: for i in range(1, 11): - set_skill_rule(logic, multi_world, player, Skill.farming, i) - set_skill_rule(logic, multi_world, player, Skill.fishing, i) - set_skill_rule(logic, multi_world, player, Skill.foraging, i) - set_skill_rule(logic, multi_world, player, Skill.mining, i) - set_skill_rule(logic, multi_world, player, Skill.combat, i) + set_skill_rule(logic, multiworld, player, Skill.farming, i) + set_skill_rule(logic, multiworld, player, Skill.fishing, i) + set_skill_rule(logic, multiworld, player, Skill.foraging, i) + set_skill_rule(logic, multiworld, player, Skill.mining, i) + set_skill_rule(logic, multiworld, player, Skill.combat, i) # Modded Skills - if ModNames.luck_skill in world_options[options.Mods]: - set_skill_rule(logic, multi_world, player, ModSkill.luck, i) - if ModNames.magic in world_options[options.Mods]: - set_skill_rule(logic, multi_world, player, ModSkill.magic, i) - if ModNames.binning_skill in world_options[options.Mods]: - set_skill_rule(logic, multi_world, player, ModSkill.binning, i) - if ModNames.cooking_skill in world_options[options.Mods]: - set_skill_rule(logic, multi_world, player, ModSkill.cooking, i) - if ModNames.socializing_skill in world_options[options.Mods]: - set_skill_rule(logic, multi_world, player, ModSkill.socializing, i) - if ModNames.archaeology in world_options[options.Mods]: - set_skill_rule(logic, multi_world, player, ModSkill.archaeology, i) - - -def set_skill_rule(logic, multi_world, player, skill: str, level: int): + if ModNames.luck_skill in world_options.mods: + set_skill_rule(logic, multiworld, player, ModSkill.luck, i) + if ModNames.magic in world_options.mods: + set_skill_rule(logic, multiworld, player, ModSkill.magic, i) + if ModNames.binning_skill in world_options.mods: + set_skill_rule(logic, multiworld, player, ModSkill.binning, i) + if ModNames.cooking_skill in world_options.mods: + set_skill_rule(logic, multiworld, player, ModSkill.cooking, i) + if ModNames.socializing_skill in world_options.mods: + set_skill_rule(logic, multiworld, player, ModSkill.socializing, i) + if ModNames.archaeology in world_options.mods: + set_skill_rule(logic, multiworld, player, ModSkill.archaeology, i) + + +def set_skill_rule(logic, multiworld, player, skill: str, level: int): location_name = f"Level {level} {skill}" - location = multi_world.get_location(location_name, player) + location = multiworld.get_location(location_name, player) rule = logic.can_earn_skill_level(skill, level).simplify() MultiWorldRules.set_rule(location, rule) -def set_entrance_rules(logic, multi_world, player, world_options: StardewOptions): +def set_entrance_rules(logic, multiworld, player, world_options: StardewValleyOptions): for floor in range(5, 120 + 5, 5): - MultiWorldRules.set_rule(multi_world.get_entrance(dig_to_mines_floor(floor), player), + MultiWorldRules.set_rule(multiworld.get_entrance(dig_to_mines_floor(floor), player), logic.can_mine_to_floor(floor).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_tide_pools, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_tide_pools, player), logic.received("Beach Bridge") | (magic.can_blink(logic)).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_quarry, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_quarry, player), logic.received("Bridge Repair") | (magic.can_blink(logic)).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_secret_woods, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_secret_woods, player), logic.has_tool(Tool.axe, "Iron") | (magic.can_blink(logic)).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.forest_to_sewer, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.forest_to_sewer, player), logic.has_rusty_key().simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.town_to_sewer, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.town_to_sewer, player), logic.has_rusty_key().simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.take_bus_to_desert, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.take_bus_to_desert, player), logic.received("Bus Repair").simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_skull_cavern, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_skull_cavern, player), logic.received(Wallet.skull_key).simplify()) for floor in range(25, 200 + 25, 25): - MultiWorldRules.set_rule(multi_world.get_entrance(dig_to_skull_floor(floor), player), + MultiWorldRules.set_rule(multiworld.get_entrance(dig_to_skull_floor(floor), player), logic.can_mine_to_skull_cavern_floor(floor).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.talk_to_mines_dwarf, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.talk_to_mines_dwarf, player), logic.can_speak_dwarf() & logic.has_tool(Tool.pickaxe, ToolMaterial.iron)) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.use_desert_obelisk, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.use_desert_obelisk, player), logic.received(Transportation.desert_obelisk).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.use_island_obelisk, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.use_island_obelisk, player), logic.received(Transportation.island_obelisk).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.use_farm_obelisk, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.use_farm_obelisk, player), logic.received(Transportation.farm_obelisk).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.buy_from_traveling_merchant, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.buy_from_traveling_merchant, player), logic.has_traveling_merchant()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_greenhouse, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_greenhouse, player), logic.received("Greenhouse")) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.mountain_to_adventurer_guild, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.mountain_to_adventurer_guild, player), logic.received("Adventurer's Guild")) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.mountain_to_railroad, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.mountain_to_railroad, player), logic.has_lived_months(2)) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_witch_warp_cave, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_witch_warp_cave, player), logic.received(Wallet.dark_talisman) | (magic.can_blink(logic)).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_witch_hut, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_witch_hut, player), (logic.has(ArtisanGood.void_mayonnaise) | magic.can_blink(logic)).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_mutant_bug_lair, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_mutant_bug_lair, player), ((logic.has_rusty_key() & logic.can_reach_region(Region.railroad) & logic.can_meet(NPC.krobus) | magic.can_blink(logic)).simplify())) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_harvey_room, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_harvey_room, player), logic.has_relationship(NPC.harvey, 2)) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.mountain_to_maru_room, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.mountain_to_maru_room, player), logic.has_relationship(NPC.maru, 2)) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_sebastian_room, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_sebastian_room, player), (logic.has_relationship(NPC.sebastian, 2) | magic.can_blink(logic)).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.forest_to_leah_cottage, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.forest_to_leah_cottage, player), logic.has_relationship(NPC.leah, 2)) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_elliott_house, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_elliott_house, player), logic.has_relationship(NPC.elliott, 2)) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_sunroom, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_sunroom, player), logic.has_relationship(NPC.caroline, 2)) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_wizard_basement, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_wizard_basement, player), logic.has_relationship(NPC.wizard, 4)) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.mountain_to_leo_treehouse, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.mountain_to_leo_treehouse, player), logic.received("Treehouse")) - if ModNames.alec in world_options[options.Mods]: - MultiWorldRules.set_rule(multi_world.get_entrance(AlecEntrance.petshop_to_bedroom, player), + if ModNames.alec in world_options.mods: + MultiWorldRules.set_rule(multiworld.get_entrance(AlecEntrance.petshop_to_bedroom, player), (logic.has_relationship(ModNPC.alec, 2) | magic.can_blink(logic)).simplify()) -def set_ginger_island_rules(logic: StardewLogic, multi_world, player, world_options: StardewOptions): - set_island_entrances_rules(logic, multi_world, player) - if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true: +def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): + set_island_entrances_rules(logic, multiworld, player) + if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true: return - set_boat_repair_rules(logic, multi_world, player) - set_island_parrot_rules(logic, multi_world, player) - MultiWorldRules.add_rule(multi_world.get_location("Open Professor Snail Cave", player), + set_boat_repair_rules(logic, multiworld, player) + set_island_parrot_rules(logic, multiworld, player) + MultiWorldRules.add_rule(multiworld.get_location("Open Professor Snail Cave", player), logic.has(Craftable.cherry_bomb).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Complete Island Field Office", player), + MultiWorldRules.add_rule(multiworld.get_location("Complete Island Field Office", player), logic.can_complete_field_office().simplify()) -def set_boat_repair_rules(logic: StardewLogic, multi_world, player): - MultiWorldRules.add_rule(multi_world.get_location("Repair Boat Hull", player), +def set_boat_repair_rules(logic: StardewLogic, multiworld, player): + MultiWorldRules.add_rule(multiworld.get_location("Repair Boat Hull", player), logic.has(Material.hardwood).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Repair Boat Anchor", player), + MultiWorldRules.add_rule(multiworld.get_location("Repair Boat Anchor", player), logic.has(MetalBar.iridium).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Repair Ticket Machine", player), + MultiWorldRules.add_rule(multiworld.get_location("Repair Ticket Machine", player), logic.has(ArtisanGood.battery_pack).simplify()) -def set_island_entrances_rules(logic: StardewLogic, multi_world, player): +def set_island_entrances_rules(logic: StardewLogic, multiworld, player): boat_repaired = logic.received(Transportation.boat_repair).simplify() - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.fish_shop_to_boat_tunnel, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.fish_shop_to_boat_tunnel, player), boat_repaired) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.boat_to_ginger_island, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.boat_to_ginger_island, player), boat_repaired) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_south_to_west, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_south_to_west, player), logic.received("Island West Turtle").simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_south_to_north, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_south_to_north, player), logic.received("Island North Turtle").simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_west_to_islandfarmhouse, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_west_to_islandfarmhouse, player), logic.received("Island Farmhouse").simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_west_to_gourmand_cave, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_west_to_gourmand_cave, player), logic.received("Island Farmhouse").simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_north_to_dig_site, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_north_to_dig_site, player), logic.received("Dig Site Bridge").simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.dig_site_to_professor_snail_cave, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.dig_site_to_professor_snail_cave, player), logic.received("Open Professor Snail Cave").simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.talk_to_island_trader, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.talk_to_island_trader, player), logic.received("Island Trader").simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_south_to_southeast, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_south_to_southeast, player), logic.received("Island Resort").simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.use_island_resort, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.use_island_resort, player), logic.received("Island Resort").simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_west_to_qi_walnut_room, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_west_to_qi_walnut_room, player), logic.received("Qi Walnut Room").simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_north_to_volcano, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.island_north_to_volcano, player), (logic.can_water(0) | logic.received("Volcano Bridge") | magic.can_blink(logic)).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.volcano_to_secret_beach, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.volcano_to_secret_beach, player), logic.can_water(2).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.climb_to_volcano_5, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.climb_to_volcano_5, player), (logic.can_mine_perfectly() & logic.can_water(1)).simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.talk_to_volcano_dwarf, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.talk_to_volcano_dwarf, player), logic.can_speak_dwarf()) - MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.climb_to_volcano_10, player), + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.climb_to_volcano_10, player), (logic.can_mine_perfectly() & logic.can_water(1) & logic.received("Volcano Exit Shortcut")).simplify()) parrots = [Entrance.parrot_express_docks_to_volcano, Entrance.parrot_express_jungle_to_volcano, Entrance.parrot_express_dig_site_to_volcano, Entrance.parrot_express_docks_to_dig_site, @@ -281,78 +285,78 @@ def set_island_entrances_rules(logic: StardewLogic, multi_world, player): Entrance.parrot_express_volcano_to_jungle, Entrance.parrot_express_jungle_to_docks, Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_volcano_to_docks] for parrot in parrots: - MultiWorldRules.set_rule(multi_world.get_entrance(parrot, player), logic.received(Transportation.parrot_express).simplify()) + MultiWorldRules.set_rule(multiworld.get_entrance(parrot, player), logic.received(Transportation.parrot_express).simplify()) -def set_island_parrot_rules(logic: StardewLogic, multi_world, player): +def set_island_parrot_rules(logic: StardewLogic, multiworld, player): has_walnut = logic.has_walnut(1).simplify() has_5_walnut = logic.has_walnut(5).simplify() has_10_walnut = logic.has_walnut(10).simplify() has_20_walnut = logic.has_walnut(20).simplify() - MultiWorldRules.add_rule(multi_world.get_location("Leo's Parrot", player), + MultiWorldRules.add_rule(multiworld.get_location("Leo's Parrot", player), has_walnut) - MultiWorldRules.add_rule(multi_world.get_location("Island West Turtle", player), + MultiWorldRules.add_rule(multiworld.get_location("Island West Turtle", player), has_10_walnut & logic.received("Island North Turtle")) - MultiWorldRules.add_rule(multi_world.get_location("Island Farmhouse", player), + MultiWorldRules.add_rule(multiworld.get_location("Island Farmhouse", player), has_20_walnut) - MultiWorldRules.add_rule(multi_world.get_location("Island Mailbox", player), + MultiWorldRules.add_rule(multiworld.get_location("Island Mailbox", player), has_5_walnut & logic.received("Island Farmhouse")) - MultiWorldRules.add_rule(multi_world.get_location(Transportation.farm_obelisk, player), + MultiWorldRules.add_rule(multiworld.get_location(Transportation.farm_obelisk, player), has_20_walnut & logic.received("Island Mailbox")) - MultiWorldRules.add_rule(multi_world.get_location("Dig Site Bridge", player), + MultiWorldRules.add_rule(multiworld.get_location("Dig Site Bridge", player), has_10_walnut & logic.received("Island West Turtle")) - MultiWorldRules.add_rule(multi_world.get_location("Island Trader", player), + MultiWorldRules.add_rule(multiworld.get_location("Island Trader", player), has_10_walnut & logic.received("Island Farmhouse")) - MultiWorldRules.add_rule(multi_world.get_location("Volcano Bridge", player), + MultiWorldRules.add_rule(multiworld.get_location("Volcano Bridge", player), has_5_walnut & logic.received("Island West Turtle") & logic.can_reach_region(Region.volcano_floor_10)) - MultiWorldRules.add_rule(multi_world.get_location("Volcano Exit Shortcut", player), + MultiWorldRules.add_rule(multiworld.get_location("Volcano Exit Shortcut", player), has_5_walnut & logic.received("Island West Turtle")) - MultiWorldRules.add_rule(multi_world.get_location("Island Resort", player), + MultiWorldRules.add_rule(multiworld.get_location("Island Resort", player), has_20_walnut & logic.received("Island Farmhouse")) - MultiWorldRules.add_rule(multi_world.get_location(Transportation.parrot_express, player), + MultiWorldRules.add_rule(multiworld.get_location(Transportation.parrot_express, player), has_10_walnut) -def set_cropsanity_rules(all_location_names: List[str], logic, multi_world, player, world_options: StardewOptions): - if world_options[options.Cropsanity] == options.Cropsanity.option_disabled: +def set_cropsanity_rules(all_location_names: List[str], logic, multiworld, player, world_options: StardewValleyOptions): + if world_options.cropsanity == Cropsanity.option_disabled: return harvest_prefix = "Harvest " harvest_prefix_length = len(harvest_prefix) - for harvest_location in locations.locations_by_tag[LocationTags.CROPSANITY]: - if harvest_location.name in all_location_names and (harvest_location.mod_name is None or harvest_location.mod_name in world_options[options.Mods]): + for harvest_location in locations_by_tag[LocationTags.CROPSANITY]: + if harvest_location.name in all_location_names and (harvest_location.mod_name is None or harvest_location.mod_name in world_options.mods): crop_name = harvest_location.name[harvest_prefix_length:] - MultiWorldRules.set_rule(multi_world.get_location(harvest_location.name, player), + MultiWorldRules.set_rule(multiworld.get_location(harvest_location.name, player), logic.has(crop_name).simplify()) -def set_story_quests_rules(all_location_names: List[str], logic, multi_world, player, world_options: StardewOptions): - for quest in locations.locations_by_tag[LocationTags.QUEST]: - if quest.name in all_location_names and (quest.mod_name is None or quest.mod_name in world_options[options.Mods]): - MultiWorldRules.set_rule(multi_world.get_location(quest.name, player), +def set_story_quests_rules(all_location_names: List[str], logic, multiworld, player, world_options: StardewValleyOptions): + for quest in locations_by_tag[LocationTags.QUEST]: + if quest.name in all_location_names and (quest.mod_name is None or quest.mod_name in world_options.mods): + MultiWorldRules.set_rule(multiworld.get_location(quest.name, player), logic.quest_rules[quest.name].simplify()) -def set_special_order_rules(all_location_names: List[str], logic: StardewLogic, multi_world, player, - world_options: StardewOptions): - if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled: +def set_special_order_rules(all_location_names: List[str], logic: StardewLogic, multiworld, player, + world_options: StardewValleyOptions): + if world_options.special_order_locations == SpecialOrderLocations.option_disabled: return board_rule = logic.received("Special Order Board") & logic.has_lived_months(4) - for board_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: + for board_order in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: if board_order.name in all_location_names: order_rule = board_rule & logic.special_order_rules[board_order.name] - MultiWorldRules.set_rule(multi_world.get_location(board_order.name, player), order_rule.simplify()) + MultiWorldRules.set_rule(multiworld.get_location(board_order.name, player), order_rule.simplify()) - if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true: + if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true: return - if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_board_only: + if world_options.special_order_locations == SpecialOrderLocations.option_board_only: return qi_rule = logic.can_reach_region(Region.qi_walnut_room) & logic.has_lived_months(8) - for qi_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: + for qi_order in locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: if qi_order.name in all_location_names: order_rule = qi_rule & logic.special_order_rules[qi_order.name] - MultiWorldRules.set_rule(multi_world.get_location(qi_order.name, player), order_rule.simplify()) + MultiWorldRules.set_rule(multiworld.get_location(qi_order.name, player), order_rule.simplify()) help_wanted_prefix = "Help Wanted:" @@ -362,8 +366,8 @@ def set_special_order_rules(all_location_names: List[str], logic: StardewLogic, slay_monsters = "Slay Monsters" -def set_help_wanted_quests_rules(logic: StardewLogic, multi_world, player, world_options): - help_wanted_number = world_options[options.HelpWantedLocations] +def set_help_wanted_quests_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): + help_wanted_number = world_options.help_wanted_locations for i in range(0, help_wanted_number): set_number = i // 7 month_rule = logic.has_lived_months(set_number).simplify() @@ -371,58 +375,58 @@ def set_help_wanted_quests_rules(logic: StardewLogic, multi_world, player, world quest_number_in_set = i % 7 if quest_number_in_set < 4: quest_number = set_number * 4 + quest_number_in_set + 1 - set_help_wanted_delivery_rule(multi_world, player, month_rule, quest_number) + set_help_wanted_delivery_rule(multiworld, player, month_rule, quest_number) elif quest_number_in_set == 4: - set_help_wanted_fishing_rule(logic, multi_world, player, month_rule, quest_number) + set_help_wanted_fishing_rule(logic, multiworld, player, month_rule, quest_number) elif quest_number_in_set == 5: - set_help_wanted_slay_monsters_rule(logic, multi_world, player, month_rule, quest_number) + set_help_wanted_slay_monsters_rule(logic, multiworld, player, month_rule, quest_number) elif quest_number_in_set == 6: - set_help_wanted_gathering_rule(multi_world, player, month_rule, quest_number) + set_help_wanted_gathering_rule(multiworld, player, month_rule, quest_number) -def set_help_wanted_delivery_rule(multi_world, player, month_rule, quest_number): +def set_help_wanted_delivery_rule(multiworld, player, month_rule, quest_number): location_name = f"{help_wanted_prefix} {item_delivery} {quest_number}" - MultiWorldRules.set_rule(multi_world.get_location(location_name, player), month_rule) + MultiWorldRules.set_rule(multiworld.get_location(location_name, player), month_rule) -def set_help_wanted_gathering_rule(multi_world, player, month_rule, quest_number): +def set_help_wanted_gathering_rule(multiworld, player, month_rule, quest_number): location_name = f"{help_wanted_prefix} {gathering} {quest_number}" - MultiWorldRules.set_rule(multi_world.get_location(location_name, player), month_rule) + MultiWorldRules.set_rule(multiworld.get_location(location_name, player), month_rule) -def set_help_wanted_fishing_rule(logic: StardewLogic, multi_world, player, month_rule, quest_number): +def set_help_wanted_fishing_rule(logic: StardewLogic, multiworld, player, month_rule, quest_number): location_name = f"{help_wanted_prefix} {fishing} {quest_number}" fishing_rule = month_rule & logic.can_fish() - MultiWorldRules.set_rule(multi_world.get_location(location_name, player), fishing_rule.simplify()) + MultiWorldRules.set_rule(multiworld.get_location(location_name, player), fishing_rule.simplify()) -def set_help_wanted_slay_monsters_rule(logic: StardewLogic, multi_world, player, month_rule, quest_number): +def set_help_wanted_slay_monsters_rule(logic: StardewLogic, multiworld, player, month_rule, quest_number): location_name = f"{help_wanted_prefix} {slay_monsters} {quest_number}" slay_rule = month_rule & logic.can_do_combat_at_level("Basic") - MultiWorldRules.set_rule(multi_world.get_location(location_name, player), slay_rule.simplify()) + MultiWorldRules.set_rule(multiworld.get_location(location_name, player), slay_rule.simplify()) -def set_fishsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int): +def set_fishsanity_rules(all_location_names: List[str], logic: StardewLogic, multiworld: MultiWorld, player: int): fish_prefix = "Fishsanity: " - for fish_location in locations.locations_by_tag[LocationTags.FISHSANITY]: + for fish_location in locations_by_tag[LocationTags.FISHSANITY]: if fish_location.name in all_location_names: fish_name = fish_location.name[len(fish_prefix):] - MultiWorldRules.set_rule(multi_world.get_location(fish_location.name, player), + MultiWorldRules.set_rule(multiworld.get_location(fish_location.name, player), logic.has(fish_name).simplify()) -def set_museumsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int, - world_options: StardewOptions): +def set_museumsanity_rules(all_location_names: List[str], logic: StardewLogic, multiworld: MultiWorld, player: int, + world_options: StardewValleyOptions): museum_prefix = "Museumsanity: " - if world_options[options.Museumsanity] == options.Museumsanity.option_milestones: - for museum_milestone in locations.locations_by_tag[LocationTags.MUSEUM_MILESTONES]: - set_museum_milestone_rule(logic, multi_world, museum_milestone, museum_prefix, player) - elif world_options[options.Museumsanity] != options.Museumsanity.option_none: - set_museum_individual_donations_rules(all_location_names, logic, multi_world, museum_prefix, player) + if world_options.museumsanity == Museumsanity.option_milestones: + for museum_milestone in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: + set_museum_milestone_rule(logic, multiworld, museum_milestone, museum_prefix, player) + elif world_options.museumsanity != Museumsanity.option_none: + set_museum_individual_donations_rules(all_location_names, logic, multiworld, museum_prefix, player) -def set_museum_individual_donations_rules(all_location_names, logic: StardewLogic, multi_world, museum_prefix, player): - all_donations = sorted(locations.locations_by_tag[LocationTags.MUSEUM_DONATIONS], +def set_museum_individual_donations_rules(all_location_names, logic: StardewLogic, multiworld, museum_prefix, player): + all_donations = sorted(locations_by_tag[LocationTags.MUSEUM_DONATIONS], key=lambda x: all_museum_items_by_name[x.name[len(museum_prefix):]].difficulty, reverse=True) counter = 0 number_donations = len(all_donations) @@ -430,13 +434,14 @@ def set_museum_individual_donations_rules(all_location_names, logic: StardewLogi if museum_location.name in all_location_names: donation_name = museum_location.name[len(museum_prefix):] required_detectors = counter * 5 // number_donations - rule = logic.can_donate_museum_item(all_museum_items_by_name[donation_name]) & logic.received("Traveling Merchant Metal Detector", required_detectors) - MultiWorldRules.set_rule(multi_world.get_location(museum_location.name, player), + rule = logic.can_donate_museum_item(all_museum_items_by_name[donation_name]) & logic.received("Traveling Merchant Metal Detector", + required_detectors) + MultiWorldRules.set_rule(multiworld.get_location(museum_location.name, player), rule.simplify()) counter += 1 -def set_museum_milestone_rule(logic: StardewLogic, multi_world: MultiWorld, museum_milestone, museum_prefix: str, +def set_museum_milestone_rule(logic: StardewLogic, multiworld: MultiWorld, museum_milestone, museum_prefix: str, player: int): milestone_name = museum_milestone.name[len(museum_prefix):] donations_suffix = " Donations" @@ -462,7 +467,7 @@ def set_museum_milestone_rule(logic: StardewLogic, multi_world: MultiWorld, muse rule = logic.can_donate_museum_item(Artifact.ancient_seed) & logic.received(metal_detector, 4) if rule is None: return - MultiWorldRules.set_rule(multi_world.get_location(museum_milestone.name, player), rule.simplify()) + MultiWorldRules.set_rule(multiworld.get_location(museum_milestone.name, player), rule.simplify()) def get_museum_item_count_rule(logic: StardewLogic, suffix, milestone_name, accepted_items, donation_func): @@ -473,156 +478,156 @@ def get_museum_item_count_rule(logic: StardewLogic, suffix, milestone_name, acce return rule -def set_backpack_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options): - if world_options[options.BackpackProgression] != options.BackpackProgression.option_vanilla: - MultiWorldRules.set_rule(multi_world.get_location("Large Pack", player), +def set_backpack_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): + if world_options.backpack_progression != BackpackProgression.option_vanilla: + MultiWorldRules.set_rule(multiworld.get_location("Large Pack", player), logic.can_spend_money(2000).simplify()) - MultiWorldRules.set_rule(multi_world.get_location("Deluxe Pack", player), + MultiWorldRules.set_rule(multiworld.get_location("Deluxe Pack", player), (logic.can_spend_money(10000) & logic.received("Progressive Backpack")).simplify()) - if ModNames.big_backpack in world_options[options.Mods]: - MultiWorldRules.set_rule(multi_world.get_location("Premium Pack", player), + if ModNames.big_backpack in world_options.mods: + MultiWorldRules.set_rule(multiworld.get_location("Premium Pack", player), (logic.can_spend_money(150000) & logic.received("Progressive Backpack", 2)).simplify()) -def set_festival_rules(all_location_names: List[str], logic: StardewLogic, multi_world, player): +def set_festival_rules(all_location_names: List[str], logic: StardewLogic, multiworld, player): festival_locations = [] - festival_locations.extend(locations.locations_by_tag[LocationTags.FESTIVAL]) - festival_locations.extend(locations.locations_by_tag[LocationTags.FESTIVAL_HARD]) + festival_locations.extend(locations_by_tag[LocationTags.FESTIVAL]) + festival_locations.extend(locations_by_tag[LocationTags.FESTIVAL_HARD]) for festival in festival_locations: if festival.name in all_location_names: - MultiWorldRules.set_rule(multi_world.get_location(festival.name, player), + MultiWorldRules.set_rule(multiworld.get_location(festival.name, player), logic.festival_rules[festival.name].simplify()) -def set_traveling_merchant_rules(logic: StardewLogic, multi_world: MultiWorld, player: int): +def set_traveling_merchant_rules(logic: StardewLogic, multiworld: MultiWorld, player: int): for day in Weekday.all_days: item_for_day = f"Traveling Merchant: {day}" for i in range(1, 4): location_name = f"Traveling Merchant {day} Item {i}" - MultiWorldRules.set_rule(multi_world.get_location(location_name, player), + MultiWorldRules.set_rule(multiworld.get_location(location_name, player), logic.received(item_for_day)) -def set_arcade_machine_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options): - MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.play_junimo_kart, player), +def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): + MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_junimo_kart, player), logic.received(Wallet.skull_key).simplify()) - if world_options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling: + if world_options.arcade_machine_locations != ArcadeMachineLocations.option_full_shuffling: return - MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.play_junimo_kart, player), + MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_junimo_kart, player), logic.has("Junimo Kart Small Buff").simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.reach_junimo_kart_2, player), + MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_2, player), logic.has("Junimo Kart Medium Buff").simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.reach_junimo_kart_3, player), + MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_3, player), logic.has("Junimo Kart Big Buff").simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Junimo Kart: Sunset Speedway (Victory)", player), + MultiWorldRules.add_rule(multiworld.get_location("Junimo Kart: Sunset Speedway (Victory)", player), logic.has("Junimo Kart Max Buff").simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.play_journey_of_the_prairie_king, player), + MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_journey_of_the_prairie_king, player), logic.has("JotPK Small Buff").simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.reach_jotpk_world_2, player), + MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_jotpk_world_2, player), logic.has("JotPK Medium Buff").simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.reach_jotpk_world_3, player), + MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_jotpk_world_3, player), logic.has("JotPK Big Buff").simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Journey of the Prairie King Victory", player), + MultiWorldRules.add_rule(multiworld.get_location("Journey of the Prairie King Victory", player), logic.has("JotPK Max Buff").simplify()) -def set_friendsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int): +def set_friendsanity_rules(all_location_names: List[str], logic: StardewLogic, multiworld: MultiWorld, player: int): friend_prefix = "Friendsanity: " friend_suffix = " <3" - for friend_location in locations.locations_by_tag[LocationTags.FRIENDSANITY]: - if not friend_location.name in all_location_names: + for friend_location in locations_by_tag[LocationTags.FRIENDSANITY]: + if friend_location.name not in all_location_names: continue friend_location_without_prefix = friend_location.name[len(friend_prefix):] friend_location_trimmed = friend_location_without_prefix[:friend_location_without_prefix.index(friend_suffix)] split_index = friend_location_trimmed.rindex(" ") friend_name = friend_location_trimmed[:split_index] num_hearts = int(friend_location_trimmed[split_index + 1:]) - MultiWorldRules.set_rule(multi_world.get_location(friend_location.name, player), + MultiWorldRules.set_rule(multiworld.get_location(friend_location.name, player), logic.can_earn_relationship(friend_name, num_hearts).simplify()) -def set_deepwoods_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options: StardewOptions): - if ModNames.deepwoods in world_options[options.Mods]: - MultiWorldRules.add_rule(multi_world.get_location("Breaking Up Deep Woods Gingerbread House", player), +def set_deepwoods_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): + if ModNames.deepwoods in world_options.mods: + MultiWorldRules.add_rule(multiworld.get_location("Breaking Up Deep Woods Gingerbread House", player), logic.has_tool(Tool.axe, "Gold") & deepwoods.can_reach_woods_depth(logic, 50).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Chop Down a Deep Woods Iridium Tree", player), + MultiWorldRules.add_rule(multiworld.get_location("Chop Down a Deep Woods Iridium Tree", player), logic.has_tool(Tool.axe, "Iridium").simplify()) - MultiWorldRules.set_rule(multi_world.get_entrance(DeepWoodsEntrance.use_woods_obelisk, player), + MultiWorldRules.set_rule(multiworld.get_entrance(DeepWoodsEntrance.use_woods_obelisk, player), logic.received("Woods Obelisk").simplify()) for depth in range(10, 100 + 10, 10): - MultiWorldRules.set_rule(multi_world.get_entrance(move_to_woods_depth(depth), player), + MultiWorldRules.set_rule(multiworld.get_entrance(move_to_woods_depth(depth), player), deepwoods.can_chop_to_depth(logic, depth).simplify()) -def set_magic_spell_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options: StardewOptions): - if ModNames.magic not in world_options[options.Mods]: +def set_magic_spell_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): + if ModNames.magic not in world_options.mods: return - MultiWorldRules.set_rule(multi_world.get_entrance(MagicEntrance.store_to_altar, player), - (logic.has_relationship(NPC.wizard, 3) & - logic.can_reach_region(Region.wizard_tower)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Clear Debris", player), - ((logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic")) - & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Till", player), - (logic.has_tool("Hoe", "Basic") & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Water", player), - (logic.has_tool("Watering Can", "Basic") & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze All Toil School Locations", player), - (logic.has_tool("Watering Can", "Basic") & logic.has_tool("Hoe", "Basic") - & (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic")) - & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.set_rule(multiworld.get_entrance(MagicEntrance.store_to_altar, player), + (logic.has_relationship(NPC.wizard, 3) & + logic.can_reach_region(Region.wizard_tower)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Clear Debris", player), + ((logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic")) + & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Till", player), + (logic.has_tool("Hoe", "Basic") & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Water", player), + (logic.has_tool("Watering Can", "Basic") & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze All Toil School Locations", player), + (logic.has_tool("Watering Can", "Basic") & logic.has_tool("Hoe", "Basic") + & (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic")) + & magic.can_use_altar(logic)).simplify()) # Do I *want* to add boots into logic when you get them even in vanilla without effort? idk - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Evac", player), - (logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Haste", player), - (logic.has("Coffee") & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Heal", player), - (logic.has("Life Elixir") & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze All Life School Locations", player), - (logic.has("Coffee") & logic.has("Life Elixir") - & logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Descend", player), - (logic.can_reach_region(Region.mines) & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Fireball", player), - (logic.has("Fire Quartz") & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Frostbite", player), - (logic.can_mine_to_floor(70) & logic.can_fish(85) & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze All Elemental School Locations", player), - (logic.can_reach_region(Region.mines) & logic.has("Fire Quartz") - & logic.can_reach_region(Region.mines_floor_70) & logic.can_fish(85) & - magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Lantern", player), - magic.can_use_altar(logic).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Tendrils", player), - (logic.can_reach_region(Region.farm) & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Shockwave", player), - (logic.has("Earth Crystal") & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze All Nature School Locations", player), - (logic.has("Earth Crystal") & logic.can_reach_region("Farm") & - magic.can_use_altar(logic)).simplify()), - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Meteor", player), - (logic.can_reach_region(Region.farm) & logic.has_lived_months(12) - & magic.can_use_altar(logic)).simplify()), - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Lucksteal", player), - (logic.can_reach_region(Region.witch_hut) & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze: Bloodmana", player), - (logic.can_reach_region(Region.mines_floor_100) & magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze All Eldritch School Locations", player), - (logic.can_reach_region(Region.witch_hut) & - logic.can_reach_region(Region.mines_floor_100) & - logic.can_reach_region(Region.farm) & logic.has_lived_months(12) & - magic.can_use_altar(logic)).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Analyze Every Magic School Location", player), - (logic.has_tool("Watering Can", "Basic") & logic.has_tool("Hoe", "Basic") - & (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic")) & - logic.has("Coffee") & logic.has("Life Elixir") - & logic.can_mine_perfectly() & logic.has("Earth Crystal") & - logic.can_reach_region(Region.mines) & - logic.has("Fire Quartz") & logic.can_fish(85) & - logic.can_reach_region(Region.witch_hut) & - logic.can_reach_region(Region.mines_floor_100) & - logic.can_reach_region(Region.farm) & logic.has_lived_months(12) & - magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Evac", player), + (logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Haste", player), + (logic.has("Coffee") & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Heal", player), + (logic.has("Life Elixir") & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze All Life School Locations", player), + (logic.has("Coffee") & logic.has("Life Elixir") + & logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Descend", player), + (logic.can_reach_region(Region.mines) & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Fireball", player), + (logic.has("Fire Quartz") & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Frostbite", player), + (logic.can_mine_to_floor(70) & logic.can_fish(85) & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze All Elemental School Locations", player), + (logic.can_reach_region(Region.mines) & logic.has("Fire Quartz") + & logic.can_reach_region(Region.mines_floor_70) & logic.can_fish(85) & + magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Lantern", player), + magic.can_use_altar(logic).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Tendrils", player), + (logic.can_reach_region(Region.farm) & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Shockwave", player), + (logic.has("Earth Crystal") & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze All Nature School Locations", player), + (logic.has("Earth Crystal") & logic.can_reach_region("Farm") & + magic.can_use_altar(logic)).simplify()), + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Meteor", player), + (logic.can_reach_region(Region.farm) & logic.has_lived_months(12) + & magic.can_use_altar(logic)).simplify()), + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Lucksteal", player), + (logic.can_reach_region(Region.witch_hut) & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze: Bloodmana", player), + (logic.can_reach_region(Region.mines_floor_100) & magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze All Eldritch School Locations", player), + (logic.can_reach_region(Region.witch_hut) & + logic.can_reach_region(Region.mines_floor_100) & + logic.can_reach_region(Region.farm) & logic.has_lived_months(12) & + magic.can_use_altar(logic)).simplify()) + MultiWorldRules.add_rule(multiworld.get_location("Analyze Every Magic School Location", player), + (logic.has_tool("Watering Can", "Basic") & logic.has_tool("Hoe", "Basic") + & (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic")) & + logic.has("Coffee") & logic.has("Life Elixir") + & logic.can_mine_perfectly() & logic.has("Earth Crystal") & + logic.can_reach_region(Region.mines) & + logic.has("Fire Quartz") & logic.can_fish(85) & + logic.can_reach_region(Region.witch_hut) & + logic.can_reach_region(Region.mines_floor_100) & + logic.can_reach_region(Region.farm) & logic.has_lived_months(12) & + magic.can_use_altar(logic)).simplify()) diff --git a/worlds/stardew_valley/test/TestLogicSimplification.py b/worlds/stardew_valley/test/TestLogicSimplification.py index 83d779ce904..33b2428098b 100644 --- a/worlds/stardew_valley/test/TestLogicSimplification.py +++ b/worlds/stardew_valley/test/TestLogicSimplification.py @@ -1,6 +1,5 @@ from .. import True_ -from ..logic import Received, Has, False_, And, Or, StardewLogic -from ..options import default_options, StardewOptions +from ..logic import Received, Has, False_, And, Or def test_simplify_true_in_and(): diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 1cd17ada1f6..712aa300d53 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,15 +1,14 @@ import itertools -import unittest from random import random from typing import Dict from BaseClasses import ItemClassification, MultiWorld -from Options import SpecialRange, OptionSet +from Options import SpecialRange from . import setup_solo_multiworld, SVTestBase -from .. import StardewItem, options, items_by_group, Group +from .. import StardewItem, items_by_group, Group, StardewValleyWorld from ..locations import locations_by_tag, LocationTags, location_table -from ..options import StardewOption, stardew_valley_option_classes, Mods -from ..strings.goal_names import Goal +from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations +from ..strings.goal_names import Goal as GoalName from ..strings.season_names import Season from ..strings.special_order_names import SpecialOrder from ..strings.tool_names import ToolMaterial, Tool @@ -51,39 +50,41 @@ def get_option_choices(option) -> Dict[str, int]: class TestGenerateDynamicOptions(SVTestBase): def test_given_special_range_when_generate_then_basic_checks(self): - for option in stardew_valley_option_classes: - if not issubclass(option, SpecialRange): + options = self.world.options_dataclass.type_hints + for option_name, option in options.items(): + if not isinstance(option, SpecialRange): continue for value in option.special_range_names: - with self.subTest(f"{option.internal_name}: {value}"): - choices = {option.internal_name: option.special_range_names[value]} + with self.subTest(f"{option_name}: {value}"): + choices = {option_name: option.special_range_names[value]} multiworld = setup_solo_multiworld(choices) basic_checks(self, multiworld) def test_given_choice_when_generate_then_basic_checks(self): seed = int(random() * pow(10, 18) - 1) - for option in stardew_valley_option_classes: + options = self.world.options_dataclass.type_hints + for option_name, option in options.items(): if not option.options: continue for value in option.options: - with self.subTest(f"{option.internal_name}: {value} [Seed: {seed}]"): - world_options = {option.internal_name: option.options[value]} + with self.subTest(f"{option_name}: {value} [Seed: {seed}]"): + world_options = {option_name: option.options[value]} multiworld = setup_solo_multiworld(world_options, seed) basic_checks(self, multiworld) class TestGoal(SVTestBase): def test_given_goal_when_generate_then_victory_is_in_correct_location(self): - for goal, location in [("community_center", Goal.community_center), - ("grandpa_evaluation", Goal.grandpa_evaluation), - ("bottom_of_the_mines", Goal.bottom_of_the_mines), - ("cryptic_note", Goal.cryptic_note), - ("master_angler", Goal.master_angler), - ("complete_collection", Goal.complete_museum), - ("full_house", Goal.full_house), - ("perfection", Goal.perfection)]: + for goal, location in [("community_center", GoalName.community_center), + ("grandpa_evaluation", GoalName.grandpa_evaluation), + ("bottom_of_the_mines", GoalName.bottom_of_the_mines), + ("cryptic_note", GoalName.cryptic_note), + ("master_angler", GoalName.master_angler), + ("complete_collection", GoalName.complete_museum), + ("full_house", GoalName.full_house), + ("perfection", GoalName.perfection)]: with self.subTest(msg=f"Goal: {goal}, Location: {location}"): - world_options = {options.Goal.internal_name: options.Goal.options[goal]} + world_options = {Goal.internal_name: Goal.options[goal]} multi_world = setup_solo_multiworld(world_options) victory = multi_world.find_item("Victory", 1) self.assertEqual(victory.name, location) @@ -91,14 +92,14 @@ def test_given_goal_when_generate_then_victory_is_in_correct_location(self): class TestSeasonRandomization(SVTestBase): def test_given_disabled_when_generate_then_all_seasons_are_precollected(self): - world_options = {options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled} + world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_disabled} multi_world = setup_solo_multiworld(world_options) precollected_items = {item.name for item in multi_world.precollected_items[1]} self.assertTrue(all([season in precollected_items for season in SEASONS])) def test_given_randomized_when_generate_then_all_seasons_are_in_the_pool_or_precollected(self): - world_options = {options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized} + world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_randomized} multi_world = setup_solo_multiworld(world_options) precollected_items = {item.name for item in multi_world.precollected_items[1]} items = {item.name for item in multi_world.get_items()} | precollected_items @@ -106,7 +107,7 @@ def test_given_randomized_when_generate_then_all_seasons_are_in_the_pool_or_prec self.assertEqual(len(SEASONS.intersection(precollected_items)), 1) def test_given_progressive_when_generate_then_3_progressive_seasons_are_in_the_pool(self): - world_options = {options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive} + world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_progressive} multi_world = setup_solo_multiworld(world_options) items = [item.name for item in multi_world.get_items()] @@ -115,7 +116,7 @@ def test_given_progressive_when_generate_then_3_progressive_seasons_are_in_the_p class TestToolProgression(SVTestBase): def test_given_vanilla_when_generate_then_no_tool_in_pool(self): - world_options = {options.ToolProgression.internal_name: options.ToolProgression.option_vanilla} + world_options = {ToolProgression.internal_name: ToolProgression.option_vanilla} multi_world = setup_solo_multiworld(world_options) items = {item.name for item in multi_world.get_items()} @@ -123,7 +124,7 @@ def test_given_vanilla_when_generate_then_no_tool_in_pool(self): self.assertNotIn(tool, items) def test_given_progressive_when_generate_then_progressive_tool_of_each_is_in_pool_four_times(self): - world_options = {options.ToolProgression.internal_name: options.ToolProgression.option_progressive} + world_options = {ToolProgression.internal_name: ToolProgression.option_progressive} multi_world = setup_solo_multiworld(world_options) items = [item.name for item in multi_world.get_items()] @@ -131,7 +132,7 @@ def test_given_progressive_when_generate_then_progressive_tool_of_each_is_in_poo self.assertEqual(items.count("Progressive " + tool), 4) def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): - world_options = {options.ToolProgression.internal_name: options.ToolProgression.option_progressive} + world_options = {ToolProgression.internal_name: ToolProgression.option_progressive} multi_world = setup_solo_multiworld(world_options) locations = {locations.name for locations in multi_world.get_locations(1)} @@ -148,50 +149,52 @@ def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): def test_given_special_range_when_generate_exclude_ginger_island(self): - for option in stardew_valley_option_classes: - if not issubclass(option, - SpecialRange) or option.internal_name == options.ExcludeGingerIsland.internal_name: + options = self.world.options_dataclass.type_hints + for option_name, option in options.items(): + if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name: continue for value in option.special_range_names: - with self.subTest(f"{option.internal_name}: {value}"): + with self.subTest(f"{option_name}: {value}"): multiworld = setup_solo_multiworld( - {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, - option.internal_name: option.special_range_names[value]}) + {ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + option_name: option.special_range_names[value]}) check_no_ginger_island(self, multiworld) def test_given_choice_when_generate_exclude_ginger_island(self): seed = int(random() * pow(10, 18) - 1) - island_option = options.ExcludeGingerIsland - for option in stardew_valley_option_classes: - if not option.options or option.internal_name == island_option.internal_name: + options = self.world.options_dataclass.type_hints + for option_name, option in options.items(): + if not option.options or option_name == ExcludeGingerIsland.internal_name: continue for value in option.options: - with self.subTest(f"{option.internal_name}: {value} [Seed: {seed}]"): + with self.subTest(f"{option_name}: {value} [Seed: {seed}]"): multiworld = setup_solo_multiworld( - {island_option.internal_name: island_option.option_true, - option.internal_name: option.options[value]}, seed) - if multiworld.worlds[self.player].options[island_option.internal_name] != island_option.option_true: + {ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + option_name: option.options[value]}, seed) + stardew_world: StardewValleyWorld = multiworld.worlds[self.player] + if stardew_world.options.exclude_ginger_island != ExcludeGingerIsland.option_true: continue basic_checks(self, multiworld) check_no_ginger_island(self, multiworld) def test_given_island_related_goal_then_override_exclude_ginger_island(self): - island_goals = [value for value in options.Goal.options if value in ["walnut_hunter", "perfection"]] - island_option = options.ExcludeGingerIsland + island_goals = [value for value in Goal.options if value in ["walnut_hunter", "perfection"]] + island_option = ExcludeGingerIsland for goal in island_goals: for value in island_option.options: with self.subTest(f"Goal: {goal}, {island_option.internal_name}: {value}"): multiworld = setup_solo_multiworld( - {options.Goal.internal_name: options.Goal.options[goal], + {Goal.internal_name: Goal.options[goal], island_option.internal_name: island_option.options[value]}) - self.assertEqual(multiworld.worlds[self.player].options[island_option.internal_name], island_option.option_false) + stardew_world: StardewValleyWorld = multiworld.worlds[self.player] + self.assertEqual(stardew_world.options.exclude_ginger_island, island_option.option_false) basic_checks(self, multiworld) class TestTraps(SVTestBase): def test_given_no_traps_when_generate_then_no_trap_in_pool(self): world_options = self.allsanity_options_without_mods() - world_options.update({options.TrapItems.internal_name: options.TrapItems.option_no_traps}) + world_options.update({TrapItems.internal_name: TrapItems.option_no_traps}) multi_world = setup_solo_multiworld(world_options) trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]] @@ -202,12 +205,12 @@ def test_given_no_traps_when_generate_then_no_trap_in_pool(self): self.assertNotIn(item, multiworld_items) def test_given_traps_when_generate_then_all_traps_in_pool(self): - trap_option = options.TrapItems + trap_option = TrapItems for value in trap_option.options: if value == "no_traps": continue world_options = self.allsanity_options_with_mods() - world_options.update({options.TrapItems.internal_name: trap_option.options[value]}) + world_options.update({TrapItems.internal_name: trap_option.options[value]}) multi_world = setup_solo_multiworld(world_options) trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups and item_data.mod_name is None] multiworld_items = [item.name for item in multi_world.get_items()] @@ -218,7 +221,7 @@ def test_given_traps_when_generate_then_all_traps_in_pool(self): class TestSpecialOrders(SVTestBase): def test_given_disabled_then_no_order_in_pool(self): - world_options = {options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled} + world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled} multi_world = setup_solo_multiworld(world_options) locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} @@ -228,7 +231,7 @@ def test_given_disabled_then_no_order_in_pool(self): self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) def test_given_board_only_then_no_qi_order_in_pool(self): - world_options = {options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_only} + world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only} multi_world = setup_solo_multiworld(world_options) locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} @@ -242,8 +245,8 @@ def test_given_board_only_then_no_qi_order_in_pool(self): self.assertIn(board_location.name, locations_in_pool) def test_given_board_and_qi_then_all_orders_in_pool(self): - world_options = {options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_victories} + world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories} multi_world = setup_solo_multiworld(world_options) locations_in_pool = {location.name for location in multi_world.get_locations()} @@ -258,8 +261,8 @@ def test_given_board_and_qi_then_all_orders_in_pool(self): self.assertIn(board_location.name, locations_in_pool) def test_given_board_and_qi_without_arcade_machines_then_lets_play_a_game_not_in_pool(self): - world_options = {options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled} + world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled} multi_world = setup_solo_multiworld(world_options) locations_in_pool = {location.name for location in multi_world.get_locations()} diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py index 293ce72d07c..2347ca33db0 100644 --- a/worlds/stardew_valley/test/TestRegions.py +++ b/worlds/stardew_valley/test/TestRegions.py @@ -3,7 +3,8 @@ import unittest from . import SVTestBase, setup_solo_multiworld -from .. import StardewOptions, options, StardewValleyWorld +from .. import options, StardewValleyWorld, StardewValleyOptions +from ..options import EntranceRandomization, ExcludeGingerIsland from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag connections_by_name = {connection.name for connection in vanilla_connections} @@ -37,11 +38,12 @@ def test_entrance_randomization(self): seed = random.randrange(sys.maxsize) with self.subTest(flag=flag, msg=f"Seed: {seed}"): rand = random.Random(seed) - world_options = StardewOptions({options.EntranceRandomization.internal_name: option, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false}) + world_options = {EntranceRandomization.internal_name: option, + ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false} + multiworld = setup_solo_multiworld(world_options) regions_by_name = {region.name: region for region in vanilla_regions} - _, randomized_connections = randomize_connections(rand, world_options, regions_by_name) + _, randomized_connections = randomize_connections(rand, multiworld.worlds[1].options, regions_by_name) for connection in vanilla_connections: if flag in connection.flag: @@ -62,11 +64,12 @@ def test_entrance_randomization_without_island(self): with self.subTest(option=option, flag=flag): seed = random.randrange(sys.maxsize) rand = random.Random(seed) - world_options = StardewOptions({options.EntranceRandomization.internal_name: option, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true}) + world_options = {EntranceRandomization.internal_name: option, + ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true} + multiworld = setup_solo_multiworld(world_options) regions_by_name = {region.name: region for region in vanilla_regions} - _, randomized_connections = randomize_connections(rand, world_options, regions_by_name) + _, randomized_connections = randomize_connections(rand, multiworld.worlds[1].options, regions_by_name) for connection in vanilla_connections: if flag in connection.flag: diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 59da64c1d77..53181154d39 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -5,9 +5,12 @@ from BaseClasses import MultiWorld from test.TestBase import WorldTestBase from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld -from .. import StardewValleyWorld, options +from .. import StardewValleyWorld from ..mods.mod_data import ModNames from worlds.AutoWorld import call_all +from ..options import Cropsanity, SkillProgression, SpecialOrderLocations, Friendsanity, NumberOfLuckBuffs, SeasonRandomization, ToolProgression, \ + ElevatorProgression, Museumsanity, BackpackProgression, BuildingProgression, ArcadeMachineLocations, HelpWantedLocations, Fishsanity, NumberOfMovementBuffs, \ + BundleRandomization, BundlePrice, FestivalLocations, FriendsanityHeartSize, ExcludeGingerIsland, TrapItems, Goal, Mods class SVTestBase(WorldTestBase): @@ -33,48 +36,48 @@ def run_default_tests(self) -> bool: def minimal_locations_maximal_items(self): min_max_options = { - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, - options.Cropsanity.internal_name: options.Cropsanity.option_shuffled, - options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, - options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, - options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, - options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled, - options.HelpWantedLocations.internal_name: 0, - options.Fishsanity.internal_name: options.Fishsanity.option_none, - options.Museumsanity.internal_name: options.Museumsanity.option_none, - options.Friendsanity.internal_name: options.Friendsanity.option_none, - options.NumberOfMovementBuffs.internal_name: 12, - options.NumberOfLuckBuffs.internal_name: 12, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_shuffled, + BackpackProgression.internal_name: BackpackProgression.option_vanilla, + ToolProgression.internal_name: ToolProgression.option_vanilla, + SkillProgression.internal_name: SkillProgression.option_vanilla, + BuildingProgression.internal_name: BuildingProgression.option_vanilla, + ElevatorProgression.internal_name: ElevatorProgression.option_vanilla, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + HelpWantedLocations.internal_name: 0, + Fishsanity.internal_name: Fishsanity.option_none, + Museumsanity.internal_name: Museumsanity.option_none, + Friendsanity.internal_name: Friendsanity.option_none, + NumberOfMovementBuffs.internal_name: 12, + NumberOfLuckBuffs.internal_name: 12, } return min_max_options def allsanity_options_without_mods(self): allsanity = { - options.Goal.internal_name: options.Goal.option_perfection, - options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled, - options.BundlePrice.internal_name: options.BundlePrice.option_expensive, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, - options.Cropsanity.internal_name: options.Cropsanity.option_shuffled, - options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, - options.ToolProgression.internal_name: options.ToolProgression.option_progressive, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, - options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, - options.HelpWantedLocations.internal_name: 56, - options.Fishsanity.internal_name: options.Fishsanity.option_all, - options.Museumsanity.internal_name: options.Museumsanity.option_all, - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, - options.FriendsanityHeartSize.internal_name: 1, - options.NumberOfMovementBuffs.internal_name: 12, - options.NumberOfLuckBuffs.internal_name: 12, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, - options.TrapItems.internal_name: options.TrapItems.option_nightmare, + Goal.internal_name: Goal.option_perfection, + BundleRandomization.internal_name: BundleRandomization.option_shuffled, + BundlePrice.internal_name: BundlePrice.option_expensive, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_shuffled, + BackpackProgression.internal_name: BackpackProgression.option_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive, + FestivalLocations.internal_name: FestivalLocations.option_hard, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + HelpWantedLocations.internal_name: 56, + Fishsanity.internal_name: Fishsanity.option_all, + Museumsanity.internal_name: Museumsanity.option_all, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize.internal_name: 1, + NumberOfMovementBuffs.internal_name: 12, + NumberOfLuckBuffs.internal_name: 12, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + TrapItems.internal_name: TrapItems.option_nightmare, } return allsanity @@ -89,7 +92,7 @@ def allsanity_options_with_mods(self): ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator ) - allsanity.update({options.Mods.internal_name: all_mods}) + allsanity.update({Mods.internal_name: all_mods}) return allsanity pre_generated_worlds = {} @@ -110,7 +113,7 @@ def setup_solo_multiworld(test_options=None, seed=None, multiworld.set_seed(seed) # print(f"Seed: {multiworld.seed}") # Uncomment to print the seed for every test args = Namespace() - for name, option in StardewValleyWorld.option_definitions.items(): + for name, option in StardewValleyWorld.options_dataclass.type_hints.items(): value = option(test_options[name]) if name in test_options else option.from_any(option.default) setattr(args, name, {1: value}) multiworld.set_options(args) diff --git a/worlds/stardew_valley/test/checks/goal_checks.py b/worlds/stardew_valley/test/checks/goal_checks.py index e1059fe2d64..d0f06a6caaf 100644 --- a/worlds/stardew_valley/test/checks/goal_checks.py +++ b/worlds/stardew_valley/test/checks/goal_checks.py @@ -1,11 +1,11 @@ from BaseClasses import MultiWorld -from .option_checks import is_setting, assert_is_setting +from .option_checks import get_stardew_options from ... import options from .. import SVTestBase def is_goal(multiworld: MultiWorld, goal: int) -> bool: - return is_setting(multiworld, options.Goal.internal_name, goal) + return get_stardew_options(multiworld).goal.value == goal def is_bottom_mines(multiworld: MultiWorld) -> bool: @@ -33,7 +33,7 @@ def is_not_perfection(multiworld: MultiWorld) -> bool: def assert_ginger_island_is_included(tester: SVTestBase, multiworld: MultiWorld): - assert_is_setting(tester, multiworld, options.ExcludeGingerIsland.internal_name, options.ExcludeGingerIsland.option_false) + tester.assertEqual(get_stardew_options(multiworld).exclude_ginger_island, options.ExcludeGingerIsland.option_false) def assert_walnut_hunter_world_is_valid(tester: SVTestBase, multiworld: MultiWorld): diff --git a/worlds/stardew_valley/test/checks/option_checks.py b/worlds/stardew_valley/test/checks/option_checks.py index e6bced5b1ce..ce8e552461e 100644 --- a/worlds/stardew_valley/test/checks/option_checks.py +++ b/worlds/stardew_valley/test/checks/option_checks.py @@ -1,5 +1,3 @@ -from typing import Union - from BaseClasses import MultiWorld from .world_checks import get_all_item_names, get_all_location_names from .. import SVTestBase @@ -8,32 +6,16 @@ from ...strings.ap_names.transport_names import Transportation -def get_stardew_world(multiworld: MultiWorld) -> Union[StardewValleyWorld, None]: +def get_stardew_world(multiworld: MultiWorld) -> StardewValleyWorld: for world_key in multiworld.worlds: world = multiworld.worlds[world_key] if isinstance(world, StardewValleyWorld): return world - return None - - -def is_setting(multiworld: MultiWorld, setting_name: str, setting_value: int) -> bool: - stardew_world = get_stardew_world(multiworld) - if not stardew_world: - return False - current_value = stardew_world.options[setting_name] - return current_value == setting_value - + raise ValueError("no stardew world in this multiworld") -def is_not_setting(multiworld: MultiWorld, setting_name: str, setting_value: int) -> bool: - return not is_setting(multiworld, setting_name, setting_value) - -def assert_is_setting(tester: SVTestBase, multiworld: MultiWorld, setting_name: str, setting_value: int) -> bool: - stardew_world = get_stardew_world(multiworld) - if not stardew_world: - return False - current_value = stardew_world.options[setting_name] - tester.assertEqual(current_value, setting_value) +def get_stardew_options(multiworld: MultiWorld) -> options.StardewValleyOptions: + return get_stardew_world(multiworld).options def assert_can_reach_island(tester: SVTestBase, multiworld: MultiWorld): @@ -49,7 +31,8 @@ def assert_cannot_reach_island(tester: SVTestBase, multiworld: MultiWorld): def assert_can_reach_island_if_should(tester: SVTestBase, multiworld: MultiWorld): - include_island = is_setting(multiworld, options.ExcludeGingerIsland.internal_name, options.ExcludeGingerIsland.option_false) + stardew_options = get_stardew_options(multiworld) + include_island = stardew_options.exclude_ginger_island.value == options.ExcludeGingerIsland.option_false if include_island: assert_can_reach_island(tester, multiworld) else: @@ -57,7 +40,7 @@ def assert_can_reach_island_if_should(tester: SVTestBase, multiworld: MultiWorld def assert_cropsanity_same_number_items_and_locations(tester: SVTestBase, multiworld: MultiWorld): - is_cropsanity = is_setting(multiworld, options.Cropsanity.internal_name, options.Cropsanity.option_shuffled) + is_cropsanity = get_stardew_options(multiworld).cropsanity.value == options.Cropsanity.option_shuffled if not is_cropsanity: return @@ -80,11 +63,10 @@ def assert_has_deluxe_scarecrow_recipe(tester: SVTestBase, multiworld: MultiWorl def assert_festivals_give_access_to_deluxe_scarecrow(tester: SVTestBase, multiworld: MultiWorld): - has_festivals = is_not_setting(multiworld, options.FestivalLocations.internal_name, options.FestivalLocations.option_disabled) + stardew_options = get_stardew_options(multiworld) + has_festivals = stardew_options.festival_locations.value != options.FestivalLocations.option_disabled if not has_festivals: return assert_all_rarecrows_exist(tester, multiworld) assert_has_deluxe_scarecrow_recipe(tester, multiworld) - - diff --git a/worlds/stardew_valley/test/long/TestModsLong.py b/worlds/stardew_valley/test/long/TestModsLong.py new file mode 100644 index 00000000000..b3ec6f1420a --- /dev/null +++ b/worlds/stardew_valley/test/long/TestModsLong.py @@ -0,0 +1,63 @@ +from typing import List, Union + +from BaseClasses import MultiWorld +from worlds.stardew_valley.mods.mod_data import ModNames +from worlds.stardew_valley.test import setup_solo_multiworld +from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestBase +from worlds.stardew_valley.items import item_table +from worlds.stardew_valley.locations import location_table +from worlds.stardew_valley.options import Mods +from .option_names import options_to_include + +all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, + ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, + ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, + ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, + ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, + ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator}) + + +def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld): + if isinstance(chosen_mods, str): + chosen_mods = [chosen_mods] + for multiworld_item in multiworld.get_items(): + item = item_table[multiworld_item.name] + tester.assertTrue(item.mod_name is None or item.mod_name in chosen_mods) + for multiworld_location in multiworld.get_locations(): + if multiworld_location.event: + continue + location = location_table[multiworld_location.name] + tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods) + + +class TestGenerateModsOptions(SVTestBase): + + def test_given_mod_pairs_when_generate_then_basic_checks(self): + if self.skip_long_tests: + return + mods = list(all_mods) + num_mods = len(mods) + for mod1_index in range(0, num_mods): + for mod2_index in range(mod1_index + 1, num_mods): + mod1 = mods[mod1_index] + mod2 = mods[mod2_index] + mod_pair = (mod1, mod2) + with self.subTest(f"Mods: {mod_pair}"): + multiworld = setup_solo_multiworld({Mods: mod_pair}) + basic_checks(self, multiworld) + check_stray_mod_items(list(mod_pair), self, multiworld) + + def test_given_mod_names_when_generate_paired_with_other_options_then_basic_checks(self): + if self.skip_long_tests: + return + num_options = len(options_to_include) + for option_index in range(0, num_options): + option = options_to_include[option_index] + if not option.options: + continue + for value in option.options: + for mod in all_mods: + with self.subTest(f"{option.internal_name}: {value}, Mod: {mod}"): + multiworld = setup_solo_multiworld({option.internal_name: option.options[value], Mods: mod}) + basic_checks(self, multiworld) + check_stray_mod_items(mod, self, multiworld) \ No newline at end of file diff --git a/worlds/stardew_valley/test/long/TestOptionsLong.py b/worlds/stardew_valley/test/long/TestOptionsLong.py index c614ddcc36f..23ac6125e64 100644 --- a/worlds/stardew_valley/test/long/TestOptionsLong.py +++ b/worlds/stardew_valley/test/long/TestOptionsLong.py @@ -24,7 +24,6 @@ class TestGenerateDynamicOptions(SVTestBase): def test_given_option_pair_when_generate_then_basic_checks(self): if self.skip_long_tests: return - num_options = len(options_to_include) for option1_index in range(0, num_options): for option2_index in range(option1_index + 1, num_options): diff --git a/worlds/stardew_valley/test/long/TestRandomWorlds.py b/worlds/stardew_valley/test/long/TestRandomWorlds.py index 6ba814aba2c..0145f471d10 100644 --- a/worlds/stardew_valley/test/long/TestRandomWorlds.py +++ b/worlds/stardew_valley/test/long/TestRandomWorlds.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict import random from BaseClasses import MultiWorld @@ -9,7 +9,6 @@ from ..checks.option_checks import assert_can_reach_island_if_should, assert_cropsanity_same_number_items_and_locations, \ assert_festivals_give_access_to_deluxe_scarecrow from ..checks.world_checks import assert_same_number_items_locations, assert_victory_exists -from ... import options def get_option_choices(option) -> Dict[str, int]: diff --git a/worlds/stardew_valley/test/long/option_names.py b/worlds/stardew_valley/test/long/option_names.py index 9bb950d3a64..649d0da5b33 100644 --- a/worlds/stardew_valley/test/long/option_names.py +++ b/worlds/stardew_valley/test/long/option_names.py @@ -1,7 +1,8 @@ -from worlds.stardew_valley.options import stardew_valley_option_classes +from ... import StardewValleyWorld options_to_exclude = ["profit_margin", "starting_money", "multiple_day_sleep_enabled", "multiple_day_sleep_cost", "experience_multiplier", "friendship_multiplier", "debris_multiplier", - "quick_start", "gifting", "gift_tax"] -options_to_include = [option_to_include for option_to_include in stardew_valley_option_classes - if option_to_include.internal_name not in options_to_exclude] + "quick_start", "gifting", "gift_tax", "progression_balancing", "accessibility", "start_inventory", "start_hints", "death_link"] + +options_to_include = [option for option_name, option in StardewValleyWorld.options_dataclass.type_hints.items() + if option_name not in options_to_exclude] diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index a3198e4d2a2..02fd30a6b11 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -4,21 +4,21 @@ import sys from BaseClasses import MultiWorld -from worlds.stardew_valley.test import setup_solo_multiworld -from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestBase -from worlds.stardew_valley import options, locations, items, Group, ItemClassification, StardewOptions -from worlds.stardew_valley.mods.mod_data import ModNames -from worlds.stardew_valley.regions import RandomizationFlag, create_final_connections, randomize_connections, create_final_regions -from worlds.stardew_valley.items import item_table, items_by_group -from worlds.stardew_valley.locations import location_table, LocationTags -from worlds.stardew_valley.options import stardew_valley_option_classes, Mods, EntranceRandomization - -mod_list = ["DeepWoods", "Tractor Mod", "Bigger Backpack", - "Luck Skill", "Magic", "Socializing Skill", "Archaeology", - "Cooking Skill", "Binning Skill", "Juna - Roommate NPC", - "Professor Jasper Thomas", "Alec Revisited", "Custom NPC - Yoba", "Custom NPC Eugene", - "'Prophet' Wellwick", "Mister Ginger (cat npc)", "Shiko - New Custom NPC", "Delores - Custom NPC", - "Ayeisha - The Postal Worker (Custom NPC)", "Custom NPC - Riley", "Skull Cavern Elevator"] +from ...mods.mod_data import ModNames +from .. import setup_solo_multiworld +from ..TestOptions import basic_checks, SVTestBase +from ... import items, Group, ItemClassification +from ...regions import RandomizationFlag, create_final_connections, randomize_connections, create_final_regions +from ...items import item_table, items_by_group +from ...locations import location_table +from ...options import Mods, EntranceRandomization, Friendsanity, SeasonRandomization, SpecialOrderLocations, ExcludeGingerIsland, TrapItems + +all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, + ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, + ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, + ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, + ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, + ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator}) def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld): @@ -37,54 +37,27 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase class TestGenerateModsOptions(SVTestBase): def test_given_single_mods_when_generate_then_basic_checks(self): - for mod in mod_list: + for mod in all_mods: with self.subTest(f"Mod: {mod}"): multi_world = setup_solo_multiworld({Mods: mod}) basic_checks(self, multi_world) check_stray_mod_items(mod, self, multi_world) - def test_given_mod_pairs_when_generate_then_basic_checks(self): - if self.skip_long_tests: - return - num_mods = len(mod_list) - for mod1_index in range(0, num_mods): - for mod2_index in range(mod1_index + 1, num_mods): - mod1 = mod_list[mod1_index] - mod2 = mod_list[mod2_index] - mods = (mod1, mod2) - with self.subTest(f"Mods: {mods}"): - multiworld = setup_solo_multiworld({Mods: mods}) - basic_checks(self, multiworld) - check_stray_mod_items(list(mods), self, multiworld) - def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basic_checks(self): for option in EntranceRandomization.options: - for mod in mod_list: + for mod in all_mods: with self.subTest(f"entrance_randomization: {option}, Mod: {mod}"): multiworld = setup_solo_multiworld({EntranceRandomization.internal_name: option, Mods: mod}) basic_checks(self, multiworld) check_stray_mod_items(mod, self, multiworld) - def test_given_mod_names_when_generate_paired_with_other_options_then_basic_checks(self): - if self.skip_long_tests: - return - for option in stardew_valley_option_classes: - if not option.options: - continue - for value in option.options: - for mod in mod_list: - with self.subTest(f"{option.internal_name}: {value}, Mod: {mod}"): - multiworld = setup_solo_multiworld({option.internal_name: option.options[value], Mods: mod}) - basic_checks(self, multiworld) - check_stray_mod_items(mod, self, multiworld) - class TestBaseItemGeneration(SVTestBase): options = { - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, - options.Mods.internal_name: mod_list + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, + SeasonRandomization.internal_name: SeasonRandomization.option_progressive, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + Mods.internal_name: all_mods } def test_all_progression_items_are_added_to_the_pool(self): @@ -105,10 +78,10 @@ def test_all_progression_items_are_added_to_the_pool(self): class TestNoGingerIslandModItemGeneration(SVTestBase): options = { - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, - options.Mods.internal_name: mod_list + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, + SeasonRandomization.internal_name: SeasonRandomization.option_progressive, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + Mods.internal_name: all_mods } def test_all_progression_items_except_island_are_added_to_the_pool(self): @@ -134,29 +107,31 @@ class TestModEntranceRando(unittest.TestCase): def test_mod_entrance_randomization(self): - for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), - (options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), - (options.EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: + for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), + (EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), + (EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: with self.subTest(option=option, flag=flag): seed = random.randrange(sys.maxsize) rand = random.Random(seed) - world_options = StardewOptions({options.EntranceRandomization.internal_name: option, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, - options.Mods.internal_name: mod_list}) - final_regions = create_final_regions(world_options) - final_connections = create_final_connections(world_options) + world_options = {EntranceRandomization.internal_name: option, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + Mods.internal_name: all_mods} + multiworld = setup_solo_multiworld(world_options) + world = multiworld.worlds[1] + final_regions = create_final_regions(world.options) + final_connections = create_final_connections(world.options) regions_by_name = {region.name: region for region in final_regions} - _, randomized_connections = randomize_connections(rand, world_options, regions_by_name) + _, randomized_connections = randomize_connections(rand, world.options, regions_by_name) for connection in final_connections: if flag in connection.flag: connection_in_randomized = connection.name in randomized_connections reverse_in_randomized = connection.reverse in randomized_connections self.assertTrue(connection_in_randomized, - f"Connection {connection.name} should be randomized but it is not in the output. Seed = {seed}") + f"Connection {connection.name} should be randomized but it is not in the output. Seed = {seed}") self.assertTrue(reverse_in_randomized, - f"Connection {connection.reverse} should be randomized but it is not in the output. Seed = {seed}") + f"Connection {connection.reverse} should be randomized but it is not in the output. Seed = {seed}") self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()), f"Connections are duplicated in randomization. Seed = {seed}") @@ -164,12 +139,11 @@ def test_mod_entrance_randomization(self): class TestModTraps(SVTestBase): def test_given_traps_when_generate_then_all_traps_in_pool(self): - trap_option = options.TrapItems - for value in trap_option.options: + for value in TrapItems.options: if value == "no_traps": continue world_options = self.allsanity_options_without_mods() - world_options.update({options.TrapItems.internal_name: trap_option.options[value], Mods: "Magic"}) + world_options.update({TrapItems.internal_name: TrapItems.options[value], Mods: "Magic"}) multi_world = setup_solo_multiworld(world_options) trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups] multiworld_items = [item.name for item in multi_world.get_items()] diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index f5e04b4ebc1..0d8fac33371 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -12,7 +12,7 @@ MultiWorld, Item, CollectionState, Entrance, Tutorial from .logic import cs_to_zz_locs from .region import ZillionLocation, ZillionRegion -from .options import ZillionStartChar, zillion_options, validate +from .options import ZillionOptions, ZillionStartChar, validate from .id_maps import item_name_to_id as _item_name_to_id, \ loc_name_to_id as _loc_name_to_id, make_id_to_others, \ zz_reg_name_to_reg_name, base_id @@ -70,7 +70,9 @@ class ZillionWorld(World): game = "Zillion" web = ZillionWebWorld() - option_definitions = zillion_options + options_dataclass = ZillionOptions + options: ZillionOptions + settings: typing.ClassVar[ZillionSettings] topology_present = True # indicate if world type has any meaningful layout/pathing @@ -142,7 +144,10 @@ def generate_early(self) -> None: if not hasattr(self.multiworld, "zillion_logic_cache"): setattr(self.multiworld, "zillion_logic_cache", {}) - zz_op, item_counts = validate(self.multiworld, self.player) + zz_op, item_counts = validate(self.options) + + if zz_op.early_scope: + self.multiworld.early_items[self.player]["Scope"] = 1 self._item_counts = item_counts @@ -299,7 +304,8 @@ def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: elif start_char_counts["Champ"] > start_char_counts["Apple"]: to_stay = "Champ" else: # equal - to_stay = multiworld.random.choice(("Apple", "Champ")) + choices: Tuple[Literal['Apple', 'Champ', 'JJ'], ...] = ("Apple", "Champ") + to_stay = multiworld.random.choice(choices) for p, sc in players_start_chars: if sc != to_stay: diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 6aa88f5b220..80f9469ec8c 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,13 +1,14 @@ from collections import Counter -# import logging -from typing import TYPE_CHECKING, Any, Dict, Tuple, cast -from Options import AssembleOptions, DefaultOnToggle, Range, SpecialRange, Toggle, Choice +from dataclasses import dataclass +from typing import Dict, Tuple +from typing_extensions import TypeGuard # remove when Python >= 3.10 + +from Options import DefaultOnToggle, PerGameCommonOptions, Range, SpecialRange, Toggle, Choice + from zilliandomizer.options import \ Options as ZzOptions, char_to_gun, char_to_jump, ID, \ VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts from zilliandomizer.options.parsing import validate as zz_validate -if TYPE_CHECKING: - from BaseClasses import MultiWorld class ZillionContinues(SpecialRange): @@ -41,6 +42,19 @@ class VBLR(Choice): option_restrictive = 3 default = 1 + def to_zz_vblr(self) -> ZzVBLR: + def is_vblr(o: str) -> TypeGuard[ZzVBLR]: + """ + This function is because mypy doesn't support narrowing with `in`, + https://github.com/python/mypy/issues/12535 + so this is the only way I see to get type narrowing to `Literal`. + """ + return o in ("vanilla", "balanced", "low", "restrictive") + + key = self.current_key + assert is_vblr(key), f"{key=}" + return key + class ZillionGunLevels(VBLR): """ @@ -225,27 +239,27 @@ class ZillionRoomGen(Toggle): display_name = "room generation" -zillion_options: Dict[str, AssembleOptions] = { - "continues": ZillionContinues, - "floppy_req": ZillionFloppyReq, - "gun_levels": ZillionGunLevels, - "jump_levels": ZillionJumpLevels, - "randomize_alarms": ZillionRandomizeAlarms, - "max_level": ZillionMaxLevel, - "start_char": ZillionStartChar, - "opas_per_level": ZillionOpasPerLevel, - "id_card_count": ZillionIDCardCount, - "bread_count": ZillionBreadCount, - "opa_opa_count": ZillionOpaOpaCount, - "zillion_count": ZillionZillionCount, - "floppy_disk_count": ZillionFloppyDiskCount, - "scope_count": ZillionScopeCount, - "red_id_card_count": ZillionRedIDCardCount, - "early_scope": ZillionEarlyScope, - "skill": ZillionSkill, - "starting_cards": ZillionStartingCards, - "room_gen": ZillionRoomGen, -} +@dataclass +class ZillionOptions(PerGameCommonOptions): + continues: ZillionContinues + floppy_req: ZillionFloppyReq + gun_levels: ZillionGunLevels + jump_levels: ZillionJumpLevels + randomize_alarms: ZillionRandomizeAlarms + max_level: ZillionMaxLevel + start_char: ZillionStartChar + opas_per_level: ZillionOpasPerLevel + id_card_count: ZillionIDCardCount + bread_count: ZillionBreadCount + opa_opa_count: ZillionOpaOpaCount + zillion_count: ZillionZillionCount + floppy_disk_count: ZillionFloppyDiskCount + scope_count: ZillionScopeCount + red_id_card_count: ZillionRedIDCardCount + early_scope: ZillionEarlyScope + skill: ZillionSkill + starting_cards: ZillionStartingCards + room_gen: ZillionRoomGen def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: @@ -262,47 +276,34 @@ def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: return tr -def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": +def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": """ adjusts options to make game completion possible - `world` parameter is MultiWorld object that has my options on it - `p` is my player id + `options` parameter is ZillionOptions object that was put on my world by the core """ - for option_name in zillion_options: - assert hasattr(world, option_name), f"Zillion option {option_name} didn't get put in world object" - wo = cast(Any, world) # so I don't need getattr on all the options - skill = wo.skill[p].value + skill = options.skill.value - jump_levels = cast(ZillionJumpLevels, wo.jump_levels[p]) - jump_option = jump_levels.current_key - required_level = char_to_jump["Apple"][cast(ZzVBLR, jump_option)].index(3) + 1 + jump_option = options.jump_levels.to_zz_vblr() + required_level = char_to_jump["Apple"][jump_option].index(3) + 1 if skill == 0: # because of hp logic on final boss required_level = 8 - gun_levels = cast(ZillionGunLevels, wo.gun_levels[p]) - gun_option = gun_levels.current_key - guns_required = char_to_gun["Champ"][cast(ZzVBLR, gun_option)].index(3) + gun_option = options.gun_levels.to_zz_vblr() + guns_required = char_to_gun["Champ"][gun_option].index(3) - floppy_req = cast(ZillionFloppyReq, wo.floppy_req[p]) + floppy_req = options.floppy_req - card = cast(ZillionIDCardCount, wo.id_card_count[p]) - bread = cast(ZillionBreadCount, wo.bread_count[p]) - opa = cast(ZillionOpaOpaCount, wo.opa_opa_count[p]) - gun = cast(ZillionZillionCount, wo.zillion_count[p]) - floppy = cast(ZillionFloppyDiskCount, wo.floppy_disk_count[p]) - scope = cast(ZillionScopeCount, wo.scope_count[p]) - red = cast(ZillionRedIDCardCount, wo.red_id_card_count[p]) item_counts = Counter({ - "ID Card": card, - "Bread": bread, - "Opa-Opa": opa, - "Zillion": gun, - "Floppy Disk": floppy, - "Scope": scope, - "Red ID Card": red + "ID Card": options.id_card_count, + "Bread": options.bread_count, + "Opa-Opa": options.opa_opa_count, + "Zillion": options.zillion_count, + "Floppy Disk": options.floppy_disk_count, + "Scope": options.scope_count, + "Red ID Card": options.red_id_card_count }) minimums = Counter({ "ID Card": 0, @@ -335,10 +336,10 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": item_counts["Empty"] += diff assert sum(item_counts.values()) == 144 - max_level = cast(ZillionMaxLevel, wo.max_level[p]) + max_level = options.max_level max_level.value = max(required_level, max_level.value) - opas_per_level = cast(ZillionOpasPerLevel, wo.opas_per_level[p]) + opas_per_level = options.opas_per_level while (opas_per_level.value > 1) and (1 + item_counts["Opa-Opa"] // opas_per_level.value < max_level.value): # logging.warning( # "zillion options validate: option opas_per_level incompatible with options max_level and opa_opa_count" @@ -347,39 +348,34 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": # that should be all of the level requirements met - name_capitalization = { + name_capitalization: Dict[str, Chars] = { "jj": "JJ", "apple": "Apple", "champ": "Champ", } - start_char = cast(ZillionStartChar, wo.start_char[p]) + start_char = options.start_char start_char_name = name_capitalization[start_char.current_key] assert start_char_name in chars - start_char_name = cast(Chars, start_char_name) - - starting_cards = cast(ZillionStartingCards, wo.starting_cards[p]) - room_gen = cast(ZillionRoomGen, wo.room_gen[p]) + starting_cards = options.starting_cards - early_scope = cast(ZillionEarlyScope, wo.early_scope[p]) - if early_scope: - world.early_items[p]["Scope"] = 1 + room_gen = options.room_gen zz_item_counts = convert_item_counts(item_counts) zz_op = ZzOptions( zz_item_counts, - cast(ZzVBLR, jump_option), - cast(ZzVBLR, gun_option), + jump_option, + gun_option, opas_per_level.value, max_level.value, False, # tutorial skill, start_char_name, floppy_req.value, - wo.continues[p].value, - wo.randomize_alarms[p].value, - False, # early scope is done with AP early_items API + options.continues.value, + bool(options.randomize_alarms.value), + bool(options.early_scope.value), True, # balance defense starting_cards.value, bool(room_gen.value) diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt index 4858ef3153c..93d2dbc1a6e 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1 +1,2 @@ zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@d7122bcbeda40da5db26d60fad06246a1331706f#0.5.4 +typing-extensions>=4.7, <5 diff --git a/worlds/zillion/test/TestOptions.py b/worlds/zillion/test/TestOptions.py index 1ec186dae50..c4f02d4bd3b 100644 --- a/worlds/zillion/test/TestOptions.py +++ b/worlds/zillion/test/TestOptions.py @@ -1,6 +1,6 @@ from . import ZillionTestBase -from worlds.zillion.options import ZillionJumpLevels, ZillionGunLevels, validate +from worlds.zillion.options import ZillionJumpLevels, ZillionGunLevels, ZillionOptions, validate from zilliandomizer.options import VBLR_CHOICES @@ -9,7 +9,9 @@ class OptionsTest(ZillionTestBase): def test_validate_default(self) -> None: self.world_setup() - validate(self.multiworld, 1) + options = self.multiworld.worlds[1].options + assert isinstance(options, ZillionOptions) + validate(options) def test_vblr_ap_to_zz(self) -> None: """ all of the valid values for the AP options map to valid values for ZZ options """ @@ -20,7 +22,9 @@ def test_vblr_ap_to_zz(self) -> None: for value in vblr_class.name_lookup.values(): self.options = {option_name: value} self.world_setup() - zz_options, _item_counts = validate(self.multiworld, 1) + options = self.multiworld.worlds[1].options + assert isinstance(options, ZillionOptions) + zz_options, _item_counts = validate(options) assert getattr(zz_options, option_name) in VBLR_CHOICES # TODO: test validate with invalid combinations of options